GoWeb基础

GoWeb基础

1. Web 简介

1.1 Web应用的工作原理

image-20211117205519152

1.2 hello world入门

在 GOPATH 下的 src 目录下创建一个 webapp 的文件夹,并在该目录中创建一个 main.go 的文件,代码如下:

package main
import (
    "fmt"
    "net/http"
)
//创建处理器函数
func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Hello World!", r.URL.Path)
}
func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

在终端执行以下命令(使用 Vscode 开发工具时):

a) 方式一(建议使用):在 webapp 目录中右键→在命令提示符中打开 执行 go build main.go 命令;然后在当前目录中就会生成一个 main.exe 的 二进制可执行文件;最后再执行 ./main.exe 就可以启动服务器

b) 方式二:在 webapp 目录中右键→在命令提示符中打开 执行 go install webapp 命令;然后在 bin 目录中会生成一个 webapp.exe 的二进制可执行文件;进入 bin 目录之后再 bin 目录中执行 ./webapp.exe 就 可以启动服务器

3) 在浏览器地址栏输入 http://localhost:8080,在浏览器中就会显示 Hello World! / 在浏览器地址栏输入 http://localhost:8080/hello,在浏览器中就会显示 Hello World! /hello

2. Web服务器的创建

说明:

Go 提供了一系列用于创建 Web 服务器的标准库,而且通过 Go 创建一个服务器的 步骤非常简单,只要通过 net/http 包调用 ListenAndServe 函数并传入网络地址以及负责处理请求的处理器( handler )作为参数就可以了。如果网络地址参数为空字符串,那么服务器默认使用 80 端口进行网络连接;如果处理器参数为 nil,那么服务器将使用默认的多路复用器 DefaultServeMux,当然,我们也可以通过调用 NewServeMux 函数创 建一个多路复用器。多路复用器接收到用户的请求之后根据请求的 URL 来判断使用哪 个处理器来处理请求,找到后就会重定向到对应的处理器来处理请求,

2.1 使用默认的多路复用器(DefaultServeMux)

1)使用处理器函数处理请求

package main
import (
    "fmt"
    "net/http"
)
//创建处理器函数
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "正在通过处理器函数处理你的请求")
}
func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

type HandlerFunc

type HandlerFunc func(ResponseWriter, *Request)

HandlerFunc type是一个适配器,通过类型转换让我们可以将普通的函数作为HTTP处理器使用。如果f是一个具有适当签名的函数,HandlerFunc(f)通过调用f实现了Handler接口。

func HandleFunc

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

HandleFunc注册一个处理器函数handler和对应的模式pattern(注册到DefaultServeMux)。ServeMux的文档解释了模式的匹配机制。

处理器函数的实现原理: Go 语言拥有一种 HandlerFunc 函数类型,它可以将一个带有正确签 名的函数 f 转换成一个带有方法 f 的 Handler。

package main

import (
	"fmt"
	"net/http"
)

type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "尼玛正在通过处理器处理你的请求")
}

func main() {
	myHandler := MyHandler{}
	//调用处理器
	http.Handle("/", &myHandler)
	http.ListenAndServe(":8080", nil)
}

type Handler

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

实现了Handler接口的对象可以注册到HTTP服务端,为特定的路径及其子树提供服务。

ServeHTTP应该将回复的头域和数据写入ResponseWriter接口然后返回。返回标志着该请求已经结束,HTTP服务端可以转移向该连接上的下一个请求。

func Handle

func Handle(pattern string, handler Handler)

Handle注册HTTP处理器handler和对应的模式pattern(注册到DefaultServeMux)。如果该模式已经注册有一个处理器,Handle会panic。ServeMux的文档解释了模式的匹配机制。

注意:只要某个结构体实现了 Handler 接口中的 ServeHTTP 方法 那么它就是一个处理器

我们还可以通过 Server 结构对服务器进行更详细的配置:

type Server

type Server struct {
    Addr           string        // 监听的TCP地址,如果为空字符串会使用":http"
    Handler        Handler       // 调用的处理器,如为nil会调用http.DefaultServeMux
    ReadTimeout    time.Duration // 请求的读取操作在超时前的最大持续时间
    WriteTimeout   time.Duration // 回复的写入操作在超时前的最大持续时间
    MaxHeaderBytes int           // 请求的头域最大长度,如为0则用DefaultMaxHeaderBytes
    TLSConfig      *tls.Config   // 可选的TLS配置,用于ListenAndServeTLS方法
    // TLSNextProto(可选地)指定一个函数来在一个NPN型协议升级出现时接管TLS连接的所有权。
    // 映射的键为商谈的协议名;映射的值为函数,该函数的Handler参数应处理HTTP请求,
    // 并且初始化Handler.ServeHTTP的*Request参数的TLS和RemoteAddr字段(如果未设置)。
    // 连接在函数返回时会自动关闭。
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    // ConnState字段指定一个可选的回调函数,该函数会在一个与客户端的连接改变状态时被调用。
    // 参见ConnState类型和相关常数获取细节。
    ConnState func(net.Conn, ConnState)
    // ErrorLog指定一个可选的日志记录器,用于记录接收连接时的错误和处理器不正常的行为。
    // 如果本字段为nil,日志会通过log包的标准日志记录器写入os.Stderr。
    ErrorLog *log.Logger
    // 内含隐藏或非导出字段
}

Server类型定义了运行HTTP服务端的参数。Server的零值是合法的配置。

package main

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

type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "测试通过Server结构详细配置服务器")
}

func main() {
	myHandler := MyHandler{}
	// 通过Server结构对服务器进行更详细的配置
	server := http.Server{
		Addr:        ":8080",
		Handler:     &myHandler,
		ReadTimeout: 2 * time.Second,
	}

	server.ListenAndServe()
}

2.2 使用自己创建的多路复用器
  1. 在创建服务器时,我们还可以通过 NewServeMux 方法创建一个多路复用器:

    type ServeMux

    type ServeMux struct {
        // 内含隐藏或非导出字段
    }
    

    ServeMux类型是HTTP请求的多路转接器。它会将每一个接收的请求的URL与一个注册模式的列表进行匹配,并调用和URL最匹配的模式的处理器。

    模式是固定的、由根开始的路径,如"/favicon.ico",或由根开始的子树,如"/images/"(注意结尾的斜杠)。较长的模式优先于较短的模式,因此如果模式"/images/“和”/images/thumbnails/“都注册了处理器,后一个处理器会用于路径以”/images/thumbnails/“开始的请求,前一个处理器会接收到其余的路径在”/images/"子树下的请求。

    注意,因为以斜杠结尾的模式代表一个由根开始的子树,模式"/“会匹配所有的未被其他注册的模式匹配的路径,而不仅仅是路径”/"。

    模式也能(可选地)以主机名开始,表示只匹配该主机上的路径。指定主机的模式优先于一般的模式,因此一个注册了两个模式"/codesearch"和"codesearch.google.com/"的处理器不会接管目标为"http://www.google.com/"的请求。

    ServeMux还会注意到请求的URL路径的无害化,将任何路径中包含".“或”…"元素的请求重定向到等价的没有这两种元素的URL。(参见path.Clean函数)

    func NewServeMux

    func NewServeMux() *ServeMux
    

    NewServeMux创建并返回一个新的*ServeMux

    func (*ServeMux) Handle

    func (mux *ServeMux) Handle(pattern string, handler Handler)
    

    Handle注册HTTP处理器handler和对应的模式pattern。如果该模式已经注册有一个处理器,Handle会panic。

    Example

    func (*ServeMux) HandleFunc

    func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
    

    HandleFunc注册一个处理器函数handler和对应的模式pattern。

    func (*ServeMux) Handler

    func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
    

    Handler根据r.Method、r.Host和r.URL.Path等数据,返回将用于处理该请求的HTTP处理器。它总是返回一个非nil的处理器。如果路径不是它的规范格式,将返回内建的用于重定向到等价的规范路径的处理器。

    Handler也会返回匹配该请求的的已注册模式;在内建重定向处理器的情况下,pattern会在重定向后进行匹配。如果没有已注册模式可以应用于该请求,本方法将返回一个内建的"404 page not found"处理器和一个空字符串模式。

    func (*ServeMux) ServeHTTP

    func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
    

    ServeHTTP将请求派遣到与请求的URL最匹配的模式对应的处理器。

