透视 Go Web:理解 net/http 背后的模型

Source

TCP/IP协议

目前大规模使用的是TCP/IP协议

应用层:
合并osi中的5,6,7层 (会话层,表示层,应用层)
常用协议:HTTP,FTP,SMTP,POP3,RPC
传输层:
OSI中第四层
常用协议TCP,UDP
网络层:
OSI中第3层
常用协议:IP,IPV4,IPV6
网络接口层
OSI中第1,2层

UDP:

User Datagram Protocol 用户 数据报协议
是一种无连接的协议
基于UDP协议主机把数据包发送给网络后就不管了,是一种不可靠协议
TCP和UDP的主要区别:
TCP是安全可靠的,UDP是不安全的,不可靠的。
UDP的速度高于TCP

go语言对socket的支持:

socket分类:

按照连接时间:

  • 短连接
  • 长连接

按照客户端和服务器数量:

  • 点对点
  • 点对多
  • 多对多

TCPAddr结构体表示服务器IP和端口:

// TCPAddr represents the address of a TCP end point.
type TCPAddr struct {
    
      
	IP   IP
	Port int
	Zone string // IPv6 scoped addressing zone
}

TCPConn结构体表示连接,封装了数据读写操作:

// TCPConn is an implementation of the [Conn] interface for TCP network
// connections.
type TCPConn struct {
    
      
	conn
}

TCPListener负责监听服务器特定端口:

// TCPListener is a TCP network listener. Clients should typically
// use variables of type [Listener] instead of assuming TCP.
type TCPListener struct {
    
      
	fd *netFD
	lc ListenConfig
}

接口,切片,map,channel都是引用类型,不用传指针

HTTP请求包

我们先来看看 Request 包的结构,Request 包分为 3 部分,第一部分叫 Request line(请求行), 第二部分叫 Request header(请求头), 第三部分是 body(主体)。header 和 body 之间有个空行,请求包的例子所示:

GET /domains/example/ HTTP/1.1      // 请求行: 请求方法 请求 URI HTTP 协议/协议版本
Host:www.iana.org               // 服务端的主机名
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4          // 浏览器信息
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  // 客户端能接收的 mine
Accept-Encoding:gzip,deflate,sdch       // 是否支持流压缩
Accept-Charset:UTF-8,*;q=0.5        // 客户端字符编码集
// 空行,用于分割请求头和消息体
// 消息体,请求资源参数,例如 POST 传递的参数

HTTP响应包

HTTP/1.1 200 OK                     // 状态行
Server: nginx/1.0.8                 // 服务器使用的 WEB 软件名及版本
Date: Tue, 30 Oct 2012 04:14:25 GMT     // 发送时间
Content-Type: text/html             // 服务器发送信息的类型
Transfer-Encoding: chunked          // 表示发送 HTTP 包是分段发的
Connection: keep-alive              // 保持连接状态
Content-Length: 90                  // 主体内容长度
// 空行 用来分割消息头和主体
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"... // 消息体

软件模型分类:

  • B/S结构,客户端浏览器/服务器,客户端是运行在浏览器中
  • C/S结构,客户端/服务器,客户端是独立软件

控制器:

单控制器:

package main

import (
	"fmt"
	"net/http"
)

//单控制器

type MyHandler struct {
    
      
}

func (mh *MyHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
    
      
	fmt.Println(res, "输入内容。")
	res.Write([]byte("我叫xxx"))
}

func main() {
    
      
	myhandler := MyHandler{
    
      }
	server := http.Server{
    
      
		Addr:    "localhost:8090",
		Handler: &myhandler,
	}
	server.ListenAndServe()

}

多控制器:

在实际开发中大部分情况是不应该只有一个控制器的,不同的请求应该交给不同的处理单元,在Golang中支持两中处理方式

  • 多个处理器(Handler)
  • 多个处理函数(HandleFunc)(推荐)

