缓存问题:
- Git 会缓存跟踪状态,即使添加了忽略规则,已跟踪的文件仍会被继续跟踪
解决方案:
git rm -r --cached /backend/tmp/ git add . git commit -m "停止跟踪 tmp 目录"
缓存问题:
解决方案:
git rm -r --cached /backend/tmp/
git add .
git commit -m "停止跟踪 tmp 目录"
go get github.com/joho/godotenv
// go install github.com/joho/godotenv/cmd/godotenv@latest
将应用程序配置添加到项目根目录中的 .env
文件中:
S3_BUCKET=YOURS3BUCKET
SECRET_KEY=YOURSECRETKEYGOESHERE
然后,在Go 应用程序中,可以执行以下作
package main
import (
"log"
"os"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
s3Bucket := os.Getenv("S3_BUCKET")
secretKey := os.Getenv("SECRET_KEY")
// now do something with s3 or whatever
}
函数未导出:
在Go语言中,只有首字母大写的标识符(如函数、变量等)才在包外可见。如果希望home
函数可以通过service.home
调用,需要将其定义为导出的函数:
func Home(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello world"})
}
这样,主程序可以通过service.Home
访问它。小写函数home
是私有的,主程序无法访问。
在Gin框架中,中间件(Middleware)是一个函数,它具有以下特点:
func(c *gin.Context) {
// 中间件逻辑
}
更准确地说,Gin中间件是一个符合gin.HandlerFunc
类型的函数,其签名如下:
type HandlerFunc func(*Context)
gin.Context
参数可以访问请求和操作响应处理流程控制:
c.Next()
继续执行后续中间件或处理函数c.Abort()
终止后续中间件执行执行时机:
一个典型的中间件执行流程:
func LoggerMiddleware(c *gin.Context) {
// 1. 请求处理前的逻辑
start := time.Now()
// 2. 调用下一个中间件或路由处理函数
c.Next()
// 3. 请求处理后的逻辑
latency := time.Since(start)
log.Printf("请求处理耗时: %v", latency)
}
router := gin.Default()
router.Use(LoggerMiddleware) // 全局生效
authGroup := router.Group("/admin")
authGroup.Use(AuthMiddleware) // 只对该路由组生效
router.GET("/secure", AuthMiddleware, func(c *gin.Context) {
// 处理逻辑
})
Gin自带了一些常用中间件:
// 1. 恢复中间件(Recovery)
router := gin.Default() // 默认包含Logger和Recovery中间件
// 2. 静态文件服务
router.Static("/assets", "./assets")
// 3. 自定义中间件
router.Use(func(c *gin.Context) {
c.Set("requestId", uuid.New().String())
c.Next()
})
一个完整的自定义中间件示例:
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 生成唯一请求ID
requestID := uuid.New().String()
// 设置到上下文和响应头
c.Set("Request-ID", requestID)
c.Writer.Header().Set("X-Request-ID", requestID)
// 继续处理
c.Next()
// 可以在这里添加请求后的处理逻辑
}
}
// 使用方式
router.Use(RequestIDMiddleware())
中间件的执行顺序遵循"洋葱模型":
请求 -> 中间件1前 -> 中间件2前 -> 处理函数 -> 中间件2后 -> 中间件1后 -> 响应
func AuthMiddleware(c *gin.Context) {
if !checkAuth(c) {
// 中止后续中间件执行
c.Abort()
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
return
}
c.Next()
}
总结来说,Gin中间件是一种强大的机制,允许开发者在请求处理流程的不同阶段插入自定义逻辑,实现横切关注点(Cross-cutting concerns)的分离和复用。
go install github.com/gin-gonic/gin@latest
// 或 go get -u github.com/gin-gonic/gin
import "github.com/gin-gonic/gin"
生成实例:
// 使用默认中间件(logger 和 recovery 中间件)创建 gin 路由
r := gin.Default()
运行
r.Run()
// 默认在 8080 端口启动服务,除非定义了一个 PORT 的环境变量。
// r.Run(":8888") 传入参数切换端口
r.GET("/someGet", getting)
r.POST("/somePost", posting)
r.PUT("/somePut", putting)
r.DELETE("/someDelete", deleting)
r.PATCH("/somePatch", patching)
r.HEAD("/someHead", head)
r.OPTIONS("/someOptions", options)
注:
对于每一种路由都是传入两个参数,一个是路由的路径,第二个是处理请求的函数
对于请求函数例如:
func(c *gin.Context) {
// 其他处理代码
...
// 其他处理代码
// 响应
c.String(http.StatusOK, "hello world")
}
需要传入一个参数 c 为gin的上下文类型
gin中存在多种不同的响应类型
func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "hello world",
"status": "success",
})
}
func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
}
// gin.H{} 传递给模板,模板中使用{{.title}}使用,其他的更多用法比如循环,分支,之类可以查找go官方文档
// https://pkg.go.dev/text/template
// https://pkg.go.dev/html/template
需要先设置模板目录:
router := gin.Default()
router.LoadHTMLGlob("templates/*")
func(c *gin.Context) {
c.XML(http.StatusOK, gin.H{
"message": "hello world",
"status": "success",
})
}
func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{
"message": "hello world",
"status": "success",
})
}
func(c *gin.Context) {
data := &protoexample.Test{
Label: proto.String("hello"),
Reps: []int64{1, 2, 3},
}
c.ProtoBuf(http.StatusOK, data)
}
func(c *gin.Context) {
c.File("/path/to/file")
}
func(c *gin.Context) {
c.FileFromFS("/path/to/file", http.Dir("/var/www"))
}
func(c *gin.Context) {
reader := strings.NewReader("some data")
c.DataFromReader(http.StatusOK, int64(reader.Len()), "text/plain", reader, nil)
}
func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "<https://www.google.com/>")
}
func(c *gin.Context) {
c.Request.URL.Path = "/"
r.HandleContext(c)
}
func(c *gin.Context) {
c.String(http.StatusOK, "hello world")
}
关于Gin框架中的gin.H
gin.H
是Gin框架中一个非常实用的快捷方式,它实际上是一个类型别名(type alias),定义如下:
type H map[string]interface{}
详细解释
本质:gin.H
就是一个map[string]interface{}
的别名
string
是键的类型interface{}
是值的类型,表示可以接受任何类型的值用途:
// 创建一个简单的JSON响应
data := gin.H{
"name": "John",
"age": 30,
"tags": []string{"golang", "gin"},
}
c.JSON(http.StatusOK, data)
优点:
map[string]interface{}
更简洁// 使用gin.H
gin.H{"key": "value"}
// 等同于
map[string]interface{}{"key": "value"}
// 也等同于
struct {
Key string `json:"key"`
}{Key: "value"}
data := gin.H{
"user": gin.H{
"name": "Alice",
"age": 25,
},
"status": "active",
}
注意事项:
gin.H
更高效gin.H
是Gin框架中一个简单但强大的工具,它简化了动态数据的构建过程,特别是在构建API响应时非常有用。
在Gin中使用自定义结构体的示例
当然可以。在Gin框架中,使用自定义结构体作为响应通常比gin.H
更规范,特别是在处理复杂数据结构时。以下是几个示例:
// 定义自定义结构体
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
func getUser(c *gin.Context) {
user := User{
ID: 1,
Username: "john_doe",
Email: "[email protected]",
}
c.JSON(http.StatusOK, user)
}
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type UserProfile struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
}
func getProfile(c *gin.Context) {
profile := UserProfile{
Name: "Alice",
Age: 28,
Address: Address{
City: "Shanghai",
Country: "China",
},
}
c.JSON(http.StatusOK, profile)
}
type Post struct {
ID int `json:"id"`
Title string `json:"title"`
Tags []string `json:"tags"`
}
func getPosts(c *gin.Context) {
posts := []Post{
{
ID: 1,
Title: "Gin框架入门",
Tags: []string{"golang", "web"},
},
{
ID: 2,
Title: "Go语言并发编程",
Tags: []string{"golang", "concurrency"},
},
}
c.JSON(http.StatusOK, posts)
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
Exp int64 `json:"exp"`
}
func login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 验证逻辑...
resp := LoginResponse{
Token: "generated_token_here",
Exp: time.Now().Add(24 * time.Hour).Unix(),
}
c.JSON(http.StatusOK, resp)
}
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Description string `json:"description,omitempty"` // 为空时不显示
Stock int `json:"-"` // 永远不序列化
}
func getProduct(c *gin.Context) {
product := Product{
ID: 101,
Name: "Laptop",
Price: 999.99,
// Description和Stock未赋值
}
c.JSON(http.StatusOK, product)
// 输出: {"id":101,"name":"Laptop","price":999.99}
}
一、路由传入的参数类型
router.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id") // 获取路径参数
})
// 请求: /search?q=gin&page=1
q := c.Query("q") // "gin"
page := c.DefaultQuery("page", "1") // 带默认值
// Content-Type: application/x-www-form-urlencoded
name := c.PostForm("name")
age := c.DefaultPostForm("age", "18") // 带默认值
用于文件上传:
file, _ := c.FormFile("file")
// 处理文件上传
if file, err := c.FormFile("avatar"); err == nil {
// 保存文件逻辑
user.Avatar = "/uploads/" + file.Filename
}
token := c.GetHeader("Authorization")
value, err := c.Cookie("cookie_name")
body, _ := ioutil.ReadAll(c.Request.Body)
二、POST请求可传递的参数类型
POST请求可以通过以下Content-Type传递不同类型的参数:
application/x-www-form-urlencoded
c.PostForm()
获取// 客户端发送: name=John&age=30
name := c.PostForm("name")
multipart/form-data
c.FormFile()
获取文件c.PostForm()
获取普通字段file, _ := c.FormFile("avatar")
name := c.PostForm("username")
application/json
c.ShouldBindJSON()
绑定到结构体type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
var user User
if err := c.ShouldBindJSON(&user); err != nil {
// 错误处理
}
//例如:
// 绑定JSON或Form数据
//var user User
//if err := c.ShouldBind(&user); err != nil {
// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
// return
//}
text/xml
/ application/xml
c.ShouldBindXML()
绑定if err := c.ShouldBindXML(&obj); err != nil {
// 错误处理
}
application/protobuf
var message pb.Message
if err := proto.Unmarshal(c.Request.Body, &message); err != nil {
// 错误处理
}
可以通过c.GetRawData()
获取原始数据后自行解析:
data, _ := c.GetRawData()
// 自定义解析逻辑
三、参数绑定最佳实践
Query()
, PostForm()
等方法ShouldBind
系列方法绑定到结构体FormFile()
四、完整示例
type User struct {
ID int `json:"id" form:"id"`
Username string `json:"username" form:"username"`
Avatar string `json:"avatar"` // 文件上传单独处理
}
func updateUser(c *gin.Context) {
// 路径参数
id := c.Param("id")
// 绑定JSON或Form数据
var user User
if err := c.ShouldBind(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 处理文件上传
if file, err := c.FormFile("avatar"); err == nil {
// 保存文件逻辑
user.Avatar = "/uploads/" + file.Filename
}
// 处理其他逻辑...
}
如果有一组路由,前缀都是/api/v1
开头,是否每个路由都需要加上/api/v1
这个前缀呢?答案是不需要,分组路由可以解决这个问题。利用分组路由还可以更好地实现权限控制,例如将需要登录鉴权的路由放到同一分组中去,简化权限控制。
// group routes 分组路由
defaultHandler := func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"path": c.FullPath(),
})
}
// group: v1
v1 := r.Group("/v1")
{
v1.GET("/posts", defaultHandler)
v1.GET("/series", defaultHandler)
}
// group: v2
v2 := r.Group("/v2")
{
v2.GET("/posts", defaultHandler)
v2.GET("/series", defaultHandler)
}
示例:
// 作用于全局
r.Use(gin.Logger())
r.Use(gin.Recovery())
// 作用于单个路由
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)
// 作用于某个组
authorized := r.Group("/")
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
}
参考文档: