跳到主要内容

Gin Web 框架

Gin 是一个用 Go 语言编写的高性能 HTTP Web 框架。它基于 HttpRouter 实现,提供了类似 Martini 的 API,但性能比 Martini 快 40 倍。Gin 是目前 Go 社区最受欢迎的 Web 框架之一。

安装

go get -u github.com/gin-gonic/gin

快速开始

第一个 Gin 应用

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})

r.Run(":8080")
}

运行后访问 http://localhost:8080/ping,将返回 {"message":"pong"}

代码解释

  • gin.Default():创建一个默认的路由引擎,包含 Logger 和 Recovery 中间件
  • r.GET():注册 GET 路由
  • c *gin.Context:请求上下文,包含请求和响应信息
  • c.JSON():返回 JSON 格式响应
  • gin.Hmap[string]interface{} 的简写
  • r.Run():启动 HTTP 服务器,默认端口 8080

路由

基本路由

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/users", getUsers)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
r.PATCH("/users/:id", patchUser)
r.HEAD("/users", headUsers)
r.OPTIONS("/users", optionsUsers)

r.Run(":8080")
}

func getUsers(c *gin.Context) {
c.JSON(200, gin.H{"method": "GET"})
}

func createUser(c *gin.Context) {
c.JSON(201, gin.H{"method": "POST"})
}

func updateUser(c *gin.Context) {
c.JSON(200, gin.H{"method": "PUT"})
}

func deleteUser(c *gin.Context) {
c.JSON(200, gin.H{"method": "DELETE"})
}

func patchUser(c *gin.Context) {
c.JSON(200, gin.H{"method": "PATCH"})
}

func headUsers(c *gin.Context) {
c.Status(200)
}

func optionsUsers(c *gin.Context) {
c.JSON(200, gin.H{"method": "OPTIONS"})
}

路径参数

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(200, "Hello %s", name)
})

r.GET("/user/:name/:action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
c.String(200, "%s is %s", name, action)
})

r.GET("/files/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
c.String(200, "File path: %s", filepath)
})

r.Run(":8080")
}

参数说明

  • :name:匹配单个路径段,如 /user/john
  • *filepath:匹配剩余所有路径,如 /files/a/b/c.txt

查询参数

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/search", func(c *gin.Context) {
keyword := c.Query("keyword")
page := c.DefaultQuery("page", "1")

c.JSON(200, gin.H{
"keyword": keyword,
"page": page,
})
})

r.GET("/filter", func(c *gin.Context) {
tags := c.QueryArray("tag")
c.JSON(200, gin.H{
"tags": tags,
})
})

r.GET("/map", func(c *gin.Context) {
params := c.QueryMap("filter")
c.JSON(200, gin.H{
"filter": params,
})
})

r.Run(":8080")
}

访问示例

  • /search?keyword=go&page=2
  • /filter?tag=go&tag=web&tag=api
  • /map?filter[name]=张三&filter[age]=25

表单参数

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.POST("/form", func(c *gin.Context) {
name := c.PostForm("name")
email := c.DefaultPostForm("email", "[email protected]")

c.JSON(200, gin.H{
"name": name,
"email": email,
})
})

r.POST("/upload", func(c *gin.Context) {
tags := c.PostFormArray("tag")
c.JSON(200, gin.H{
"tags": tags,
})
})

r.Run(":8080")
}

路由分组

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

api := r.Group("/api")
{
api.GET("/status", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})

users := api.Group("/users")
{
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}

posts := api.Group("/posts")
{
posts.GET("", listPosts)
posts.POST("", createPost)
posts.GET("/:id", getPost)
}
}

admin := r.Group("/admin")
admin.Use(AuthMiddleware())
{
admin.GET("/dashboard", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "admin dashboard"})
})
}

r.Run(":8080")
}

func listUsers(c *gin.Context) {
c.JSON(200, []gin.H{{"id": 1, "name": "张三"}})
}

func createUser(c *gin.Context) {
c.JSON(201, gin.H{"id": 2, "name": "李四"})
}

func getUser(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "name": "张三"})
}

func updateUser(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "name": "张三更新"})
}

func deleteUser(c *gin.Context) {
c.Status(204)
}

func listPosts(c *gin.Context) {
c.JSON(200, []gin.H{{"id": 1, "title": "文章1"}})
}

func createPost(c *gin.Context) {
c.JSON(201, gin.H{"id": 2, "title": "文章2"})
}