使用多处理器

  • 使用http.handler把不同的URL绑定到不同的处理器
  • 在浏览器输入不同路径,对应不同处理器方法,但是其他URL会出现404资源未找到页面
多处理器:
package main

import (
	"fmt"
	"net/http"
)

//多控制器  多处理器

type MyHandler struct {
    
      
}

func (mh *MyHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
    
      
	fmt.Println(res, "输入内容。")
	res.Write([]byte("我叫魏正想"))
}

func main() {
    
      
	myhandler := MyHandler{
    
      }
	myhandler2 := MyHandler{
    
      }

	server := http.Server{
    
      
		Addr: "localhost:8090",
	}
	http.Handle("/fir", &myhandler)
	http.Handle("/sec", &myhandler2)

	err := server.ListenAndServe()
	if err != nil {
    
      
		return
	}

}
多处理函数:
package main

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

//多控制器  多处理器

func first(res http.ResponseWriter, req *http.Request) {
    
      
	fmt.Println(res, "输入内容。")
	res.Write([]byte("first"))
}
func second(res http.ResponseWriter, req *http.Request) {
    
      
	fmt.Println(res, "输入内容。")
	res.Write([]byte("second"))
}
func main() {
    
      
	// 1. 创建独立的路由表
	mux := http.NewServeMux()
	mux.HandleFunc("/fir", first)
	mux.HandleFunc("/sec", second)

	// 2. 创建服务器,并显式绑定这个独立路由表
	server := &http.Server{
    
      
		Addr:         "localhost:8090",
		Handler:      mux,             // 显式指定!这样就不会受全局路由干扰
		ReadTimeout:  5 * time.Second, // 可以配置超时,这是用 struct 的最大优势
		WriteTimeout: 10 * time.Second,
	}

	// 3. 启动
	server.ListenAndServe()
	//http.ListenAndServe("123",mux)
}

向前端html模板传递数据

  • 可以在HTML中使用{ {}}获取template.Execte()第二个参数传递
  • 最常用的{ {.}}中的“.”是指针,指向当前变量,成为“dot”
  • 在{ {}}可以有的Argument,官方给定如下
  • 如果是字符串直接**{ {.}}**
  • 如果结构体类型数据就是**{ {.field}}**
  • 传递map类型数据**{ {.key}}** 深度不限,如果value是一个对象还可以 { {.field1.key1.field2.key2}}
type User struct {
    
      
	Name string
	Age  int
	Tim  time.Time
}

// 模板中调用字符串
func fir(w http.ResponseWriter, r *http.Request) {
    
      
	t, _ := template.ParseFiles("./view/index.html")
	t.Execute(w, "wzx")
}

// 模板中调用结构体
//
//	func sec(w http.ResponseWriter, r *http.Request) {
    
      
//		t, _ := template.ParseFiles("./view/index.html")
//		t.Execute(w, User{Name: "张三", Age: 18})
//	}
//
// 模板中调用结构体
func sec(w http.ResponseWriter, r *http.Request) {
    
      
	t, err := template.ParseFiles("./view/index.html")
	if err != nil {
    
      
		http.Error(w, "模板解析失败: "+err.Error(), http.StatusInternalServerError)
		log.Printf("sec 函数模板解析错误: %v", err)
		return
	}
	if err := t.Execute(w, User{
    
      Name: "张三", Age: 18}); err != nil {
    
      
		http.Error(w, "模板执行失败: "+err.Error(), http.StatusInternalServerError)
		log.Printf("sec 函数模板执行错误: %v", err)
	}
}

// 在模板中调用函数
func thr(w http.ResponseWriter, r *http.Request) {
    
      
	t, _ := template.ParseFiles("./view/index.html")
	time1 := time.Date(2026, 2, 26, 8, 47, 34, 0, time.Local)
	//time1.Year()
	//time1.Format()
	t.Execute(w, User{
    
      
		Name: "张三",
		Age:  18,
		Tim:  time1,
	})
}

func MyTransfer(t time.Time) string {
    
      
	return t.Format("2006-01-02 15:04:05")

}
func fou(w http.ResponseWriter, r *http.Request) {
    
      
	fm := template.FuncMap{
    
      "mt": MyTransfer}
	t := template.New("index.html").Funcs(fm)
	t, _ = t.ParseFiles("./view/index.html")
	time1 := time.Date(2026, 2, 26, 8, 47, 34, 0, time.Local)
	t.Execute(w, time1)
}

func main() {
    
      
	mux := http.NewServeMux()
	mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
	mux.HandleFunc("/fir", fir)
	mux.HandleFunc("/sec", sec)
	mux.HandleFunc("/thr", thr)
	mux.HandleFunc("/fou", fou)
	server := http.Server{
    
      
		Addr:    ":8090",
		Handler: mux,
	}
	server.ListenAndServe()
}

处理表单输入

login.gtpl

<html>
<head>
<title></title>
</head>
<body>
<form action="/login" method="post">
    用户名:<input type="text" name="username">
    密码:<input type="password" name="password">
    <input type="submit" value="登录">
</form>
</body>
</html>
func login(w http.ResponseWriter, r *http.Request) {
    
      
	fmt.Println("method:", r.Method) // 获取请求的方法
	if r.Method == "GET" {
    
      
		t, _ := template.ParseFiles("./view/login.gtpl")
		log.Println(t.Execute(w, nil))
	} else {
    
      
		err := r.ParseForm() // 解析 url 传递的参数,对于 POST 则解析响应包的主体(request body)
		if err != nil {
    
      
			// handle error http.Error() for example
			log.Fatal("ParseForm: ", err)
		}
		// 请求的是登录数据,那么执行登录的逻辑判断
		fmt.Println("username:", r.Form["username"])
		fmt.Println("password:", r.Form["password"])
	}
}

func main() {
    
      
	http.HandleFunc("/", sayhelloName)       // 设置访问的路由
	http.HandleFunc("/login", login)         // 设置访问的路由
	err := http.ListenAndServe(":9090", nil) // 设置监听的端口
	if err != nil {
    
      
		log.Fatal("ListenAndServe: ", err)
	}
}

防止表单多次递交

解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该唯一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了 Ajax 模式递交表单的话,当表单递交后,通过 javascript 来禁用表单的递交按钮。

预防跨站脚本

Go 里面是怎么做这个有效防护的呢?Go 的 html/template 里面带有下面几个函数可以帮你转义

func HTMLEscape (w io.Writer, b [] byte) // 把 b 进行转义之后写到 w
func HTMLEscapeString (s string) string // 转义 s 之后返回结果字符串
func HTMLEscaper (args …interface {
    
      }) string // 支持多个参数一起转义,返回结果字符串

文件上传

文件上传:客户端把上传文件转换为二进制后发送给服务器,服务器对二进制流进行解析

HTML表单(form)enctype(Encode Type)属性控制表单在提交数据到服务器时对数据的编码类型

   <form action="upload" method="post" enctype="multipart/form-data">   //编码成消息,每个控件对应消息的一部分,请求方式必须是post
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        头像:<input type="file" name="avatar"><br>
        <input type="submit" value="提交">
    </form>
    <form action="upload" method="post" enctype="text/plain">  //纯文本形式编码的
    </form>
    <form action="upload" method="post" enctype="application/x-www-form-urlencoded"> //默认值,表单数据会被编码为名称/值形式
    </form>

文件方面两个关键的结构体

File

  • File是一个接口,实现了对一个multipart信息中文件记录的访问。它的内容可以保持在内存或者硬盘中,如果保持在硬盘中,底层类型就会是*os.File。
type File interface {
    
      
    io.Reader
    io.ReaderAt
    io.Seeker
    io.Closer
}

FlieHeader

  • FileHeader描述一个multipart请求的(一个)文件记录的信息。
