Go HTTP编程

当你在浏览器中输入URL时,实际上是在发送一个对Web页面的请求。该请求被发送到服务器。服务器的工作是获取适当的页面并将其作为响应发送回浏览器。

在Web的早期,服务器通常读取服务器硬盘上HTML文件的内容并将该HTML发送回浏览器。

但今天,服务器更常见的是与程序通信来完成请求,而不是读取文件。这个程序可以用任何你想要的语言编写,包括Go!

net/http介绍

Go语言标准库内建提供了net/http包,涵盖了HTTP客户端和服务端的具体实现。使用net/http包,我们可以很方便地编写HTTP客户端或服务端的程序。

服务端

默认的Server

首先,我们编写一个最简单的Web服务器。编写这个Web服务只需要两步:

  1. 注册一个处理器函数(注册到DefaultServeMux);
  2. 设置监听的TCP地址并启动服务;

对应到我们的代码里就是这样的:

package main

import (
	"log"
	"net/http"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("Hello World!"))
}
func main() {

	//1.注册一个给定模式的处理器函数到DefaultServeMux
	http.HandleFunc("/", sayHello)

	//2.设置监听的TCP地址并启动服务
	//参数1:TCP地址(IP+Port)
	//参数2:当设置为nil时表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}

运行该程序,通过浏览器访问,可以看到Hello World!显示在了页面上

ListenAndServe使用指定的监听地址和处理器启动一个HTTP服务端。当处理器参数是nil时,这表示采用包变量DefaultServeMux作为处理器。

Handle和HandleFunc函数可以向DefaultServeMux添加任意多个处理器函数。

http.HandleFunc

使用Go语言中的net/http包来编写一个简单的接收HTTP请求的Server端示例,net/http包是对net包的进一步封装,专门用来处理HTTP协议的数据。具体的代码如下:

处理器函数的实现原理:

通过源码可知,这个函数实际上是调用了默认的DefaultServeMux的HandleFunc方法,这也解释了我们第一步里所说的默认的实际注册到DefaultServeMux。

既然说了http.ListenAndServe的第二个参数为nil时采用默认的DefaultServeMux,那么如果我们不想采用默认的,而是想自己创建一个ServerMux该怎么办呢,http给我们提供了方法,NewServeMux创建并返回一个新的*ServeMux

// NewServeMux分配并返回一个新的ServeMux。
func NewServeMux() *ServeMux { return new(ServeMux) }

如果是我们自己创建的ServeMux,我们只需要简单的更新一下代码:

package main

import (
	"log"
	"net/http"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("Hello World!"))
}
func main() {

	//1.创建一个ServeMux,将处理器函数注册到serverMux
	serveMux := http.NewServeMux()
	serveMux.HandleFunc("/", sayHello)

	//2.设置监听的TCP地址并启动服务
	//参数1:TCP地址(IP+Port)
	//参数2:当设置为nil时表示使用DefaultServeMux,如果指定了则表示使用自定义ServeMux
	err := http.ListenAndServe("127.0.0.1:8080", serveMux)
	log.Fatal(err)
}

运行修改后的代码,和采用默认ServeMux一样正常运行

http.Handle

如果是使用http的handle方法,则handle的第二个参数需要实现handler接口,要想实现这个接口,就得实现这个接口的serveHTTP方法

示例:

package main

import (
	"log"
	"net/http"
)
type MyHandler struct {}