func getPost(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "title": "文章"})
}

func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "未授权"})
c.Abort()
return
}
c.Next()
}
}

请求处理

获取请求信息

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/info", func(c *gin.Context) {
c.JSON(200, gin.H{
"method": c.Request.Method,
"path": c.Request.URL.Path,
"host": c.Request.Host,
"remoteAddr": c.ClientIP(),
"headers": c.Request.Header,
"userAgent": c.GetHeader("User-Agent"),
"contentType": c.ContentType(),
})
})

r.Run(":8080")
}

绑定请求参数

Gin 支持多种绑定方式,可以自动将请求参数绑定到结构体:

package main

import (
"github.com/gin-gonic/gin"
)

type User struct {
Name string `form:"name" json:"name" binding:"required"`
Email string `form:"email" json:"email" binding:"required,email"`
Age int `form:"age" json:"age" binding:"gte=0,lte=130"`
}

func main() {
r := gin.Default()

r.GET("/query", func(c *gin.Context) {
var user User
if err := c.ShouldBindQuery(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})

r.POST("/form", func(c *gin.Context) {
var user User
if err := c.ShouldBind(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})

r.POST("/json", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})

r.POST("/bind", func(c *gin.Context) {
var user User
if err := c.ShouldBind(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})

r.Run(":8080")
}

绑定 URI 参数

package main

import (
"github.com/gin-gonic/gin"
)

type UserURI struct {
ID string `uri:"id" binding:"required"`
Name string `uri:"name" binding:"required"`
}

func main() {
r := gin.Default()

r.GET("/user/:id/:name", func(c *gin.Context) {
var user UserURI
if err := c.ShouldBindUri(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"id": user.ID,
"name": user.Name,
})
})

r.Run(":8080")
}

绑定 Header

package main

import (
"github.com/gin-gonic/gin"
)

type HeaderParams struct {
Token string `header:"Authorization" binding:"required"`
Rate int `header:"Rate"`
}

func main() {
r := gin.Default()

r.GET("/header", func(c *gin.Context) {
var h HeaderParams
if err := c.ShouldBindHeader(&h); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"token": h.Token,
"rate": h.Rate,
})
})

r.Run(":8080")
}

响应处理

JSON 响应

package main

import (
"github.com/gin-gonic/gin"
)

type User struct {
ID int `json:"id"`
Name string `json:"name"`
}

func main() {
r := gin.Default()

r.GET("/json", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello"})
})

r.GET("/json/struct", func(c *gin.Context) {
c.JSON(200, User{ID: 1, Name: "张三"})
})

r.GET("/json/array", func(c *gin.Context) {
users := []User{
{ID: 1, Name: "张三"},
{ID: 2, Name: "李四"},
}
c.JSON(200, users)
})

r.GET("/json/indent", func(c *gin.Context) {
c.IndentedJSON(200, User{ID: 1, Name: "张三"})
})

r.GET("/json/secure", func(c *gin.Context) {
c.SecureJSON(200, []string{"张三", "李四"})
})

r.Run(":8080")
}

其他响应类型

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/string", func(c *gin.Context) {
c.String(200, "Hello, %s", "World")
})

r.GET("/xml", func(c *gin.Context) {
c.XML(200, gin.H{"message": "hello"})
})

r.GET("/yaml", func(c *gin.Context) {
c.YAML(200, gin.H{"message": "hello"})
})

r.GET("/html", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "主页",
})
})

r.GET("/file", func(c *gin.Context) {
c.File("./static/file.txt")
})

r.GET("/attachment", func(c *gin.Context) {
c.FileAttachment("./static/file.txt", "download.txt")
})

r.GET("/data", func(c *gin.Context) {
c.Data(200, "text/plain; charset=utf-8", []byte("Hello"))
})

r.GET("/redirect", func(c *gin.Context) {
c.Redirect(302, "/string")
})

r.Run(":8080")
}

中间件

内置中间件

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.New()

r.Use(gin.Logger())
r.Use(gin.Recovery())

r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})

r.Run(":8080")
}

自定义中间件

package main

import (
"log"
"time"

"github.com/gin-gonic/gin"
)

func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path

c.Next()

latency := time.Since(start)
status := c.Writer.Status()

log.Printf("[%s] %s %d %v", c.Request.Method, path, status, latency)
}
}

func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "未授权"})
c.Abort()
return
}

c.Set("userID", "123")
c.Next()
}
}

func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}

c.Next()
}
}