type FileHeader struct {
    
      
    Filename string
    Header   textproto.MIMEHeader
    // 内含隐藏或非导出字段
}

表单类型 & 对应解析方式

1. 普通键值对表单

(application/x-www-form-urlencoded)

  • 适用场景:最常见的表单类型(HTML 表单默认类型),仅包含文本类键值对(无文件),比如登录表单、搜索表单。
  • 请求特征:请求体是key1=value1&key2=value2格式的字符串,数据会被 URL 编码(比如空格转+,特殊字符转%xx)。
  • Go 解析方式:
    • 自动解析:r.FormValue("key") / r.PostFormValue("key")自动触发 r.ParseForm(),无需手动调用;
    • 手动解析(如需批量处理):err := r.ParseForm(),解析后可通过r.Form(GET+POST)/ r.PostForm(仅 POST)获取所有键值对。
2. 文件上传表单

(multipart/form-data)

  • 适用场景:包含文件的表单(如图片 / 文档上传),也可同时包含普通文本字段。

  • 请求特征:请求体按 “边界符” 分割,分为多个部分(每个部分对应一个字段 / 文件),支持二进制数据(文件流)。

  • Go 解析方式:

    • 必须手动调用 r.ParseMultipartForm(maxSize)maxSize为请求体最大字节数,如10<<20=10MB);
    • 普通字段:r.PostFormValue("key")(解析后才能取到);
    • 文件字段:file, fileHeader, err := r.FormFile("fileKey")(获取文件流和文件信息)。
  • 关键注意点:

    • 不能用r.ParseForm()解析,否则拿不到文件和普通字段;
    • 解析后r.MultipartForm可获取所有字段(包括文件),但日常用r.PostFormValue/r.FormFile更便捷;
    • 用完文件流后需defer file.Close(),避免句柄泄漏。
3. JSON 格式表单

(application/json)

  • 适用场景:前后端分离项目(如 Vue/React+Go),前端以 JSON 格式提交数据(无文件)。
  • 请求特征:请求体是纯 JSON 字符串(如{"username":"test","age":18}),Content-Type 为application/json
  • Go 解析方式:
    • 无内置的 “自动解析” 方法,需手动读取请求体,再反序列化为结构体 / Map;
    • 步骤:① 读取请求体字节流 ② json.Unmarshal 解析到目标变量。