func (m *MyHandler)ServeHTTP(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("Hello World!"))
}
func main() {

	//1.创建一个ServeMux,将处理器函数注册到serverMux
	http.Handle("/", &MyHandler{})

	//2.设置监听的TCP地址并启动服务
	//参数1:TCP地址(IP+Port)
	//参数2:当设置为nil时表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}

http.Request

一个Web服务器最基本的工作就是接收请求,做出响应。http包帮助我们封装了一个Request结构体,我们通过这个结构体拿到很多用户的一次HTTP请求的所有信息。这个Request结构体定义如下:

type Request struct {
    //Method指定HTTP方法(GET、POST、PUT等)
    //Go的客户端不支持CONNECT方法
	Method string

    //请求URL相关信息
	URL *url.URL

    //接收到的请求的协议版本。HTTP客户端代码始终使用HTTP/1.1或HTTP2
	Proto      string // "HTTP/1.0"

	//请求头相关信息
	Header Header

	//请求主体
	Body io.ReadCloser
    
    //客户端IP地址
	RemoteAddr string
    
    //请求URI
    RequestURI string
    
    .......
    
	//内容上下文
	ctx context.Context
}

我这里列举的并不是完整的Request结构体定义,只是大致的说明一下。完整的定义以及这些字段的中文含义可以查看Go语言标准库文档

我们通过通过浏览器可以发现,我们一次HTTP请求会携带很多信息

这些信息,我们可以通过http.Request来获取到

package main

import (
	"fmt"
	"log"
	"net/http"
)

func HeaderInfo(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	fmt.Printf("Method: %#v\n", r.Method)
	fmt.Printf("URL: %#v\n", r.URL)
	fmt.Printf("Header: %#v\n", r.Header)
	fmt.Printf("Body: %#v\n", r.Body)
	fmt.Printf("RemoteAddr: %#v\n", r.RemoteAddr)
	_, _ = w.Write([]byte("请求成功,请在终端查看!"))
}
func main() {

	//1.注册一个给定模式的处理器函数到DefaultServeMux
	http.HandleFunc("/header", HeaderInfo)

	//2.设置监听的TCP地址并启动服务
	//参数1:TCP地址(IP+Port)
	//参数2:当设置为nil时表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}

自定义Server

要管理服务端的行为,可以创建一个自定义的Server:

import (
	"fmt"
	"net/http"
	"time"
)
type MyHandler struct {}

func (h *MyHandler) ServeHTTP (w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello world!")
}

func main() {
	var handler MyHandler
	var server = http.Server{
		Addr:              ":8080",
		Handler:           &handler,
		ReadTimeout:       2 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	var err = server.ListenAndServe()
	if err != nil {
		fmt.Printf("http server failed, err: %v\n", err)
		return
	}
}

HTML模板

template包(html/template)实现了数据驱动的模板,以生成可防止代码注入的HTML输出。它提供了与text/template包相同的接口,并且只要实在输出HTML的场景,就应该使用html/template而不是text/template。

Go语言模板的使用可以分为三步:定义模板文件、解析模板文件和渲染模板文件。

定义模板文件

定义模板文件时需要我们按照相关语法规则去编写,后面会详细介绍。

解析模板文件

上面定义好了模板文件之后,可以使用下面额常用方法去解析模板文件,得到模板对象:

func ParseFS(fs fs.FS, patterns ...string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)
func ParseGlob(pattern string) (*Template, error)

当然,你也可以使用func New(name string) *Template函数创建一个名为name的模板,然后对其调用上面的方法去解析模板字符串或模板文件。

模板渲染

渲染模板简单来说就是使用数据去填充模板,当然实际上可能会复杂很多。

func (t *Template) Execute(wr io.Writer, data interface{}) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

基本示例

定义模板文件

我们按照Go模板语法定义一个index.html的模板文件,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <h2>{{.}}</h2>
    <img src="./img/pyy.jpeg" style="width: 30%" alt="彭于晏">
</body>
</html>
解析和渲染模板

创建一个main.go文件,服务端代码如下

package main

import (
	"html/template"
	"log"
	"net/http"
)

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定文件生成模板对象
	tmpl, err := template.ParseFiles("./public/index.html")
	if err != nil {
		log.Fatal(err)
	}
	err = tmpl.Execute(w, "彭于晏")
	if err != nil {
		log.Fatal(err)
	}
}

func main() {

	//1.注册给定模式的处理器函数到DefaultServeMux
	files := http.FileServer(http.Dir("./public"))
	http.Handle("/", files)
	http.HandleFunc("/index", index)

	//2.设置监听的TCP地址并启动服务
	//参数1:TCP地址(IP+Port)
	//参数2:当设置为nil时表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}

项目结构如下:

.
├── go.mod
├── main.go
├── public
    ├── img
    │   └── pyy.jpeg
    └── index.html

将上面的main.go文件编译执行,然后使用浏览器访问http://127.0.0.1:8080就能看到页面上显示了“彭于晏”。 这就是一个最简单的模板渲染的示例,Go语言模板引擎详细用法请往下阅读。

{{.}}

模板语法都包含在{{}}中间,其中{{.}}中的点表示当前对象。

当我们传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。例如:

package main

import (
	"html/template"
	"log"
	"net/http"
)
type Address struct {
	Province string
	City     string
}
type User struct {
	Name string
	Age  int
	Addr Address
}

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定文件生成模板对象
	tmpl, err := template.ParseFiles("./public/index.html")
	if err != nil {
		log.Fatal(err)
	}
	user := User{
		Name: "彭于晏",
		Age:  28,
		Addr: Address{Province: "台湾省", City: "澎湖县"},
	}
	//利用给定数据渲染模板,并将结果写入
	err = tmpl.Execute(w, user)
	if err != nil {
		log.Fatal(err)
	}
}

func main() {

	//1.注册给定模式的处理器函数到DefaultServeMux
	files := http.FileServer(http.Dir("./public"))
	http.Handle("/", files)
	http.HandleFunc("/index", index)

	//2.设置监听的TCP地址并启动服务
	//参数1:TCP地址(IP+Port)
	//参数2:当设置为nil时表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)
}

模板文件index.html内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <p>姓名:{{.Name}}</p>
    <p>年龄:{{.Age}}</p>
    <p>地址:{{.Addr.Province}}-{{.Addr.City}}</p>
    <p>你好呀{{/* 这是注释 */}},彭于晏</p>
    <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
</body>
</html>

显示效果如下图所示

同理,当我们传入的变量是map时,也可以在模板文件中通过.根据key来取值。

注意:在模板中使用的变量应当是可导出的,否则的话对应的变量无法显示。

注释
//注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。
{{/* 这是注释 */}}
pipeline

pipeline是指产生数据的操作。比如{{.}}{{.Name}}等。Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。

注意:并不是只有使用了|才是pipeline。Go的模板语法中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline

示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <div align="center">
        <p>姓名:{{.Name|len}}</p>
        <p>年龄:{{.Age}}</p>
        <p>地址:{{.Addr.Province}}-{{.Addr.City}}</p>
        <p>你好呀{{/* 这是注释 */}},彭于晏</p>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
变量

我们还可以在模板中声明变量,用来保存传入模板的数据或其他语句生成的结果。具体语法如下:

$obj := {{.}}

其中$obj是变量的名字,在后续的代码中就可以使用该变量了。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <div align="center">
        {{$obj := .Name}}
        <p>姓名:{{ $obj }}</p>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
移除空格

template引擎在进行替换的时候,是完全按照文本格式进行替换的。除了需要评估和替换的地方,所有的行分隔符、空格等等空白都原样保留。所以,对于要解析的内容,不要随意缩进、随意换行

有时候我们在使用模板语法的时候会不可避免的引入一下空格或者换行符,这样模板最终渲染出来的内容可能就和我们想的不一样,这个时候可以使用{{-语法去除模板内容左侧的所有空白符号, 使用-}}去除模板内容右侧的所有空白符号。

注意-要紧挨{{}},同时与模板值之间需要使用空格分隔。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <div align="center">
        <p>姓名:{{- .Name -}} -king</p> <!-- 彭于晏-king -->
        <p>姓名:{{ .Name }} -king</p> <!-- 彭于晏 -king -->
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
if语句

Go模板语法中的条件判断有以下几种:

{{if pipeline}} T1 {{end}}

{{if pipeline}} T1 {{else}} T0 {{end}}

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}

需要注意的是,pipeline为false的情况是各种数据对象的0值:数值0,指针或接口是nil,数组、slice、map或string则是len为0。

示例:

<!-- 由于.Name不是空字符串,所以可以渲染出彭于晏 -->
<p>姓名:{{if .Name}} 彭于晏 {{end}}</p>
range

Go的模板语法中使用range关键字进行遍历,有以下两种写法,其中pipeline的值必须是数组、切片、map或者channel。

{{range pipeline}} T1 {{end}}
如果pipeline的值其长度为0,不会有任何输出

{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其长度为0,则会执行T0。

需注意的是,range的参数部分是pipeline,所以在迭代的过程中是可以进行赋值的。但有两种赋值情况:

{{range $value := .}}
{{range $key,$value := .}}

如果range中只赋值给一个变量,则这个变量是当前正在迭代元素的值。如果赋值给两个变量,则第一个变量是索引值(数组/切片是数值,map是key),第二个变量是当前正在迭代元素的值。

示例:

index.html文件

{{range $x := .}}
<p>{{$x}}</p>
{{else}}
<p>这是一个空切片</p>
{{end}}

也可以这样

{{range .}}
<p>{{.}}</p>
{{else}}
<p>这是一个空切片</p>
{{end}}

main.go文件

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定文件生成模板对象
	tmpl, err := template.ParseFiles("./public/index.html")
	if err != nil {
		log.Fatal(err)
	}
	var arr []int
	//利用给定数据渲染模板,并将结果写入
	err = tmpl.Execute(w, arr)
	if err != nil {
		log.Fatal(err)
	}
}
with
{{with pipeline}} T1 {{end}}
如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。

{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。

对于第一种格式,当pipeline不为0值的时候,点"."设置为pipeline运算的值,否则跳过。对于第二种格式,当pipeline为0值时,执行else语句块,否则"."设置为pipeline运算的值,并执行T1。

示例:

<p>{{with "彭于晏"}} {{.}} {{end}}</p>

上面将输出彭于晏,因为"."已经设置为"彭于晏"。

比较函数

布尔函数会将任何类型的零值视为假,其余视为真。

下面是定义为函数的二元比较运算的集合:

eq      如果arg1 == arg2则返回真
ne      如果arg1 != arg2则返回真
lt      如果arg1 < arg2则返回真
le      如果arg1 <= arg2则返回真
gt      如果arg1 > arg2则返回真
ge      如果arg1 >= arg2则返回真

为了简化多参数相等检测,eq(只有eq)可以接受2个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:

{{eq arg1 arg2 arg3}}

等价于

arg1==arg2 || arg1==arg3

比较函数只适用于基本类型(或重定义的基本类型,如”type Celsius float32”)。但是,整数和浮点数不能互相比较。

示例:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <div align="center">
        {{if (eq .Name "彭于晏") }}
        <p>彭于晏</p>
        {{else}}
        <p>晏晏</p>
        {{end}}
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
预定义函数

预定义的全局函数如下:

and
    函数返回它的第一个empty参数或者最后一个参数;
    就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
    返回第一个非empty参数或者最后一个参数;
    亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
    返回它的单个参数的布尔值的否定
len
    返回它的参数的整数类型长度
index
    执行结果为第一个参数以剩下的参数为索引/键指向的值;
    如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    返回与其参数的文本表示形式等效的转义HTML。
    这个函数在html/template中不可用。
urlquery
    以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。
    这个函数在html/template中不可用。
js
    返回与其参数的文本表示形式等效的转义JavaScript。
call
    执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
    如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
    其中Y是函数类型的字段或者字典的值,或者其他类似情况;
    call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
    该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
    如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;

示例:

<div align="center">
    <!-- if (.Name == '彭于晏' && .Age == 28)  -->
    {{if (and (eq .Name "彭于晏") (eq .Age 28)) }}
    <p>彭于晏</p>
    {{else}}
    <p>晏晏</p>
    {{end}}
</div>
自定义函数

Go的模板支持自定义函数。需要注意的是自定义函数必须在解析模板之前。

main.go文件

func index(w http.ResponseWriter, r *http.Request) {
	fn := func(name string) string {
		return name + ",你好!"
	}
	//自定义函数必须在解析模板之前
	tmpl := template.New("index.html").Funcs(template.FuncMap{"hello": fn})
	//解析指定文件生成模板对象
	tmpl, err := tmpl.ParseFiles("./public/index.html")
	if err != nil {
		log.Fatal(err)
	}
	var user = User{Name: "彭于晏", Age: 28}
	//利用给定数据渲染模板,并将结果写入
	err = tmpl.Execute(w, user)
	if err != nil {
		log.Fatal(err)
	}
}

index.html文件

<p>{{hello .Name}}</p>
嵌套template

我们可以在template中嵌套其他的template。这个template可以是单独的文件,也可以是通过define定义的template。

举个例子: index.html文件内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <div align="center">
        <p>嵌套template示例</p>
        <br>
        {{template "ul.html"}}
        <br>
        {{template "ol.tmpl"}}
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>
{{define "ol.tmpl"}}
<ol>
    <li>彭于晏</li>
    <li>高圆圆</li>
    <li>赵又廷</li>
</ol>
{{end}}

ul.html文件内容如下:

<ul>
  <li>彭于晏</li>
  <li>高圆圆</li>
  <li>赵又廷</li>
</ul>

main.go文件

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定文件生成模板对象
	tmpl, err := template.ParseFiles("./public/index.html", "./public/ul.html")
	if err != nil {
		log.Fatal(err)
	}

	var user = User{Name: "彭于晏", Age: 28}
	//利用给定数据渲染模板,并将结果写入
	err = tmpl.Execute(w, user)
	if err != nil {
		log.Fatal(err)
	}
}

注意:在解析模板时,被嵌套的模板一定要在后面解析,例如上面的示例中index.html模板中嵌套了ul.html,所以ul.html要在index.html后进行解析。

block

根据官方文档的解释:block等价于define定义一个名为name的模板,并在"有需要"的地方执行这个模板,执行时将"."设置为pipeline的值。

{{block "name" pipeline}} T1 {{end}}

但应该注意,block的第一个动作是执行名为name的模板,如果不存在,则在此处自动定义这个模板,并执行这个临时定义的模板。换句话说,block可以认为是设置一个默认模板

例如:

{{block "T1" .}} one {{end}}

它首先表示{{template "T1" .}},也就是说先找到T1模板,如果T1存在,则执行找到的T1,如果没找到T1,则临时定义一个{{define "T1"}} one {{end}},并执行它。

示例:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <div align="center">
        <p>block用法示例</p>
        {{block "content" .}}
            <p style="color: blue;">彭于晏</p>
        {{end}}
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>

red.html

{{define "content"}}
  <p style="color: red;">彭于晏</p>
{{end}}

main.go

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定文件生成模板对象
	var tmpl *template.Template
	var err error
	if rand.Intn(2) > 0 {
		tmpl, err = template.ParseFiles("./public/index.html", "./public/red.html")
	} else {
		tmpl, err = template.ParseFiles("./public/index.html")
	}
	if err != nil {
		log.Fatal(err)
	}

	//利用给定数据渲染模板,并将结果写入
	err = tmpl.Execute(w, nil)
	if err != nil {
		log.Fatal(err)
	}
}

这样示例的效果就是刷新页面,彭于晏的名字颜色在红蓝之间反复变化

标识符

Go标准库的模板引擎使用的花括号{{}}作为标识,而许多前端框架(如VueAngularJS)也使用{{}}作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:

template.New("index.html").Delims("{[", "]}").ParseFiles("./public/index.html")
上下文感知

对于html/template包,有一个很好用的功能:上下文感知。text/template没有该功能。

上下文感知具体指的是根据所处环境css、js、html、url的path、url的query,自动进行不同格式的转义。

例如,一个handler函数的代码如下:

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定文件生成模板对象
	tmpl, err := template.ParseFiles("./public/index.html")

	if err != nil {
		log.Fatal(err)
	}

	str := `I asked: <i>"What's up?"</i>`
	//利用给定数据渲染模板,并将结果写入
	err = tmpl.Execute(w, str)
	if err != nil {
		log.Fatal(err)
	}
}

上面str是Execute的第二个参数,它的内容是包含了特殊符号的字符串。

下面是index.html文件的内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <div align="center">
        <p>html/template上下文感知示例</p>
        <div>{{ . }}</div>
        <div><a href="/{{ . }}">Path</a></div>
        <div><a href="/?q={{ . }}">Query</a></div>
        <div><a onclick="f('{{ . }}')">Onclick</a></div>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>

上面index.html中有4个不同的环境,分别是html环境、url的path环境、url的query环境以及js环境。虽然对象都是{{.}},但解析执行后的值是不一样的。如果使用curl获取源代码,结果将如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <div align="center">
        <p>html/template上下文感知示例</p>
        <div>I asked: &lt;i&gt;&#34;What&#39;s up?&#34;&lt;/i&gt;</div>
        <div><a href="/I%20asked:%20%3ci%3e%22What%27s%20up?%22%3c/i%3e">Path</a></div>
        <div><a href="/?q=I%20asked%3a%20%3ci%3e%22What%27s%20up%3f%22%3c%2fi%3e">Query</a></div>
        <div><a onclick="f('I asked: \u003ci\u003e\u0022What\u0027s up?\u0022\u003c\/i\u003e')">Onclick</a></div>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>

html/template针对的是需要返回HTML内容的场景,在模板渲染过程中会对一些有风险的内容进行转义,以此来防范跨站脚本攻击。

例如,定义下面的模板文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index页面</title>
</head>
<body>
    <div align="center">
        <p>html/template上下文感知示例</p>
        {{ . }}
        <br>
        <img src="./img/pyy.jpeg" style="width: 10%" alt="彭于晏">
    </div>
</body>
</html>

handler控制器代码如下:

func index(w http.ResponseWriter, r *http.Request) {
	//解析指定文件生成模板对象
	tmpl, err := template.ParseFiles("./public/index.html")

	if err != nil {
		log.Fatal(err)
	}
	str := `<script>alert('嘿嘿嘿')</script>`
	//利用给定数据渲染模板,并将结果写入
	err = tmpl.Execute(w, str)
	if err != nil {
		log.Fatal(err)
	}
}

运行代码显示如下

如果我们将包由html/template改为text/template,重新运行代码,可以发现被注入了js代码

客户端

http包提供了很多访问Web服务器的函数,比如http.Get()http.Post()http.Head()等,读到的响应报文数据被保存在 Response 结构体中。

我们通过查看http.Get()方法的源码可知,实际上是默认的DefaultClient调用自己的Get方法。

func Get(url string) (resp *Response, err error) {
	return DefaultClient.Get(url)
}

真正用来发送请求的Client结构体定义如下:

type Client struct {
	// Transport指定执行独立、单次HTTP请求的机制。
	// 如果Transport为nil,则使用DefaultTransport。
	Transport RoundTripper

	// CheckRedirect指定处理重定向的策略。
	// 如果CheckRedirect不为nil,客户端会在执行重定向之前调用本函数字段。
	// 参数req和via是将要执行的请求和已经执行的请求(切片,越新的请求越靠后)。
	// 如果CheckRedirect返回一个错误,本类型的Get方法不会发送请求req,
	// 而是返回之前得到的最后一个回复和该错误。(包装进url.Error类型里)
	//
	// 如果CheckRedirect为nil,会采用默认策略:连续10此请求后停止。
	CheckRedirect func(req *Request, via []*Request) error

	// Jar指定cookie管理器。
    // 如果Jar为nil,请求中不会发送cookie,响应的cookie会被忽略。
	Jar CookieJar

	// Timeout指定本类型的值执行请求的时间限制。
    // 该超时限制包括连接时间、重定向和读取回复主体的时间。
    // 计时器会在Head、Get、Post或Do方法返回后继续运作并在超时后中断回复主体的读取。
    //
    // Timeout为零值表示不设置超时。
    //
    // Client实例的Transport字段必须支持CancelRequest方法,
    // 否则Client会在试图用Head、Get、Post或Do方法执行请求时返回错误。
    // 本类型的Transport字段默认值(DefaultTransport)支持CancelRequest方法。
	Timeout time.Duration
}