package main

import (
	"fmt"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "通过自己创建的多路复用器来处理请求")
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/myMux", handler)
	http.ListenAndServe(":8080", mux)
}

3. HTTP协议
3.1 简介

HTTP 超文本传输协议 (HTTP-Hypertext transfer protocol),是一个属于应用层的 面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于 1990 年提出,经过几年的使用与发展,得到不断地完善和扩展。它是一种详细规定了浏览器 和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。

客户端与服务端通信时传输的内容我们称之为报文

HTTP 就是一个通信规则,这个规则规定了客户端发送给服务器的报文格式,也规 定了服务器发送给客户端的报文格式。实际我们要学习的就是这两种报文。客户端发送 给服务器的称为”请求报文“,服务器发送给客户端的称为”响应报文

3.2 HTTP 协议的会话方式

image-20211117211514000

3.3 HTTP1.0 和 HTTP1.1 的区别

在 HTTP1.0 版本中,浏览器请求一个带有图片的网页,会由于下载图片而与服务器 之间开启一个新的连接;但在 HTTP1.1 版本中,允许浏览器在拿到当前请求对应的全部 资源后再断开连接,提高了效率。

image-20211117211605815

HTTP 1.1 是目前使用最为广泛的一个版本,而最新的一个版本则是 HTTP 2.0,又 称 HTTP/2。在开放互联网上 HTTP 2.0 将只用于 https://网址。HTTPS,即 SSL(Secure Socket Layer,安全套接字层)之上的 HTTP,实际上就是在 SSL/TLS 连接的上层进行 HTTP 通信。

备注:SSL 最初由 Netscape 公司开发,之后由 IETF(Internet Engineering Task Force, 互联网工程任务组)接手并将其改名为 TLS(Transport Layer Security,传输层安全协议)

3.4 报文
3.4.1 报文格式

image-20211117211802995

3.4.2 请求报文

1) 报文格式

请求首行(请求行);
请求头信息(请求头);
空行;
请求体;

2) Get 请求

GET /Hello/index.jsp HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; 
Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; 
Media Center PC 6.0; .NET4.0C; .NET4.0E)
Accept-Encoding: gzip, deflate
Host: localhost:8080
Connection: Keep-Alive
Cookie: JSESSIONID=C55836CDA892D9124C03CF8FE8311B15
  • Get 请求没有请求体,Post 请求才有请求体

image-20211117212448463

3) Post 请求

POST 请求要求将 form 标签的 method 的属性设置为 post

image-20211117212548593