func main() {
r := gin.New()

r.Use(Logger())
r.Use(gin.Recovery())
r.Use(CORSMiddleware())

public := r.Group("/api")
{
public.POST("/login", login)
}

protected := r.Group("/api")
protected.Use(Auth())
{
protected.GET("/profile", profile)
}

r.Run(":8080")
}

func login(c *gin.Context) {
c.JSON(200, gin.H{"token": "fake-token"})
}

func profile(c *gin.Context) {
userID := c.GetString("userID")
c.JSON(200, gin.H{"userID": userID})
}

中间件控制

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/normal", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "normal"})
})

r.GET("/abort", func(c *gin.Context) {
c.JSON(401, gin.H{"error": "unauthorized"})
c.Abort()
})

r.GET("/next", func(c *gin.Context) {
c.Next()
c.JSON(200, gin.H{"message": "after next"})
})

r.Run(":8080")
}

文件上传

单文件上传

package main

import (
"fmt"
"path/filepath"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.MaxMultipartMemory = 8 << 20

r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

filename := filepath.Base(file.Filename)
dst := filepath.Join("./uploads", filename)

if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}

c.JSON(200, gin.H{
"filename": filename,
"size": file.Size,
"header": file.Header,
})
})

r.Run(":8080")
}

多文件上传

package main

import (
"net/http"
"path/filepath"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.POST("/uploads", func(c *gin.Context) {
form, _ := c.MultipartForm()
files := form.File["files"]

var results []gin.H

for _, file := range files {
filename := filepath.Base(file.Filename)
dst := filepath.Join("./uploads", filename)

if err := c.SaveUploadedFile(file, dst); err != nil {
results = append(results, gin.H{
"filename": filename,
"error": err.Error(),
})
continue
}

results = append(results, gin.H{
"filename": filename,
"size": file.Size,
})
}

c.JSON(http.StatusOK, gin.H{"files": results})
})

r.Run(":8080")
}

模板渲染

HTML 模板

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.LoadHTMLGlob("templates/**/*")

r.GET("/index", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "主页",
"message": "欢迎来到 Gin",
})
})

r.GET("/posts/index", func(c *gin.Context) {
c.HTML(200, "posts/index.html", gin.H{
"title": "文章列表",
"posts": []gin.H{
{"id": 1, "title": "文章1"},
{"id": 2, "title": "文章2"},
},
})
})

r.Run(":8080")
}

静态文件

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.Static("/assets", "./assets")
r.StaticFile("/favicon.ico", "./favicon.ico")
r.StaticFS("/static", http.Dir("./static"))

r.Run(":8080")
}

数据验证

Gin 使用 validator 包进行数据验证:

package main

import (
"github.com/gin-gonic/gin"
)

type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=20"`
Password string `json:"password" binding:"required,min=6,max=20"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=1,lte=120"`
Phone string `json:"phone" binding:"omitempty,len=11"`
Website string `json:"website" binding:"omitempty,url"`
}

func main() {
r := gin.Default()

r.POST("/register", func(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

c.JSON(200, gin.H{
"username": req.Username,
"email": req.Email,
"age": req.Age,
})
})

r.Run(":8080")
}

常用验证标签

标签说明
required必填
min=3最小长度/值
max=20最大长度/值
gte=1大于等于
lte=120小于等于
email邮箱格式
urlURL 格式
len=11精确长度
oneof=男 女枚举值
dive深入切片/数组验证

错误处理

统一错误处理

package main

import (
"github.com/gin-gonic/gin"
)

type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}

func Success(c *gin.Context, data interface{}) {
c.JSON(200, Response{
Code: 0,
Message: "success",
Data: data,
})
}

func Error(c *gin.Context, code int, message string) {
c.JSON(code, Response{
Code: code,
Message: message,
})
}

func main() {
r := gin.Default()

r.GET("/success", func(c *gin.Context) {
Success(c, gin.H{"name": "张三"})
})

r.GET("/error", func(c *gin.Context) {
Error(c, 400, "参数错误")
})

r.NoRoute(func(c *gin.Context) {
Error(c, 404, "路由不存在")
})

r.NoMethod(func(c *gin.Context) {
Error(c, 405, "方法不允许")
})

r.Run(":8080")
}

Recovery 中间件

package main

import (
"github.com/gin-gonic/gin"
)

func RecoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
c.JSON(500, gin.H{
"code": 500,
"message": "服务器内部错误",
"error": err,
})
c.Abort()
}
}()
c.Next()
}
}

func main() {
r := gin.New()
r.Use(RecoveryMiddleware())

r.GET("/panic", func(c *gin.Context) {
panic("故意触发的错误")
})

r.Run(":8080")
}

WebSocket 支持

Gin 框架本身不内置 WebSocket 支持,但可以与 gorilla/websocket 库配合使用来实现 WebSocket 功能。WebSocket 提供了全双工通信通道,适合实时应用如聊天、游戏、实时数据推送等场景。

安装依赖

go get github.com/gorilla/websocket

基本 WebSocket 示例

package main

import (
"log"
"net/http"

"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)

// WebSocket 升级器
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// 生产环境应该检查请求来源
CheckOrigin: func(r *http.Request) bool {
return true
},
}

func main() {
r := gin.Default()

// WebSocket 端点
r.GET("/ws", func(c *gin.Context) {
// 将 HTTP 连接升级为 WebSocket 连接
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Println("WebSocket 升级失败:", err)
return
}
defer conn.Close()

// 消息循环
for {
// 读取消息
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("读取消息失败:", err)
break
}

log.Printf("收到消息: %s", message)

// 回显消息
err = conn.WriteMessage(messageType, message)
if err != nil {
log.Println("发送消息失败:", err)
break
}
}
})

r.Run(":8080")
}

代码解释

  • websocket.Upgrader:用于将 HTTP 连接升级为 WebSocket 连接
  • CheckOrigin:用于验证请求来源,开发环境可以返回 true,生产环境应验证来源
  • conn.ReadMessage():从 WebSocket 连接读取消息
  • conn.WriteMessage():向 WebSocket 连接发送消息
  • messageType:消息类型,websocket.TextMessage(文本)或 websocket.BinaryMessage(二进制)

广播消息示例

package main

import (
"log"
"net/http"
"sync"

"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)

// 客户端连接管理
type Hub struct {
clients map[*websocket.Conn]bool
broadcast chan []byte
register chan *websocket.Conn
unregister chan *websocket.Conn
mu sync.RWMutex
}

func NewHub() *Hub {
return &Hub{
clients: make(map[*websocket.Conn]bool),
broadcast: make(chan []byte),
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
}
}

func (h *Hub) Run() {
for {
select {
case conn := <-h.register:
h.mu.Lock()
h.clients[conn] = true
h.mu.Unlock()
log.Printf("客户端连接,当前连接数: %d", len(h.clients))

case conn := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[conn]; ok {
delete(h.clients, conn)
conn.Close()
}
h.mu.Unlock()
log.Printf("客户端断开,当前连接数: %d", len(h.clients))

case message := <-h.broadcast:
h.mu.RLock()
for conn := range h.clients {
err := conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
conn.Close()
delete(h.clients, conn)
}
}
h.mu.RUnlock()
}
}
}

var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}

func main() {
hub := NewHub()
go hub.Run()

r := gin.Default()

r.GET("/ws", func(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}

hub.register <- conn

defer func() {
hub.unregister <- conn
}()

for {
_, message, err := conn.ReadMessage()
if err != nil {
break
}
// 广播消息给所有客户端
hub.broadcast <- message
}
})

r.Run(":8080")
}

JSON 消息处理

type Message struct {
Type string `json:"type"`
Content interface{} `json:"content"`
}

r.GET("/ws", func(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()

for {
messageType, data, err := conn.ReadMessage()
if err != nil {
break
}

// 解析 JSON 消息
var msg Message
if err := json.Unmarshal(data, &msg); err != nil {
log.Println("JSON 解析失败:", err)
continue
}

// 根据消息类型处理
switch msg.Type {
case "chat":
// 处理聊天消息
response, _ := json.Marshal(Message{
Type: "chat",
Content: msg.Content,
})
conn.WriteMessage(messageType, response)
case "ping":
// 心跳响应
conn.WriteMessage(messageType, []byte(`{"type":"pong"}`))
}
}
})

流式响应

流式响应(Streaming Response)允许服务器逐步发送数据,而不是等待所有数据准备好后一次性发送。这在处理大量数据或需要实时推送的场景中非常有用。

分块传输(Chunked Transfer)

package main

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

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/stream", func(c *gin.Context) {
// 设置响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("Transfer-Encoding", "chunked")

// 发送响应头
c.Status(http.StatusOK)

// 获取 Flusher 接口
flusher, ok := c.Writer.(http.Flusher)
if !ok {
c.String(http.StatusInternalServerError, "不支持流式响应")
return
}

// 流式发送数据
for i := 0; i < 10; i++ {
// 写入数据
fmt.Fprintf(c.Writer, "数据块 %d\n", i)

// 立即刷新缓冲区,发送给客户端
flusher.Flush()

// 模拟处理延迟
time.Sleep(time.Second)
}

fmt.Fprint(c.Writer, "流式传输完成\n")
flusher.Flush()
})

r.Run(":8080")
}

关键点

  • Transfer-Encoding: chunked:告诉客户端使用分块传输
  • http.Flusher:提供 Flush() 方法立即发送数据
  • 每次写入数据后调用 Flush() 确保数据立即发送

Server-Sent Events (SSE)

SSE 是一种服务器向客户端推送数据的技术,比 WebSocket 更简单,适合单向数据推送场景:

package main

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

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/events", func(c *gin.Context) {
// SSE 必需的响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")

flusher, ok := c.Writer.(http.Flusher)
if !ok {
c.String(http.StatusInternalServerError, "不支持 SSE")
return
}

// 发送事件
for i := 0; i < 10; i++ {
// SSE 格式: data: 消息内容\n\n
fmt.Fprintf(c.Writer, "data: {\"count\": %d, \"time\": \"%s\"}\n\n",
i, time.Now().Format(time.RFC3339))
flusher.Flush()
time.Sleep(time.Second)
}

// 发送自定义事件
fmt.Fprintf(c.Writer, "event: done\ndata: 完成\n\n")
flusher.Flush()
})

r.Run(":8080")
}

前端接收 SSE 示例

<script>
const eventSource = new EventSource('/events');

// 接收默认消息
eventSource.onmessage = function(event) {
console.log('收到消息:', JSON.parse(event.data));
};

// 接收自定义事件
eventSource.addEventListener('done', function(event) {
console.log('完成:', event.data);
eventSource.close();
});

// 错误处理
eventSource.onerror = function(error) {
console.error('SSE 错误:', error);
eventSource.close();
};
</script>

实时日志推送

r.GET("/logs", func(c *gin.Context) {
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")

flusher, _ := c.Writer.(http.Flusher)

// 模拟日志流
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()

for {
select {
case <-c.Request.Context().Done():
return
case t := <-ticker.C:
fmt.Fprintf(c.Writer, "data: [%s] 服务器运行正常\n\n", t.Format("15:04:05"))
flusher.Flush()
}
}
})

大文件流式下载

r.GET("/download", func(c *gin.Context) {
// 设置响应头
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=large_file.txt")

flusher, _ := c.Writer.(http.Flusher)

// 流式生成和发送数据
for i := 0; i < 1000; i++ {
c.Writer.Write([]byte(fmt.Sprintf("第 %d 行数据\n", i)))
flusher.Flush()
}
})

优雅关闭

优雅关闭确保服务器在停止时完成正在处理的请求,避免连接中断导致的数据丢失或客户端错误。生产环境的服务器必须实现优雅关闭。

为什么需要优雅关闭?

当服务器收到终止信号(如部署更新或扩缩容)时,立即关闭会导致:

  • 正在处理的请求被中断
  • 客户端收到连接错误
  • 可能造成数据不一致
  • 用户体验受损

优雅关闭解决的问题

  • 完成正在处理的请求
  • 停止接受新请求
  • 清理资源(数据库连接、文件句柄等)
  • 实现零停机部署

Go 1.8+ 的优雅关闭实现

Go 1.8 及以上版本的 http.Server 内置了 Shutdown() 方法:

package main

import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second) // 模拟耗时请求
c.String(http.StatusOK, "Welcome Gin Server")
})

// 创建 HTTP 服务器
srv := &http.Server{
Addr: ":8080",
Handler: router,
}

// 在 goroutine 中启动服务器
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务器启动失败: %s\n", err)
}
}()

log.Println("服务器启动,监听 :8080")

// 等待中断信号
quit := make(chan os.Signal, 1)
// 捕获 SIGINT (Ctrl+C) 和 SIGTERM (kill)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

log.Println("正在关闭服务器...")

// 给正在处理的请求最多 5 秒时间完成
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// 优雅关闭
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("服务器强制关闭:", err)
}

log.Println("服务器已关闭")
}

信号说明

信号触发方式说明
SIGINTCtrl+C中断信号
SIGTERMkill 命令终止信号
SIGKILLkill -9强制终止(无法捕获)

带资源清理的优雅关闭

package main

import (
"context"
"database/sql"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"

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

type App struct {
server *http.Server
db *sql.DB
}

func NewApp() *App {
// 初始化数据库
db, err := sql.Open("mysql", "user:pass@/dbname")
if err != nil {
log.Fatal(err)
}

// 初始化路由
router := gin.Default()

router.GET("/users", func(c *gin.Context) {
// 使用数据库...
c.JSON(200, gin.H{"message": "ok"})
})

return &App{
server: &http.Server{
Addr: ":8080",
Handler: router,
},
db: db,
}
}

func (a *App) Run() error {
// 启动服务器
go func() {
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务器错误: %v", err)
}
}()

log.Println("服务器启动在 :8080")

// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

log.Println("正在关闭服务器...")

// 创建关闭上下文
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// 关闭服务器
if err := a.server.Shutdown(ctx); err != nil {
log.Printf("服务器关闭错误: %v", err)
}

// 关闭数据库连接
if err := a.db.Close(); err != nil {
log.Printf("数据库关闭错误: %v", err)
}

log.Println("应用已关闭")
return nil
}

func main() {
app := NewApp()
app.Run()
}

使用第三方库实现优雅重启

如果需要在不中断服务的情况下重启(零停机部署),可以使用第三方库:

// 使用 endless 库
import "github.com/fvbock/endless"

func main() {
router := gin.Default()
router.GET("/", handler)

// endless 支持优雅重启
endless.ListenAndServe(":8080", router)
}

其他可选库:

  • manners:优雅的 Go HTTP 服务器
  • graceful:支持优雅关闭的 HTTP 服务器
  • grace:支持优雅重启和零停机部署
建议

对于 Go 1.8+ 版本,推荐使用标准库的 http.Server.Shutdown() 方法,无需引入第三方依赖。

项目结构示例

project/
├── main.go
├── config/
│ └── config.go
├── controllers/
│ ├── user.go
│ └── post.go
├── models/
│ ├── user.go
│ └── post.go
├── services/
│ ├── user.go
│ └── post.go
├── middlewares/
│ ├── auth.go
│ └── logger.go
├── routes/
│ └── router.go
├── utils/
│ └── response.go
├── templates/
│ └── index.html
└── static/
├── css/
└── js/

路由分离

package routes

import (
"github.com/gin-gonic/gin"
"myapp/controllers"
"myapp/middlewares"
)

func SetupRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())

api := r.Group("/api")
{
api.POST("/login", controllers.Login)
api.POST("/register", controllers.Register)
}

userGroup := api.Group("/users")
userGroup.Use(middlewares.Auth())
{
userGroup.GET("", controllers.ListUsers)
userGroup.GET("/:id", controllers.GetUser)
userGroup.PUT("/:id", controllers.UpdateUser)
userGroup.DELETE("/:id", controllers.DeleteUser)
}

return r
}
package main

import (
"myapp/routes"
)

func main() {
r := routes.SetupRouter()
r.Run(":8080")
}

小结

本章学习了 Gin 框架的核心内容和高级特性:

基础功能

  1. 路由:基本路由、路径参数、查询参数、路由分组
  2. 请求处理:获取请求信息、参数绑定
  3. 响应处理:JSON、XML、HTML、文件响应
  4. 中间件:内置中间件、自定义中间件
  5. 文件上传:单文件、多文件上传
  6. 模板渲染:HTML 模板、静态文件
  7. 数据验证:常用验证标签
  8. 错误处理:统一错误处理、Recovery

高级特性

  1. WebSocket 支持:与 gorilla/websocket 配合实现全双工通信
  2. 流式响应:分块传输、Server-Sent Events (SSE)
  3. 优雅关闭:确保请求完成、资源清理、零停机部署

最佳实践

  • 使用路由分组组织 API 结构
  • 中间件处理通用逻辑(认证、日志、恢复)
  • 统一的错误响应格式
  • 合理配置连接池和超时
  • 生产环境必须实现优雅关闭

练习

  1. 实现一个完整的 RESTful API,包含 CRUD 操作和 JWT 认证
  2. 编写 WebSocket 聊天室,支持多用户广播消息
  3. 实现 SSE 实时数据推送,模拟股票价格或日志流
  4. 为现有项目添加优雅关闭,包括数据库连接清理
  5. 使用流式响应实现大文件导出功能

参考资源