基本的HTTP/HTTPS请求

Get、Head、Post和PostForm函数发出HTTP/HTTPS请求。

resp, err := http.Get("http://example.com/")
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
resp, err := http.PostForm("http://example.com/form", url.Values{"key": {"Value"}, "id": {"123"}})

程序在使用完response后必须关闭回复的主体。

resp, err := http.Get("http://example.com/")
if err != nil {
	// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)

GET请求示例

使用net/http包编写一个简单的发送HTTP请求的Client端,代码如下:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	resp, err := http.Get("http://127.0.0.1:8080")
	if err != nil {
		log.Fatalf("http.Get()函数执行错误,错误为:%v\n", err)
	}
	defer resp.Body.Close()

	//由于ioutil包的歧义性,1.16版本之后不再建议使用ioutil包,包内方法的具体实现都改为了调用的其它包方法
	//ioutil.ReadAll(resp.Body)
	body, err := io.ReadAll(resp.Body)

	if err != nil {
		log.Fatalf("io.RedAll()函数执行出错,错误为:%v\n", err)
	}
	fmt.Println(string(body))
}

服务端代码如下:

package main

import (
	"log"
	"net/http"
)

func index(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}
func main() {

	http.HandleFunc("/", index)
	err := http.ListenAndServe(":8080", nil)
	log.Fatal(err)
}