POST /Hello/target.html HTTP/1.1
Accept: application/x-ms-application, image/jpeg, application/xaml+xml, 
image/gif, image/pjpeg, application/x-ms-xbap, */*
Referer: http://localhost:8080/Hello/
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; 
Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; 
Media Center PC 6.0; .NET4.0C; .NET4.0E)
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Host: localhost:8080
Content-Length: 14
Connection: Keep-Alive
Cache-Control: no-cache
Cookie: JSESSIONID=774DA38C1B78AE288610D77621590345
username=admin

image-20211117212638175

3.4.3 响应报文

1) 报文格式

响应首行(响应行);
响应头信息(响应头);
空行;
响应体;
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=UTF-8
Content-Length: 274
Date: Tue, 07 Apr 2015 10:08:26 GMT
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" >
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Hello</h1>
</body>
</html>

image-20211117212818531

3.5 响应状态码

1) 状态码用来告诉 HTTP 客户端,HTTP 服务器是否产生了预期的 Response。HTTP/1.1 协议中定义了 5 类状态码, 状态码由三位数字组成,第一个数字定义了响应的类别

  • 1XX 提示信息 - 表示请求已被成功接收,继续处理
  • 2XX 成功 - 表示请求已被成功接收,理解,接受
  • 3XX 重定向 - 要完成请求必须进行更进一步的处理
  • 4XX 客户端错误 - 请求有语法错误或请求无法实现
  • 5XX 服务器端错误 - 服务器未能实现合法的请求

2) 响应码对浏览器来说很重要,它告诉浏览器响应的结果,常见的状态码有:

  • 200:请求成功,浏览器会把响应体内容(通常是 html)显示在浏览器中;
  • 404:请求的资源没有找到,说明客户端错误的请求了不存在的资源;
  • 500:请求资源找到了,但服务器内部出现了错误;
  • 302:重定向,当响应码为 302 时,表示服务器要求浏览器重新再发一个请求, 服务器会发送一个响应头 Location,它指定了新请求的 URL 地址;
4. 操作数据库

Go 语言中的database/sql包定义了对数据库的一系列操作。database/sql/driver 包定义了应被数据库驱动实现的接口,这些接口会被 sql 包使用。但是 Go 语言没有提供任何官方的数据库驱动,所以我们需要导入第三方的数据库驱动。不过我们连接数据库之后对数据库操作的大部分代码都使用 sql 包。

4.1 获取数据库连接
  1. 创建一个 db.go 文件,导入 database/sql 包以及第三方驱动包
  2. 定义两个全局变量
  3. 创建 init 函数,在函数体中调用 sql 包的 Open 函数获取连接
package utils

import (
	"database/sql"

	_ "github.com/go-sql-driver/mysql"
)

var (
	Db  *sql.DB
	err error
)

func init() {
	Db, err = sql.Open("mysql", "root:123456@tcp(localhost:3306)/test")
	if err != nil {
		panic(err.Error())
	}
}

image-20211117213554475

4.2 增删改操作
  1. 在连接的 test 数据库中创建一个 users 表 (在mysql数据库中完成,先创建test数据库,再创建users表)
CREATE TABLE users(
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(100) UNIQUE NOT NULL,
PASSWORD VARCHAR(100) NOT NULL,
email VARCHAR(100)
)
  1. 向 users 表中插入一条记录
  • 创建 user.go 文件,文件中编写一下代码

image-20211117213909619

package model

import (
	"fmt"
	"go_web/chapter04/web01_db/utils"
)

//User 结构体
type User struct {
	ID       int
	Username string
	Password string
	Email    string
}

//AddUser 添加User的方法一
func (user *User) AddUser() error {
	//1.写sql语句
	sqlStr := "insert into users(username,password,email) values(?,?,?)"
	//2.预编译
	inStmt, err := utils.Db.Prepare(sqlStr)
	if err != nil {
		fmt.Println("预编译出现异常:", err)
		return err
	}
	//3.执行
	_, err2 := inStmt.Exec("admin3", "123456", "admin@atguigu.com")
	if err2 != nil {
		fmt.Println("执行出现异常:", err2)
		return err
	}
	return nil
}

//AddUser2 添加User的方法二
func (user *User) AddUser2() error {
	//1.写sql语句
	sqlStr := "insert into users(username,password,email) values(?,?,?)"
	//2.执行
	_, err := utils.Db.Exec(sqlStr, "admin2", "666666", "admin2@sina.com")
	if err != nil {
		fmt.Println("执行出现异常:", err)
		return err
	}
	return nil
}

//GetUserByID 根据用户的id从数据库中查询一条记录
func (user *User) GetUserByID() (*User, error) {
	//写sql语句
	sqlStr := "select id,username,password,email from users where id = ?"
	//执行
	row := utils.Db.QueryRow(sqlStr, user.ID)
	//声明
	var id int
	var username string
	var password string
	var email string
	err := row.Scan(&id, &username, &password, &email)
	if err != nil {
		return nil, err
	}
	u := &User{
		ID:       id,
		Username: username,
		Password: password,
		Email:    email,
	}
	return u, nil
}



//GetUsers 获取数据库中所有的记录
func (user *User) GetUsers() ([]*User, error) {
	//写sql语句
	sqlStr := "select id,username,password,email from users"
	//执行
	rows, err := utils.Db.Query(sqlStr)
	if err != nil {
		return nil, err
	}
	//创建User切片
	var users []*User
	for rows.Next() {
		//声明
		var id int
		var username string
		var password string
		var email string
		err := rows.Scan(&id, &username, &password, &email)
		if err != nil {
			return nil, err
		}
		u := &User{
			ID:       id,
			Username: username,
			Password: password,
			Email:    email,
		}
		users = append(users, u)
	}
	return users, nil
}

image-20211117214117156

image-20211117214139856

所以还可以通过调用 DB 的 Exec 方法添加用户

image-20211118110515974

image-20211117215045889

image-20211117215058288

4.3 单元测试
4.3.1 简介

Go 的单元测试需要用到 testing 包 以及 go test 命令,而且对测试文件也有以下要求 :

  1. 被测试的源文件和测试文件必须位于同一个包下
  2. 测试文件必须要以 _test.go 结尾
  • 虽然 Go 对测试文件_test.go 的前缀没有强制要求,不过一般我们都设置 为被测试的文件的文件名,如:要对 user.go 进行测试,那么测试文件的 名字我们通常设置为 user_test.go
  1. 测试文件中的测试函数为 TestXxx(t *testing.T)
  • 其中 Xxx 的首字母必须是大写的英文字母
  • 函数参数必须是 test.T 的指针类型(如果是 Benchmark 测试则参数是 testing.B 的指针类型)
4.3.2 测试代码
  1. 在 user_test.go 中测试添加员工的方法

  2. 如果一个测试函数的函数名的不是以 Test 开头,那么在使用 go test 命令时默 认不会执行,不过我们可以设置该函数时一个子测试函数,可以在其他测试函 数里通过 t.Run 方法来执行子测试函数

  3. 我们还可以通过 TestMain(m *testing.M)函数在测试之前和之后做一些其他的操作

    • 测试文件中有 TestMain 函数时,执行 go test 命令将直接运行 TestMain 函数,不直接运行测试函数,只有在 TestMain 函数中执行 m.Run()时才 会执行测试函数

    • 如果想查看测试的详细过程,可以使用 go test -v 命令

package model

import (
	"fmt"
	"testing"
)

//TestMain函数可以在测试函数执行之前做一些其他操作
func TestMain(m *testing.M) {
	fmt.Println("测试开始:")
	//通过m.Run()来执行测试函数
	m.Run()
}

func TestUser(t *testing.T) {
	fmt.Println("开始测试User中的相关方法")
	//通过t.Run()来执行子测试函数
	t.Run("测试添加用户:", testAddUser)
}

//如果函数名不是以Test开头,那么该函数默认不执行,我们可以将它设置成为一个子测试函数
func testAddUser(t *testing.T) {
	fmt.Println("子测试函数执行:")
	user := &User{
		Username: "admin6",
		Password: "123456",
		Email:    "admin3@atguigu.com",
	}
	//调用添加用户

	user.AddUser()
	//user.AddUser2()
}

func TestGetUserById(t *testing.T) {
	fmt.Println("测试GetUserById()方法")
	user := &User{
		ID: 7,
	}

	// t.Run("测试查询id:", TestGetUserById)
	u, _ := user.GetUserByID()
	fmt.Println("得到的User信息:", u)

}

//测试获取所有User
func TestGetUsers(t *testing.T) {
	fmt.Println("测试查询所有记录:")
	user := &User{}
	//调用获取所有User的方法
	us, _ := user.GetUsers()
	//遍历切片
	for k, v := range us {
		fmt.Printf("第%v个用户是:%v\n", k+1, v)
	}
	fmt.Println(us)

}

image-20211117214732645

5. 处理请求

Go 语言的 net/http 包提供了一系列用于表示 HTTP 报文的结构,我们可以使用它 处理请求和发送相应,其中 Request 结构代表了客户端发送的请求报文,下面让我们看 一下 Request 结构体

type Request

type Request struct {
    // Method指定HTTP方法(GET、POST、PUT等)。对客户端,""代表GET。
    Method string
    // URL在服务端表示被请求的URI,在客户端表示要访问的URL。
    //
    // 在服务端,URL字段是解析请求行的URI(保存在RequestURI字段)得到的,
    // 对大多数请求来说,除了Path和RawQuery之外的字段都是空字符串。
    // (参见RFC 2616, Section 5.1.2)
    //
    // 在客户端,URL的Host字段指定了要连接的服务器,
    // 而Request的Host字段(可选地)指定要发送的HTTP请求的Host头的值。
    URL *url.URL
    // 接收到的请求的协议版本。本包生产的Request总是使用HTTP/1.1
    Proto      string // "HTTP/1.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0
    // Header字段用来表示HTTP请求的头域。如果头域(多行键值对格式)为:
    //	accept-encoding: gzip, deflate
    //	Accept-Language: en-us
    //	Connection: keep-alive
    // 则:
    //	Header = map[string][]string{
    //		"Accept-Encoding": {"gzip, deflate"},
    //		"Accept-Language": {"en-us"},
    //		"Connection": {"keep-alive"},
    //	}
    // HTTP规定头域的键名(头名)是大小写敏感的,请求的解析器通过规范化头域的键名来实现这点。
    // 在客户端的请求,可能会被自动添加或重写Header中的特定的头,参见Request.Write方法。
    Header Header
    // Body是请求的主体。
    //
    // 在客户端,如果Body是nil表示该请求没有主体买入GET请求。
    // Client的Transport字段会负责调用Body的Close方法。
    //
    // 在服务端,Body字段总是非nil的;但在没有主体时,读取Body会立刻返回EOF。
    // Server会关闭请求的主体,ServeHTTP处理器不需要关闭Body字段。
    Body io.ReadCloser
    // ContentLength记录相关内容的长度。
    // 如果为-1,表示长度未知,如果>=0,表示可以从Body字段读取ContentLength字节数据。
    // 在客户端,如果Body非nil而该字段为0,表示不知道Body的长度。
    ContentLength int64
    // TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
    // 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
    TransferEncoding []string
    // Close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。
    Close bool
    // 在服务端,Host指定URL会在其上寻找资源的主机。
    // 根据RFC 2616,该值可以是Host头的值,或者URL自身提供的主机名。
    // Host的格式可以是"host:port"。
    //
    // 在客户端,请求的Host字段(可选地)用来重写请求的Host头。
    // 如过该字段为"",Request.Write方法会使用URL字段的Host。
    Host string
    // Form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    Form url.Values
    // PostForm是解析好的POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    PostForm url.Values
    // MultipartForm是解析好的多部件表单,包括上传的文件。
    // 本字段只有在调用ParseMultipartForm后才有效。
    // 在客户端,会忽略请求中的本字段而使用Body替代。
    MultipartForm *multipart.Form
    // Trailer指定了会在请求主体之后发送的额外的头域。
    //
    // 在服务端,Trailer字段必须初始化为只有trailer键,所有键都对应nil值。
    // (客户端会声明哪些trailer会发送)
    // 在处理器从Body读取时,不能使用本字段。
    // 在从Body的读取返回EOF后,Trailer字段会被更新完毕并包含非nil的值。
    // (如果客户端发送了这些键值对),此时才可以访问本字段。
    //
    // 在客户端,Trail必须初始化为一个包含将要发送的键值对的映射。(值可以是nil或其终值)
    // ContentLength字段必须是0或-1,以启用"chunked"传输编码发送请求。
    // 在开始发送请求后,Trailer可以在读取请求主体期间被修改,
    // 一旦请求主体返回EOF,调用者就不可再修改Trailer。
    //
    // 很少有HTTP客户端、服务端或代理支持HTTP trailer。
    Trailer Header
    // RemoteAddr允许HTTP服务器和其他软件记录该请求的来源地址,一般用于日志。
    // 本字段不是ReadRequest函数填写的,也没有定义格式。
    // 本包的HTTP服务器会在调用处理器之前设置RemoteAddr为"IP:port"格式的地址。
    // 客户端会忽略请求中的RemoteAddr字段。
    RemoteAddr string
    // RequestURI是被客户端发送到服务端的请求的请求行中未修改的请求URI
    // (参见RFC 2616, Section 5.1)
    // 一般应使用URI字段,在客户端设置请求的本字段会导致错误。
    RequestURI string
    // TLS字段允许HTTP服务器和其他软件记录接收到该请求的TLS连接的信息
    // 本字段不是ReadRequest函数填写的。
    // 对启用了TLS的连接,本包的HTTP服务器会在调用处理器之前设置TLS字段,否则将设TLS为nil。
    // 客户端会忽略请求中的TLS字段。
    TLS *tls.ConnectionState
}

Request类型代表一个服务端接受到的或者客户端发送出去的HTTP请求。

Request各字段的意义和用途在服务端和客户端是不同的。除了字段本身上方文档,还可参见Request.Write方法和RoundTripper接口的文档。

5.1 获取请求 URL

Request 结构中的 URL 字段用于表示请求行中包含的 URL,改字段是一个指向 url.URL 结构的指针,让我们来看一下 URL 结构

image-20211117215508122

5.2 获取请求头中的信息

通过 Request 结果中的 Header 字段用来获取请求头中的所有信息,Header 字段 的类型是 Header 类型,而 Header 类型是一个

 map[string][]string,string

类型的 key, string 切片类型的值。下面是 Header 类型及它的方法:

image-20211117215816700

  1. 获取请求头中的所有信息
  • r.Header
map[User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36] Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8] Accept-Encoding:[gzip, deflate, br] Accept-Language:[zhCN,zh;q=0.9,en-US;q=0.8,en;q=0.7] Connection:[keep-alive] Upgrade-InsecureRequests:[1]]
  1. 获取请求头中的某个具体属性的值,如获取 Accept-Encoding 的值
  • 方式一:r.Header[“Accept-Encoding”]

​ 得到的是一个字符串切片

[gzip, deflate, br]
  • 方式二:r.Header.Get(“Accept-Encoding”)

​ 得到的是字符串形式的值,多个值使用逗号分隔

gzip, deflate, br
5.3 获取请求体中的信息

请求和响应的主体都是有 Request 结构中的 Body 字段表示,这个字段的类型是 io.ReadCloser 接口,该接口包含了 Reader 接口和 Closer 接口,Reader 接口拥有 Read 方法,Closer 接口拥有 Close 方法

image-20211117220256387

  1. 由于 GET 请求没有请求体,所以我们需要在 HTML 页面中创建一个 form 表单,通 过指定 method=”post”来发送一个 POST 请求
<form action="http://localhost:8080/getBody" method="POST">
 		用户名: <input type="text" name="username" value="hanzong"><br/>
 		密 码 : <input type="password" name="password" value="666666"><br/>
 		<input type="submit">
 </form>
package main
import (
	"fmt"
	"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
	//获取内容的长度
	length := r.ContentLength
	//创建一个字节切片
	body := make([]byte, length)
	//读取请求体
	r.Body.Read(body)
	fmt.Fprintln(w, "请求体中的内容是:", string(body))
}

func main() {
http.HandleFunc("/getBody", handler)
http.ListenAndServe(":8080", nil)
}


在浏览器上的显示结果:

请求体中的内容是: username=hanzong&password=666666
5.4 获取请求参数

通过 net/http 库中的 Request 结构的字段以及方法获取请求 URL 后面 的请求参数以及 form 表单中提交的请求参数

5.4.1 Form字段
  1. 类型是 url.Values 类型,Form 是解析好的表单数据,包括 URL 字段的 query 参数和 POST 或 PUT 的表单数据。

image-20211118090454647

  1. Form 字段只有在调用 Request 的 ParseForm 方法后才有效。在客户端,会忽 略请求中的本字段而使用 Body 替代

image-20211118090552332

  1. 获取 5.3 中的表单中提交的请求参数(username 和 password)
func handler(w http.ResponseWriter, r *http.Request) {
    //解析表单
    r.ParseForm()
    //获取请求参数
    fmt.Fprintln(w, "请求参数为:", r.Form)
}

注意:在执行 r.Form 之前一定要调用 ParseForm 方法

运行结果:

请求参数为: map[password:[666666] username:[hanzong]]

如果对 form 表单做一些修改,在 action 属性的 URL 后面也添加相同的请求参 数,如下:

<form 
action="http://localhost:8080/getBody?username=admin&pwd=123456" method="POST">
 		用 户 名 : <input type="text" name="username" value="hanzong"><br/>
 		密码: <input type="password" name="password" value="666666"><br/>
 		<input type="submit">
 </form>

运行结果:

请求参数为:map[username:[hanzong admin] password:[666666] pwd:[123456]]
  • 我们发现:表单中的请求参数 username 和 URL 中的请求参数 username 都获取到了,而且表单中的请求参数的值排在 URL 请求参 数值的前面

  • 如果此时我们只想获取表单中的请求参数该怎么办呢?那就需要使 用 Request 结构中的 PostForm 字段

5.4.2 PostForm字段
  1. 类型也是 url.Values 类型,用来获取表单中的请求参数
  • 将 r.Form 改为 r.PostForm 之后的代码
func handler(w http.ResponseWriter, r *http.Request) {
    //解析表单
    r.ParseForm()
    //获取请求参数
    fmt.Fprintln(w, "请求参数为:", r.PostForm)
}

运行结果:

请求参数为: map[username:[hanzong] password:[666666]]
  1. 但是 PostForm 字段只支持 application/x-www-form-urlencoded 编码,如果 form 表单的 enctype 属性值为 multipart/form-data,那么使用 PostForm 字段 无法获取表单中的数据,此时需要使用 MultipartForm 字段
  • 说明:form 表单的 enctype 属性的默认值是 application/x-www-form-urlencoded 编 码 , 实 现 上 传 文 件 时 需 要 讲 该 属 性 的 值 设 置 为 multipart/form-data 编码格式
5.4.3 FormValue 方法和 PostFormValue 方法
  1. FormValue 方法

    可以通过 FormValue 方法快速地获取某一个请求参数,该方法调用之前 会自动调用 ParseMultipartForm 和 ParseForm 方法对表单进行解析

image-20211118091523045

func handler(w http.ResponseWriter, r *http.Request) {
    //获取请求参数
    fmt.Fprintln(w, "请求参数username的值为:",r.FormValue("username"))
}
请求参数 username 的值为: hanzong
  1. PostFormValue 方法

​ 可以通过 PostFormValue 方法快速地获取表单中的某一个请求参数,该 方法调用之前会自动调用 ParseMultipartForm 和 ParseForm 方法对表单 进行解析

func (*Request) PostFormValue

func (r *Request) PostFormValue(key string) string

PostFormValue返回key为键查询r.PostForm字段得到结果[]string切片的第一个值。如果必要,本函数会隐式调用ParseMultipartForm和ParseForm。

func handler(w http.ResponseWriter, r *http.Request) {
    //获取请求参数
    fmt.Fprintln(w, "请求参数 username 的值为:", r.PostFormValue("username"))
}

请求参数 username 的值为: hanzong
5.4.4 MultipartForm 字段

为了取得 multipart/form-data 编码的表单数据,我们需要用到 Request 结构的 ParseMultipartForm 方法和 MultipartForm 字段,我们通常上传文件时会将 form 表单的 enctype 属性值设置为 multipart/form-data

func (*Request) ParseMultipartForm

func (r *Request) ParseMultipartForm(maxMemory int64) error

ParseMultipartForm将请求的主体作为multipart/form-data解析。请求的整个主体都会被解析,得到的文件记录最多maxMemery字节保存在内存,其余部分保存在硬盘的temp文件里。如果必要,ParseMultipartForm会自行调用ParseForm。重复调用本方法是无意义的。

<form action="http://localhost:8080/upload" method="POST" enctype="multipart/form-data">
 		用 户 名 : <input type="text" name="username" value="hanzong"><br/>
 		文件:<input type="file" name="photo" /><br/>
 		<input type="submit">
 </form>
package main
import (
    "fmt"
    "net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
    //解析表单
    r.ParseMultipartForm(1024)
    //打印表单数据
    fmt.Fprintln(w, r.MultipartForm)
}
func main() {
    http.HandleFunc("/upload", handler)
    http.ListenAndServe(":8080", nil)
}

浏览器运行结果:

&{map[username:[hanzong]] map[photo:[0xc042126000]]}
  • 结果中有两个映射,第一个映射映射的是用户名;第二个映射的值是一个地址
  • MuiltipartForm 字段的类型为 *multipart.Form,multipart 包下 Form 结构的指 针类型

image-20211118092622208

func handler(w http.ResponseWriter, r *http.Request) {
    //解析表单
    r.ParseMultipartForm(1024)
    fileHeader := r.MultipartForm.File["photo"][0]
    file, err := fileHeader.Open()
	if err == nil {
		data, err := ioutil.ReadAll(file)
		if err == nil {
			fmt.Fprintln(w, string(data))
		}
	}
}

  1. FormFile 方法
  • net/http 提供的 FormFile 方法可以快速的获取被上传的文件,但是只能处理上 传一个文件的情况。

func (*Request) FormFile

func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)

FormFile返回以key为键查询r.MultipartForm字段得到结果中的第一个文件和它的信息。如果必要,本函数会隐式调用ParseMultipartForm和ParseForm。查询失败会返回ErrMissingFile错误。

func handler(w http.ResponseWriter, r *http.Request) {
	file, _, err := r.FormFile("photo")
	if err == nil {
		data, err := ioutil.ReadAll(file)
		if err == nil {
			fmt.Fprintln(w, string(data))
		}
	}
}

5.5 给客户端响应

下面我 们来说一下如何使用 http.ResponseWriter 来给用户响应

type ResponseWriter

type ResponseWriter interface {
    // Header返回一个Header类型值,该值会被WriteHeader方法发送。
    // 在调用WriteHeader或Write方法后再改变该对象是没有意义的。
    Header() Header
    // WriteHeader该方法发送HTTP回复的头域和状态码。
    // 如果没有被显式调用,第一次调用Write时会触发隐式调用WriteHeader(http.StatusOK)
    // WriterHeader的显式调用主要用于发送错误码。
    WriteHeader(int)
    // Write向连接中写入作为HTTP的一部分回复的数据。
    // 如果被调用时还未调用WriteHeader,本方法会先调用WriteHeader(http.StatusOK)
    // 如果Header中没有"Content-Type"键,
    // 本方法会使用包函数DetectContentType检查数据的前512字节,将返回值作为该键的值。
    Write([]byte) (int, error)
}

ResponseWriter接口被HTTP处理器用于构造HTTP回复。

  1. 给客户端响应一个字符串

image-20211118093218280

image-20211118093231292

  1. 给客户端响应一个 HTML 页面
func handler(w http.ResponseWriter, r *http.Request) {
	html := `<html>
	 <head>
 		<title>测试响应内容为网页</title>
 		<meta charset="utf-8"/>
 	</head>
 	<body>
 		我是以网页的形式响应过来的!
 	</body>
</html>`
	w.Write([]byte(html))
}

image-20211118093410441

  1. 给客户端响应 JSON 格式的数据

image-20211118093545616

  1. 让客户端重定向
func handler(w http.ResponseWriter, r *http.Request) {
    //以下操作必须要在 WriteHeader 之前进行
    w.Header().Set("Location", "https:www.baidu.com")
    w.WriteHeader(302)
}

image-20211118093650370

6. 模板引擎

Go 为我们提供了 text/template 库和 html/template 库这两个模板引擎,模板引擎通过将数据和模板组合在一起生成最终的 HTML,而处理器负责调用模板引擎并将引 擎生成的 HTMl 返回给客户端。

Go 的模板都是文本文档(其中 Web 应用的模板通常都是 HTML),它们都嵌入了 一些称为动作的指令。从模板引擎的角度来说,模板就是嵌入了动作的文本(这些文本 通常包含在模板文件里面),而模板引擎则通过分析并执行这些文本来生成出另外一些 文本。

6.1 hello world入门

使用 Go 的 Web 模板引擎需要以下两个步骤:

(1) 对文本格式的模板源进行语法分析,创建一个经过语法分析的模板结构,其中 模板源既可以是一个字符串,也可以是模板文件中包含的内容。

(2 )执行经过语法分析的模板,将 ResponseWriter 和模板所需的动态数据传递给模 板引擎,被调用的模板引擎会把经过语法分析的模板和传入的数据结合起来,生成出最 终的 HTML,并将这些 HTML 传递给 ResponseWriter。

下面就让我们写一个简单的 HelloWorld

  1. 创建模板文件 hello.htm

image-20211118093925898

6.2 解析模板

image-20211118094057157

image-20211118094121095

image-20211118094147769

6.3 执行模板

image-20211118094559407

6.4 动作

Go 模板的动作就是一些嵌入到模板里面的命令,这些命令在模板中需要放到两个 大括号里=={{ 动作 }},之前我们已经用过一个很重要的动作:点(.)==,它代表了传递给模板的数据。下面我们再介绍几个常用的动作,如果还想了解其他类型的动作,可以参考 text/template 库的文档。

6.4.1 条件动作

image-20211118094518670

<html>
    <head>
        <title>模板文件</title>
        <meta charset="utf-8"/>
    </head>
    <body>
        <!-- 嵌入动作 -->
        {{if .}}
        你已经成年了
        {{else}}
        你还未成年
        {{end}}
    </body>
</html>
package main

import (
	_ "fmt"
	"html/template"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	// 解析模板文件
	t := template.Must(template.ParseFiles("index.html"))
	// 声明一个变量
	age := 20
	// 执行模板
	t.Execute(w, age > 18)
}

func main() {
	http.HandleFunc("/action", handler)
	http.ListenAndServe(":8080", nil)
}

浏览器结果:

你还未成年
6.4.2 迭代动作

迭代动作可以对数组、切片、映射或者通道进行迭代。

  • 格式一
{{range . }}
	遍历到的元素是 {{ . }}
{{ end }}

  • 格式二
{{range . }}
	遍历到的元素是 {{ . }}
{{ else }}
	没有任何元素
{{ end }}

注意:range 后面的点代表被遍历的元素;要显示的内容里面的点代表遍历 到的元素

<html>
    <head>
        <title>模板文件</title>
        <meta charset="utf-8">
    </head>
    <body>
        <!-- 嵌入动作 -->
        {{range .}}
            <a href="#">{{.}}</a>
            <!-- {{.}} -->
        {{else}}
            没有遍历到任何内容
        {{end}}
    </body>
</html>
package main

import (
	_ "fmt"
	"html/template"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	// 解析模板文件
	t := template.Must(template.ParseFiles("range.html"))
	// 声明一个字符串切片
	stars := []string{"马蓉", "baibaihe", "lixiaolu"}
	// 执行模板
	t.Execute(w, stars)
}

func main() {
	http.HandleFunc("/range", handler)
	http.ListenAndServe(":8080", nil)
}

  • 如果迭代之后是一个个的结构体,获取结构体中的字段值使用 .字段名 方式获取
{{range . }}
	获取结构体的 Name 字段名 {{ .Name }}
{{ end }}
  • 迭代 Map 时可以设置变量,变量以$开头
{{ range $k , $v := . }}
	键是 {{ $k }} , 值是 {{ $v }}
{{ end }}
  • 迭代管道
{{ c1 | c2 | c3 }}

注意:c1、c2 和 c3 可以是参数或者函数。管道允许用户将一个参数的输出 传递给下一个参数,各个参数之间使用 | 分割。

6.4.3 设置动作

设置动作允许在指定的范围内对点(.)设置值。

  • 格式一
{{ with arg }}
	为传过来的数据设置的新值是{{ . }}
{{ end }}

  • 格式二
{{ with arg }}
	为传过来的数据设置的新值是{{ . }}
{{ else }}
	传过来的数据仍然是{{ . }}
{{ end }}
<html>
    <head>
        <meta charset="UTF-8"/>
    </head>
    <body>
      <!-- 嵌入动作 -->
        <div>得到的数据是:{{.}}</div>
        {{with "太子"}}
        <div>替换之后的数据是:{{.}}</div>
        {{end}}
        <hr/>
        {{with ""}}
        <div>看一下现在的数据是:{{.}}</div>
        {{else}}
        <div>数据没有被替换,还是:{{.}}</div>
        {{end}}
    </body>
</html>
func handler(w http.ResponseWriter, r *http.Request) {
	// 解析模板文件
	t := template.Must(template.ParseFiles("with.html"))
	// 执行模板
	t.Execute(w, "狸猫")
}

image-20211118095825897

6.4.4 包含动作

包含动作允许用户在一个模板里面包含另一个模板,从而构建出嵌套的模板。

  • 格式一:{{ template “name” }}

​ name 为被包含的模板的名字

  • 格式二:{{ template “name” arg }}

​ arg 是用户想要传递给被嵌套模板的数据

例子:

hello.html

<html>
    <head>
        <title>模板文件</title>
        <meta charset="utf-8"/>
    </head>
    <body>
        <!-- 嵌入动作 -->
        <div>从后台得到的数据是:{{.}}</div>
        <!-- 包含 hello2.html 模板 -->
        {{ template "hello2.html"}}
        <div>hello.html 文件内容结束</div>
        <hr/>
        <div>将 hello.html 模板文件中的数据传递给 hello2.html模板文件</div>
         {{ template "hello2.html" . }}
     </body>
</html>

hello2.html

<html>
    <head>
        <title>hello2 模板文件</title>
        <meta charset="utf-8"/>
    </head>
    <body>
        <!-- 嵌入动作 -->
        <div>hello2.html 模板文件中的数据是:{{.}}</div>
    </body>
</html>

处理器端代码

// 包含动作
func test01(w http.ResponseWriter, r *http.Request) {
	t := template.Must(template.ParseFiles("hello.html", "hello2.html"))
	t.Execute(w, "测试包含")

}

注意:在解析模板文件时,当前文件以及被包含的文件都要解析

image-20211118100343628

6.4.5 定义动作

当我们访问一些网站时,经常会看到好多网页中有相同的部分:比如导航栏、版权 信息、联系方式等。这些相同的布局我们可以通过定义动作在模板文件中定义模板来实 现。定义模板的格式是:以{{ define “layout” }}开头,以{{ end }}结尾。

在一个模板文件中定义一个模板

<!-- 定义模板 -->
{{ define "model"}}
<html>
 	<head>
 		<title>模板文件</title>
 		<meta charset="utf-8"/>
	</head>
 	<body>
 		{{ template "content"}}
 	</body>
</html>
{{ end }}

在一个模板文件中定义多个模板

<!-- 定义模板 -->
{{define "model"}}
<html>
    <head>
        <meta charset="UTF-8"/>
    </head>
    <body>
       {{template "contennt"}}
    </body>
</html>
{{end}}

{{define "contennt"}}
    <a href="https://www.baidu.com">百度</a>
{{end}}

处理器端代码

// 定义动作
func test02(w http.ResponseWriter, r *http.Request) {
	t := template.Must(template.ParseFiles("hello3.html"))
	t.ExecuteTemplate(w, "model", "")
}

注意:需要调用 ExecuteTemplate 方法并指定模板的名字

在不同的模板文件中定义同名的模板

<!-- 定义模板 -->
{{ define "model"}}
<html>
 	<head>
 		<title>模板文件</title>
 		<meta charset="utf-8"/>
 	</head>
 	<body>
 		{{ template "content"}}
 	</body>
</html>
{{ end }}

<html>
    <head>
        <title>content模板文件</title>
        <meta charset="utf-8">
    </head>
    <body>
        <!-- 定义content模板 -->
        {{define "content"}}
            <h1>content1.html的内容</h1>
        {{end}}
    </body>
</html>
<html>
 	<head>
 		<title>content 模板文件</title>
 		<meta charset="utf-8"/>
 	</head>
 	<body>
		 <!-- 定义 content 模板 -->
		{{ define “content” }}
			 <h1>我是 content2.html 模板文件中的内容</h1>
 		{{ end }}
 	</body>
</html>
func test03(w http.ResponseWriter, r *http.Request) {
	rand.Seed(time.Now().Unix())
	var t *template.Template
	if rand.Intn(5) > 2 {
		//解析模板文件
		t = template.Must(template.ParseFiles("hello4.html", "content1.html"))

	} else {
		t = template.Must(template.ParseFiles("content2.html"))

	}

	// 执行模板
	t.ExecuteTemplate(w, "model", "")
}
6.4.6 块动作

Go 1.6 引入了一个新的块动作,这个动作允许用户定义一个模板并立即使用。相当 于设置了一个默认的模板

  • 格式 :
{{ block arg }}
如果找不到模板我就要显示了
{{ end }}
{{define "model"}}
<html>
    <head>
        <title>模板文件</title>
        <meta charset="utf-8">
    </head>
    <body>
        {{block "content" .}}
            找不到模板就显示
        {{end}}
    </body>
</html>
{{end}}
func test03(w http.ResponseWriter, r *http.Request) {
	rand.Seed(time.Now().Unix())
	var t *template.Template
	if rand.Intn(5) > 2 {
		//解析模板文件
		t = template.Must(template.ParseFiles("hello4.html", "content1.html"))

	} else {
		t = template.Must(template.ParseFiles("hello4.html"))

	}

	// 执行模板
	t.ExecuteTemplate(w, "model", "")
}
7. 会话控制

HTTP 是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不 能区分中两次请求是否由一个客户端发出。

这样的设计严重阻碍的 Web 程序的设计。 如:在我们进行网购时,买了一条裤子,又买了一个手机。由于 http 协议是无状态的, 如果不通过其他手段,服务器是不能知道用户到底买了什么。而 Cookie 就是解决方案

7.1 Cookie
7.1.1 简介

Cookie 实际上就是服务器保存在浏览器上的一段信息。浏览器有了 Cookie 之后, 每次向服务器发送请求时都会同时将该信息发送给服务器,服务器收到请求后,就可以 根据该信息处理请求。

type Cookie

type Cookie struct {
    Name       string
    Value      string
    Path       string
    Domain     string
    Expires    time.Time
    RawExpires string
    // MaxAge=0表示未设置Max-Age属性
    // MaxAge<0表示立刻删除该cookie,等价于"Max-Age: 0"
    // MaxAge>0表示存在Max-Age属性,单位是秒
    MaxAge   int
    Secure   bool
    HttpOnly bool
    Raw      string
    Unparsed []string // 未解析的“属性-值”对的原始文本
}

Cookie代表一个出现在HTTP回复的头域中Set-Cookie头的值里或者HTTP请求的头域中Cookie头的值里的HTTP cookie。

  • func (*Cookie) String
func (c *Cookie) String() string

String返回该cookie的序列化结果。如果只设置了Name和Value字段,序列化结果可用于HTTP请求的Cookie头或者HTTP回复的Set-Cookie头;如果设置了其他字段,序列化结果只能用于HTTP回复的Set-Cookie头。

7.2 Cookie的运行原理
  1. 第一次向服务器发送请求时在服务器端创建 Cookie
  2. 将在服务器端创建的 Cookie 以响应头的方式发送给浏览器
  3. 以后再发送请求浏览器就会携带着该 Cookie
  4. 服务器得到 Cookie 之后根据 Cookie 的信息来区分不同的用户
7.1.3 创建 Cookie 并将它发送给浏览器
  • 在服务器创建 Cookie 并将它发送给浏览器
package main

import (
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	cookie1 := http.Cookie{
		Name:     "user1",
		Value:    "admin",
		HttpOnly: true,
	}

	cookie2 := http.Cookie{
		Name:     "user2",
		Value:    "superAdmin",
		HttpOnly: true,
	}

	// 将Cookie发送给浏览器,即添加第一个Cookie
	w.Header().Set("Set-Cookie", cookie1.String())
	// 再添加一个Cookie
	w.Header().Add("Set-Cookie", cookie2.String())
}

func main() {
	http.HandleFunc("/cookie01", handler)
	http.ListenAndServe(":8080", nil)
}

浏览器响应报文中的内容

HTTP/1.1 200 OK
Set-Cookie: user1=admin; HttpOnly
Set-Cookie: user2=superAdmin; HttpOnly
Date: Sun, 12 Aug 2018 07:24:49 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8
  • 以后每次发送请求浏览器都会携带着 Cookie
GET /cookie HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 
Safari/537.36
Accept: 
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/
apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: user1=admin; user2=superAdmin

  • 除了 Set 和 Add 方法之外,Go 还提供了一种更快捷的设置 Cookie 的方式, 就是通过 net/http 库中的 SetCookie 方法

  • func SetCookie

func SetCookie(w ResponseWriter, cookie *Cookie)

SetCookie在w的头域中添加Set-Cookie头,该HTTP头的值为cookie。

package main

import (
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	cookie1 := http.Cookie{
		Name:     "user1",
		Value:    "admin",
		HttpOnly: true,
	}

	cookie2 := http.Cookie{
		Name:     "user2",
		Value:    "superAdmin",
		HttpOnly: true,
	}

	// 将Cookie发送给浏览器,即添加第一个Cookie
	// w.Header().Set("Set-Cookie", cookie1.String())
	// // 再添加一个Cookie
	// w.Header().Add("Set-Cookie", cookie2.String())
	http.SetCookie(w, &cookie1)
	http.SetCookie(w, &cookie2)
}

func main() {
	http.HandleFunc("/cookie01", handler)
	http.ListenAndServe(":8080", nil)
}
7.1.4 读取Cookie

由于我们在发送请求时 Cookie 在请求头中,所以我们可以通过 Request 结构中的 Header 字段来获取 Cookie

func handler(w http.ResponseWriter, r *http.Request) {
    //获取请求头中的 Cookie
    cookies := r.Header["Cookie"]
    fmt.Fprintln(w, cookies)
}

浏览器结果

[user1=admin; user2=superAdmin]
7.1.5 设置 Cookie 的有效时间

Cookie默认是会话级别的,当关闭浏览器之后Cookie将失效,我们可以通过Cookie 结构的 MaxAge 字段设置 Cookie 的有效时间

func handler2(w http.ResponseWriter, r *http.Request) {
	cookie := http.Cookie{
		Name:     "user",
		Value:    "aaaadmin",
		HttpOnly: true,
		MaxAge:   60,
	}
	// 将cookie发送给浏览器
	w.Header().Set("Set-Cookie", cookie.String())
}

浏览器响应报文中的内容

HTTP/1.1 200 OK
Set-Cookie: user=persistAdmin; Max-Age=60; HttpOnly
Date: Sun, 12 Aug 2018 07:32:57 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8
7.1.6 Cookie的用途
  1. 广告推荐
  2. 免登录
7.2 Session

使用 Cookie 有一个非常大的局限,就是如果 Cookie 很多,则无形的增加了客户 端与服务端的数据传输量。而且由于浏览器对 Cookie 数量的限制,注定我们不能再 Cookie 中保存过多的信息,于是 Session 出现。

Session 的作用就是在服务器端保存一些用户的数据,然后传递给用户一个特殊 的 Cookie,这个 Cookie 对应着这个服务器中的一个 Session,通过它就可以获取到保 存用户信息的 Session,进而就知道是那个用户再发送请求。

Session 的运行原理

  1. 第一次向服务器发送请求时创建 Session,给它设置一个全球唯一的 ID(可以通过 UUID 生成)
  2. 创建一个 Cookie,将 Cookie 的 Value 设置为 Session 的 ID 值,并将 Cookie 发送 给浏览器
  3. 以后再发送请求浏览器就会携带着该 Cookie
  4. 服务器获取 Cookie 并根据它的 Value 值找到服务器中对应的 Session,也就知道了 请求是那个用户发的
8. 处理静态文件

对于 HTML 页面中的 css 以及 js 等静态文件,需要使用使用 net/http 包下的以下 方法来处理

func StripPrefix

func StripPrefix(prefix string, h Handler) Handler

StripPrefix返回一个处理器,该处理器会将请求的URL.Path字段中给定前缀prefix去除后再交由h处理。StripPrefix会向URL.Path字段中没有给定前缀的请求回复404 page not found。

func FileServer

func FileServer(root FileSystem) Handler

FileServer返回一个使用FileSystem接口root提供文件访问服务的HTTP处理器。要使用操作系统的FileSystem接口实现,可使用http.Dir:

http.Handle("/", http.FileServer(http.Dir("/tmp")))

type FileSystem

type FileSystem interface {
    Open(name string) (File, error)
}

FileSystem接口实现了对一系列命名文件的访问。文件路径的分隔符为’/’,不管主机操作系统的惯例如何。

type Dir

type Dir string

Dir使用限制到指定目录树的本地文件系统实现了http.FileSystem接口。空Dir被视为".",即代表当前目录。

func (Dir) Open

func (d Dir) Open(name string) (File, error)

例子 :

  • 项目的静态文件的目录结构如下:

image-20211118104035399

  • index.html 模板文件中引入的 css 样式的地址如下

image-20211118104110438

  • 对静态文件的处理
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("views/static"))))

/static/会匹配 以 /static/开发的路径,当浏览器请求 index.html 页面中的 style.css 文件时,static 前缀会被替换为 views/staic,然后去 views/static/css 目录中取查找 style.css 文件

func ListenAndServe

func ListenAndServe(addr string, handler Handler) error

ListenAndServe监听TCP地址addr,并且会使用handler参数调用Serve函数处理接收到的连接。handler参数一般会设为nil,此时会使用DefaultServeMux。

一个简单的服务端例子:

package main
import (
	"io"
	"net/http"
	"log"
)
// hello world, the web server
func HelloServer(w http.ResponseWriter, req *http.Request) {
	io.WriteString(w, "hello, world!\n")
}
func main() {
	http.HandleFunc("/hello", HelloServer)
	err := http.ListenAndServe(":12345", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

并将 Cookie 发送 给浏览器
3) 以后再发送请求浏览器就会携带着该 Cookie
4) 服务器获取 Cookie 并根据它的 Value 值找到服务器中对应的 Session,也就知道了 请求是那个用户发的

8. 处理静态文件

对于 HTML 页面中的 css 以及 js 等静态文件,需要使用使用 net/http 包下的以下 方法来处理

func StripPrefix

func StripPrefix(prefix string, h Handler) Handler

StripPrefix返回一个处理器,该处理器会将请求的URL.Path字段中给定前缀prefix去除后再交由h处理。StripPrefix会向URL.Path字段中没有给定前缀的请求回复404 page not found。

func FileServer

func FileServer(root FileSystem) Handler

FileServer返回一个使用FileSystem接口root提供文件访问服务的HTTP处理器。要使用操作系统的FileSystem接口实现,可使用http.Dir:

http.Handle("/", http.FileServer(http.Dir("/tmp")))

type FileSystem

type FileSystem interface {
    Open(name string) (File, error)
}

FileSystem接口实现了对一系列命名文件的访问。文件路径的分隔符为’/’,不管主机操作系统的惯例如何。

type Dir

type Dir string

Dir使用限制到指定目录树的本地文件系统实现了http.FileSystem接口。空Dir被视为".",即代表当前目录。

func (Dir) Open

func (d Dir) Open(name string) (File, error)

例子 :

  • 项目的静态文件的目录结构如下:

[外链图片转存中…(img-KBqG8hM4-1637204950137)]

  • index.html 模板文件中引入的 css 样式的地址如下

[外链图片转存中…(img-WNQ2lF2L-1637204950138)]

  • 对静态文件的处理
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("views/static"))))

/static/会匹配 以 /static/开发的路径,当浏览器请求 index.html 页面中的 style.css 文件时,static 前缀会被替换为 views/staic,然后去 views/static/css 目录中取查找 style.css 文件

func ListenAndServe

func ListenAndServe(addr string, handler Handler) error

ListenAndServe监听TCP地址addr,并且会使用handler参数调用Serve函数处理接收到的连接。handler参数一般会设为nil,此时会使用DefaultServeMux。

一个简单的服务端例子:

package main
import (
	"io"
	"net/http"
	"log"
)
// hello world, the web server
func HelloServer(w http.ResponseWriter, req *http.Request) {
	io.WriteString(w, "hello, world!\n")
}
func main() {
	http.HandleFunc("/hello", HelloServer)
	err := http.ListenAndServe(":12345", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值