func handleJSON(w http.ResponseWriter, r *http.Request) {
    
      
    // 读取请求体
    body, err := io.ReadAll(r.Body)
    if err != nil {
    
      
        http.Error(w, "读取请求体失败", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()
    
    // 反序列化JSON到结构体
    var user User
    err = json.Unmarshal(body, &user)
    if err != nil {
    
      
        http.Error(w, "JSON解析失败", http.StatusBadRequest)
        return
    }
    fmt.Fprintf(w, "用户名:%s,年龄:%d", user.Username, user.Age)
}
表单类型 核心解析方法 取值方式 核心区别
application/x-www-form-urlencoded r.ParseForm ()(可自动触发) r.FormValue()/r.PostFormValue() 仅文本,URL 编码,自动解析
multipart/form-data r.ParseMultipartForm(maxSize) r.PostFormValue()/r.FormFile() 支持文件,需手动解析,二进制
application/json 手动读 body + json.Unmarshal 解析到结构体 / Map 无键值对结构,纯 JSON 字符串

总结

  1. 普通文本表单:用r.FormValue()/r.PostFormValue()即可,无需手动调用r.ParseForm()(方法内部会自动触发);
  2. 文件上传表单:必须先手动调用r.ParseMultipartForm(maxSize),再用r.PostFormValue()取文本、r.FormFile()取文件;
  3. JSON 表单:没有内置解析方法,需手动读取请求体后用json.Unmarshal反序列化,是前后端分离的主流方式。

核心原则:表单类型和解析方法必须匹配,比如文件上传用r.ParseForm()会导致字段解析失败,JSON 表单用r.FormValue()也拿不到数据。

4.文件上传代码:

index3.html

 <form action="upload" method="post" enctype="multipart/form-data">
        文件名:<input type="text" name="name"><br>
        文件:<input type="file" name="file"><br>
        <input type="submit" value="上传">
    </form>

main.go

func file(w http.ResponseWriter, r *http.Request) {
    
      
	files, _ := template.ParseFiles("./view/index3.html")
	files.Execute(w, nil)
}

func upload(w http.ResponseWriter, r *http.Request) {
    
      
	//fmt.Println(r.Header)
	//const maxSize = 10 << 20 // 10MB
	//r.ParseMultipartForm(maxSize)
	//获取普通表单数据
	fileName := r.FormValue("name")
	fmt.Println(fileName)
	//获取文件流,第三个返回值是错误对象
	file, fileHeader, _ := r.FormFile("file")
	//取出文件扩展名
	//split := strings.Split(fileHeader.Filename, ".")
	extensionName := fileHeader.Filename[strings.LastIndex(fileHeader.Filename, "."):]
	//读取文件流[]byte
	b, _ := ioutil.ReadAll(file)
	//把文件保存到指定位置
	ioutil.WriteFile("d:/"+fileName+extensionName, b, 0777)
	//输出上传时文件名
	fmt.Println("上传文件名", fileHeader.Filename)
}

func main() {
    
      
	mux := http.NewServeMux()
	mux.HandleFunc("/file", file)
	mux.HandleFunc("/upload", upload)
	server := http.Server{
    
      
		Addr:    ":8090",
		Handler: mux,
	}
	server.ListenAndServe()
}

文件下载

文件下载总体步骤:

客户端向服务端发起请求,请求参数包含要下载文件的名称

服务器接收到客户端请求后把文件设置到响应对象中,响应给客户端浏览器

下载时需要设置的响应头信息

  • content-Type:内容MIME类型

    1. application/octet-stream任意类型
  • Content-Disposition:客户端对内容的操作方式

    1. inline默认值,表示浏览器能解析就解析,不能解析以下载形式展示给客户端
    2. attachment;filename=下载时显示的文件名,客户端浏览器恒下载

    index3.html

        <a href="download?filename=abc.png">下载</a>
    
        <form action="upload" method="post" enctype="multipart/form-data">
            文件名:<input type="text" name="name"><br>
            文件:<input type="file" name="file"><br>
            <input type="submit" value="上传">
        </form>
    

    main.go

    func file(w http.ResponseWriter, r *http.Request) {
          
            
    	files, _ := template.ParseFiles("./view/index3.html")
    	files.Execute(w, nil)
    }
    func download(w http.ResponseWriter, r *http.Request) {
          
            
    	//获取请求参数
    	filename := r.FormValue("filename")
    	//设置响应头
    	header := w.Header()
    	header.Add("Content-Type", "application/octet-stream")
    	header.Add("Content-Disposition", "attachment;filename="+filename)
    	//使用ioutil包读取文件
    	readFile, err := ioutil.ReadFile("d:/" + filename)
    	if err != nil {
          
            
    		fmt.Fprintln(w, "文件下载失败", err)
    		return
    	}
    	//写入到响应中
    	w.Write(readFile)
    }
    

http请求参数分类

很多人会把URL 查询参数?key=value)和路径参数、请求头参数弄混,这里顺带区分:

  • 查询参数:r.URL.Query().Get("key")r.FormValue("key")(比如/user?id=123里的123);
  • 路径参数:/user/123里的123(无key,靠位置识别);
  • 请求头参数:Authorization: Bearer token里的token(在 Header 里)。
方法 是否能获取查询参数 是否会被 POST 参数干扰 适用场景
r.URL.Query().Get("key") ✅ 是 ❌ 不会 只想获取 URL 中的查询参数
r.FormValue("key") ✅ 是 ✅ 可能(优先级低) 兼容 GET/POST 参数的场景
r.PostFormValue("key") ❌ 否 - 只获取 POST 请求体中的参数

json简介

轻量级数据传输格式

总体上分为两种

  • 一种是JSONObject(json对象)
  • 一种是JSONArray(json数组),包含多个JSONObject

属性tag(键key)的配置

type User struct {
    
      
	Name string `json:"Name"`  //字段 在json里的键为“Name” 
	Age  int `json:"-"`    //字段被本包忽略
	Tim  time.Time `json:",omitempty"`  //字段在json里的键默认为原来的属性“Tim”(通称Field),但如果字段为空值,就会跳过,注意前面的逗号
	Sex int `json:"Sex,omitempty"`  //字段在json中的键为“Sex”,且如果字段为空值将在对象中省略掉
}

处理json

func showUser(w http.ResponseWriter, r *http.Request) {
    
      
	if r.Method != http.MethodGet {
    
      
		//返回405错误,方法不允许
		http.Error(w, "只支持GET请求", http.StatusMethodNotAllowed)
	}
	var users = make([]User, 0)
	users = append(users, User{
    
      Name: "wzx", Age: 12, Tim: time.Now(), Sex: "0"})
	users = append(users, User{
    
      Name: "wzx1", Age: 18, Tim: time.Now(), Sex: "1"})
	users = append(users, User{
    
      Name: "wzx2", Age: 16, Tim: time.Now(), Sex: "0"})
	header := w.Header()
	header.Add("Content-Type", "application/json;charset=utf-8")
	marshal, _ := json.Marshal(users)
	//用js的index3_2函数
	//fmt.Fprintln(w, string(marshal))
	w.WriteHeader(200)
	//用js的index3函数
	w.Write(marshal)
}

Cookie

cookie简介

  • cookie就是客户端存储技术,以键值对的形式存在
  • 在B/S架构中,服务端产生Cookie响应给客户端,浏览器接收后把Cookie存在在特定的文件夹中,以后每次请求浏览器会把Cookie内容放入请求中

Go语言对Cookie的支持

在net/http包下提供了Cookie结构体

net/http包下的Cookie结构体

type Cookie struct {
    
      
    Name       string  //设置cookie的名称
    Value      string  //表示cookie的值
    Path       string  //有效范围
    Domain     string  //可访问Cookie的域
    Expires    time.Time  //Expires过期时间
    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。

注意:

响应头:可传 Set-Cookie,也可以不传

请求头:只有存过才自动带 Cookie

<body>
    <a href="setCookie">产生Cookie</a>
    <a href="getCookie">获取Cookie</a>
<br/>
    {
   
     {range $key, $value := .}}
    键:{
   
     {$key}},值:{
   
     {$value}}
    {
   
     {end}}
</body>
func Cookie(w http.ResponseWriter, r *http.Request) {
    
      
	files, _ := template.ParseFiles("./view/index4.html")
	files.Execute(w, nil)
}

// setCookie
func setCookie(w http.ResponseWriter, r *http.Request) {
    
      
	cookie := http.Cookie{
    
      Name: "myKey", Value: "myValue"}
	http.SetCookie(w, &cookie)
	files, _ := template.ParseFiles("./view/index4.html")
	files.Execute(w, nil)
}

// getCookie
func getCookie(w http.ResponseWriter, r *http.Request) {
    
      
	//根据key来取cookie
	//cookie, _ := r.Cookie("mykey")
	//取出全部Cookie内容
	cookies := r.Cookies()
	m := make(map[string]string)
	for _, n := range cookies {
    
      
		m[n.Name] = n.Value
	}
	files, _ := template.ParseFiles("./view/index4.html")
	files.Execute(w, m)
}

cookie常用设置

前文中讲的cookie结构体

type Cookie struct {
    
      
    Name       string  //设置cookie的名称
    Value      string  //表示cookie的值
    Path       string  //有效范围
    Domain     string  //可访问Cookie的域   例如(www.baidu.com)只能这个url可以访问
    Expires    time.Time  //Expires过期时间
    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 // 未解析的“属性-值”对的原始文本
}

HttpOnly

  • 控制Cookie的内容是否可以被JavaScript访问到。通过设置HttpOnly为true时防止XSS攻击防御手段之一
  • 默认HttpOnly为false,表示客户端可以通过js获取
  • 在项目中导入jquery.cookie.js库,使用jquery获取客户端Cookie内容

Expires

  • Cookie默认存活时间是浏览器不关闭,当浏览器关闭后,Cookie失效
  • 可以通过Expires设置具体什么时候过期,Cookie失效,也可以通过MaxAge设置Cookie多长时间后实现
  • IE6,7,8和很多浏览器不支持MaxAge,建议使用Expires(谷歌浏览器不影响)
  • Expires是time.Time类型,所以设置时需要明确设置过期时间

cookie分类

cookie 是有时间限制的,根据生命期不同分成两种:会话 cookie 和持久 cookie;

会话Cookie

如果不设置过期时间,则表示这个 cookie 的生命周期为从创建到浏览器关闭为止,只要关闭浏览器窗口,cookie 就消失了。这种生命期为浏览会话期的 cookie 被称为会话 cookie。会话 cookie 一般不保存在硬盘上而是保存在内存里。

持久Cookie

如果设置了过期时间 (setMaxAge (606024)),浏览器就会把 cookie 保存到硬盘上,关闭后再次打开浏览器,这些 cookie 依然有效直到超过设定的过期时间。存储在硬盘上的 cookie 可以在不同的浏览器进程间共享,比如两个 IE 窗口。而对于保存在内存的 cookie,不同的浏览器有不同的处理方式。

Session

session与cookie关系

session 和 cookie 的目的相同,都是为了克服 http 协议无状态的缺陷,但完成的方法不同。session 通过 cookie,在客户端保存 session id,而将用户的其他会话消息保存在服务端的 session 对象中,与此相对的,cookie 需要将所有信息都保存在客户端。

Go如何使用session

如何发送session唯一标识符:

存入cookie和URL重写

生产环境最佳实践

  1. 优先用 Cookie 存储:

    • 必须配置 HttpOnly: true(防 XSS)、Secure: true(仅 HTTPS 传输)、SameSite: Lax/Strict(防 CSRF);
    • 用 Redis 替代内存存储 session(避免服务重启丢失,支持分布式)。
  2. URL 重写仅作为降级:

    • 仅在检测到用户禁用 Cookie 时触发,且需对 session_id 做加密 / 有效期限制;

    • 禁止在 URL 中携带敏感操作(如支付、修改密码)的请求。

    • URL 重写(兼容无 Cookie 场景)

      当用户禁用浏览器 Cookie 时,无法通过 Cookie 携带 session_id,此时用 URL 重写—— 把 session_id 拼接到 URL 中(如 /profile;jsessionid=xxx/profile?sid=xxx),前端每次请求都携带这个 URL,服务器从 URL 中提取 session_id

维度 Cookie 存储 URL 重写
安全性 高(HttpOnly/Secure/SameSite 防护) 低(URL 易泄露、被劫持)
开发成本 低(浏览器自动管理,库封装) 高(手动拼接所有 URL,前端配合)
用户体验 无感知(自动携带) 有感知(URL 带额外参数)
适用场景 99% 的主流场景(登录、用户态管理) 仅兼容用户禁用 Cookie 的降级场景
Go 实现 用 gorilla/sessions 自动处理 手动拼接 URL + 解析查询 / 路径参数

Restful风格

go语言多路复用技术

在http包中提供了ServeMux实现多路复用器,它会对URL进行解析,然后重定向到正确的处理器上

// 1. 定义用户结构体(模拟数据库模型)
type User struct {
    
      
	ID   int    `json:"id"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}

// 2. 模拟内存数据库(实际项目替换为MySQL/PostgreSQL)
var users = []User{
    
      
	{
    
      ID: 1, Name: "张三", Age: 20},
	{
    
      ID: 2, Name: "李四", Age: 22},
}

// 3. 统一响应结构体(RESTful 规范:返回结构化JSON)
type Response struct {
    
      
	Code int         `json:"code"` // 业务状态码
	Msg  string      `json:"msg"`  // 提示信息
	Data interface{
    
      } `json:"data"` // 数据体
}

// 辅助函数:统一返回JSON响应(简化代码)
func sendJSON(w http.ResponseWriter, statusCode int, resp Response) {
    
      
	w.Header().Set("Content-Type", "application/json") // 设置响应头为JSON
	w.WriteHeader(statusCode)                          // 设置HTTP状态码
	json.NewEncoder(w).Encode(resp)                    // 序列化JSON并返回
}

// **************************
// RESTful 接口处理函数
// **************************

// listUsers 获取所有用户(GET /users)
func listUsers(w http.ResponseWriter, r *http.Request) {
    
      
	// RESTful:查询成功返回200 + 所有用户数据
	sendJSON(w, http.StatusOK, Response{
    
      
		Code: 200,
		Msg:  "查询成功",
		Data: users,
	})
}

// getUser 获取单个用户(GET /users/{id})
func getUser(w http.ResponseWriter, r *http.Request) {
    
      
	// 1. 提取mux路由参数(你代码里的 vars["key"] 核心逻辑)
	vars := mux.Vars(r)
	idStr := vars["id"]
	id, err := strconv.Atoi(idStr)
	if err != nil {
    
      
		// RESTful:参数错误返回400
		sendJSON(w, http.StatusBadRequest, Response{
    
      
			Code: 400,
			Msg:  "无效的用户ID:" + idStr,
			Data: nil,
		})
		return
	}

	// 2. 查找用户
	for _, user := range users {
    
      
		if user.ID == id {
    
      
			sendJSON(w, http.StatusOK, Response{
    
      
				Code: 200,
				Msg:  "查询成功",
				Data: user,
			})
			return
		}
	}

	// 3. 资源不存在返回404
	sendJSON(w, http.StatusNotFound, Response{
    
      
		Code: 404,
		Msg:  "用户不存在",
		Data: nil,
	})
}

// createUser 创建用户(POST /users)
func createUser(w http.ResponseWriter, r *http.Request) {
    
      
	// 1. 解析请求体(JSON格式)
	var newUser User
	err := json.NewDecoder(r.Body).Decode(&newUser)
	if err != nil {
    
      
		sendJSON(w, http.StatusBadRequest, Response{
    
      
			Code: 400,
			Msg:  "参数解析失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 2. 模拟生成ID(实际用数据库自增ID)
	newUser.ID = len(users) + 1
	users = append(users, newUser)

	// 3. RESTful:创建成功返回201
	sendJSON(w, http.StatusCreated, Response{
    
      
		Code: 201,
		Msg:  "创建用户成功",
		Data: newUser,
	})
}

// **************************
// 主函数:路由注册 + 启动服务
// **************************
func main() {
    
      
	// 1. 创建mux路由器(你代码里的核心对象)
	router := mux.NewRouter()

	// 2. 静态文件服务(保留你代码里的逻辑)
	router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

	// 3. RESTful 核心:按「资源+HTTP方法」注册路由
	// 注意:gorilla/mux 用 Methods() 绑定HTTP方法,这是实现RESTful的关键
	router.HandleFunc("/users", listUsers).Methods("GET")       // 获取所有用户
	router.HandleFunc("/users", createUser).Methods("POST")     // 创建用户
	router.HandleFunc("/users/{id}", getUser).Methods("GET")    // 获取单个用户

	err := http.ListenAndServe(":80", router)
	if err != nil {
    
      
		fmt.Printf("服务启动失败:%v\n", err)
	}
}