将上面的代码保存之后编译成可执行文件,执行之后就能在终端打印hello world网站首页的内容了,我们的浏览器其实就是一个发送和接收HTTP协议数据的客户端,我们平时通过浏览器访问网页其实就是从网站的服务器接收HTTP数据,然后浏览器会按照HTML、CSS等规则将网页渲染展示出来。

带参数的GET请求示例

关于GET请求的参数需要使用Go语言内置的net/url这个标准库来处理。

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
)

func main() {
	//1.处理请求参数
	params := url.Values{}
	params.Set("name", "itbsl")
	params.Set("hobby", "fishing")

	//2.设置请求URL
	rawUrl := "http://127.0.0.1:8080"
	reqUrl, err := url.ParseRequestURI(rawUrl)
	if err != nil {
		fmt.Printf("url.ParseRequestURI()函数执行错误,错误为:%v\n", err)
		return
	}

	//3.整合请求URL和参数
	//Encode方法将请求参数编码为url编码格式("bar=baz&foo=foz"),编码时会以键进行排序。
	reqUrl.RawQuery = params.Encode()

	//4.发送HTTP请求
	//说明:reqURL.String() String将URL重构为一个合法URL字符串。
	resp, err := http.Get(reqUrl.String())
	if err != nil {
		log.Fatalf("http.Get()函数执行错误,错误为:%v\n", err)
	}
	defer resp.Body.Close()
	
	//5.一次性读取响应的所有内容
	body, err := io.ReadAll(resp.Body)

	if err != nil {
		log.Fatalf("io.RedAll()函数执行出错,错误为:%v\n", err)
	}
	fmt.Println(string(body))
}

对应的Server端代码如下:

package main

import (
	"fmt"
	"log"
	"net/http"
)

func index(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	params := r.URL.Query()
	fmt.Printf("name: %v, hobby: %v\n", params.Get("name"), params.Get("nobody"))
	w.Write([]byte("hello world"))
}
func main() {

	http.HandleFunc("/", index)
	err := http.ListenAndServe(":8080", nil)
	log.Fatal(err)
}

POST请求示例

面演示了使用net/http包发送GET请求的示例,发送POST请求的示例代码如下:

package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	url := "http://127.0.0.1:8080"
	// 表单数据
	//contentType := "application/x-www-form-urlencoded"
	//data := "name=itbsl&age=18"
	// json
	contentType := "application/json"
	data := `{"name":"itbsl","age":18}`
	resp, err := http.Post(url, contentType, strings.NewReader(data))
	if err != nil {
		fmt.Printf("post failed, err:%v\n", err)
		return
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("get resp failed,err:%v\n", err)
		return
	}
	fmt.Println(string(body))
}

对应的Server端HandlerFunc如下:

func index(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	// 1. 请求类型是application/x-www-form-urlencoded时解析form数据
	//r.ParseForm()
	//fmt.Println(r.PostForm) // 打印form数据
	//fmt.Println(r.PostForm.Get("name"), r.PostForm.Get("age"))
	// 2. 请求类型是application/json时从r.Body读取数据
	body, err := io.ReadAll(r.Body)
	if err != nil {
		fmt.Printf("read request.Body failed, err:%v\n", err)
		return
	}
	fmt.Println(string(body))
	answer := `{"status": "ok"}`
	w.Write([]byte(answer))
}

自定义Client

要管理HTTP客户端的头域、重定向策略和其他设置,创建一个Client:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	client := &http.Client{}
	req, err := http.NewRequest("GET", "http://127.0.0.1:8080", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("sign", "sign_aes")
	resp, err := client.Do(req)
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(body))
}

参考:
Head First Go语言程序设计
Go语言标准库之http/template

posted @ 2020-01-10 13:14  itbsl  阅读(15949)  评论(0编辑  收藏  举报