问题分析:
函数未导出:
在Go语言中,只有首字母大写的标识符(如函数、变量等)才在包外可见。如果希望home
函数可以通过service.home
调用,需要将其定义为导出的函数:
func Home(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello world"})
}
这样,主程序可以通过service.Home
访问它。小写函数home
是私有的,主程序无法访问。
函数未导出:
在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)
}
参考文档:
gorm golang对象关系映射框架
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
sqlite
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
mysql
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
其他数据库或高级用法:https://gorm.io/zh_CN/docs/connecting_to_the_database.html
例:
type Product struct {
gorm.Model
Code string
Price uint
}
GORM 通过将 Go 结构体(Go structs) 映射到数据库表来简化数据库交互。
模型是使用普通结构体定义的。 这些结构体可以包含具有基本Go类型、指针或这些类型的别名,甚至是自定义类型(只需要实现 database/sql
包中的Scanner和Valuer接口)。
ID
的字段作为每个模型的默认主键。snake_case
并为表名加上复数形式。 例如,一个 User
结构体在数据库中的表名变为 users
。snake_case
作为数据库中的列名。CreatedAt
和 UpdatedAt
来自动跟踪记录的创建和更新时间。gorm.Model
GORM提供了一个预定义的结构体,名为gorm.Model
,其中包含常用字段:
// gorm.Model 的定义
type Modelstruct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
gorm.Model
,以便自动包含这些字段。 这对于在不同模型之间保持一致性并利用GORM内置的约定非常有用,请参考嵌入结构。包含的字段:
ID
:每个记录的唯一标识符(主键)。CreatedAt
:在创建记录时自动设置为当前时间。UpdatedAt
:每当记录更新时,自动更新为当前时间。DeletedAt
:用于软删除(将记录标记为已删除,而实际上并未从数据库中删除)。文档:https://gorm.io/zh_CN/docs/models.html
迁移是指对数据库结构进行版本控制的机制,它允许您:
特性 | 迁移(Migration) | 创建数据库 |
---|---|---|
范围 | 表结构、字段、索引等 | 整个数据库实例 |
功能 | 修改现有结构、版本控制 | 创建新的数据库容器 |
粒度 | 细粒度(表/字段级) | 粗粒度(数据库级) |
目的 | 模式演进 | 数据库初始化 |
在GORM中,迁移通常通过AutoMigrate
方法实现:
// 自动迁移模型结构到数据库表
db.AutoMigrate(&User{}, &Product{}, &Order{})
// 这会在数据库中创建对应的表(如果不存在)或修改表结构以匹配模型
GORM还支持更复杂的迁移操作:
// 使用Migrator接口进行精细控制
m := db.Migrator()
// 检查表是否存在
m.HasTable(&User{})
// 创建表
m.CreateTable(&User{})
// 添加列
m.AddColumn(&User{}, "CreditCardNumber")
// 修改列
m.AlterColumn(&User{}, "Name")
// 删除列
m.DropColumn(&User{}, "Age")
// Create
db.Create(&Product{Code: "D42", Price: 100})
// Read
var product Product
db.First(&product, 1) // 根据整型主键查找
db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录
// Update - 将 product 的 price 更新为 200
db.Model(&product).Update("Price", 200)
// Update - 更新多个字段
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
// Delete - 删除 product
db.Delete(&product, 1)
注:
Product是一个结构体
type Product struct {
gorm.Model
Code string
Price uint
}
对于CRUD的每一个操作都是传入一个地址,直接修改作用的对象
例如对于create来说,就是传入一个Product类型的结构体。传入的结构体实例不需要为每个字段都赋值。
基本赋值规则:
零值处理:未赋值的字段会根据类型自动设置为相应的零值:
示例代码
type User struct {
ID uint
Name string
Age int
Email *string // 指针类型
IsActive bool
CreatedAt time.Time
}
// 只设置部分字段
user := User{
Name: "张三",
Age: 25,
// 其他字段未设置
}
result := db.Create(&user) // 成功插入,未设置字段使用零值
特殊情况处理
指针字段:
// 明确设置Email为NULL
user := User{
Name: "李四",
Email: nil, // 将存储为NULL
}
默认值:
gorm
标签设置默认值type Product struct {
Code string
Price float64 `gorm:"default:100"` // 默认值100
}
忽略字段:
gorm:"-"
标签可以完全忽略字段,不会被插入type User struct {
Name string
Temp string `gorm:"-"` // 不会存入数据库
}
数据库约束的影响
NOT NULL约束:
主键:
最佳实践
对于read,可以现在外面定义一个新的对应类型的变量,然后将这个变量传入,因为传的是地址,所以执行完,read之后对于product就是我们需要的一个变量,对应着数据库表中特定的某一行。
不可以直接使用未经过Read赋值的product变量进行Update操作。这样会导致更新失败或产生意外行为。
Update操作的工作机制
GORM的Update操作需要两个关键信息:
为什么需要先Read
主键标识:
模型状态:
具体问题分析
var product Product // 未赋值的product变量
db.Model(&product).Update("Price", 200)
这种情况下:
product
的ID字段为0(零值)正确做法
先查询再更新 (推荐):
var product Product
db.First(&product, 1) // 或通过其他条件查询
db.Model(&product).Update("Price", 200)
直接指定条件更新 (不依赖模型实例):
// 更新所有code="D42"的记录
db.Model(&Product{}).Where("code = ?", "D42").Update("Price", 200)
// 更新主键为1的记录
db.Model(&Product{}).Where("id = ?", 1).Update("Price", 200)
特殊情况
如果明确知道要更新的记录ID,也可以这样:
product := Product{ID: 1} // 只设置ID
db.Model(&product).Update("Price", 200)
但这种方法:
最佳实践
错误示例
// 错误用法1: 完全未初始化的模型
var p Product
db.Model(&p).Update("Price", 200) // 不会按预期工作
// 错误用法2: 只有部分字段的模型
p := Product{Code: "D42"}
db.Model(&p).Update("Price", 200) // 仍然不知道更新哪条记录
在GORM中执行Delete操作时,如果已经有一个完整赋值的product实例,通常不需要再额外传入primary key。以下是详细说明:
基本删除方式
var product Product
db.First(&product, 1) // 先查询获取完整记录
// 方式1:直接使用实例删除
db.Delete(&product) // 不需要再传ID
这种方式:
// 方式2:虽然可以运行,但冗余
db.Delete(&product, 1) // 传入实例同时再传ID
这种方式:
特殊情况处理
批量删除
如果需要批量删除,可以不使用实例:
// 删除所有price>100的记录
db.Where("price > ?", 100).Delete(&Product{})
// 删除主键为1的记录(不依赖实例)
db.Delete(&Product{}, 1)
软删除考虑
如果模型启用了软删除(有DeletedAt
字段):
// 软删除会自动设置DeletedAt时间
db.Delete(&product)
// 要真正硬删除可以加Unscoped
db.Unscoped().Delete(&product)
为什么不需要重复传主键?
GORM的智能处理:
WHERE id = ?
的条件数据一致性:
代码简洁性:
最佳实践建议
查询后删除:
var product Product
if err := db.First(&product, 1).Error; err != nil {
// 处理错误
}
db.Delete(&product)
批量删除:
// 明确条件
db.Where("code LIKE ?", "D%").Delete(&Product{})
避免的做法:
// 不推荐 - 可能误删
db.Delete(&Product{}, someVariable)
// 危险 - 可能清空整个表
db.Delete(&Product{})
基本概念
Unscoped()
是GORM提供的一个链式调用方法,用于绕过GORM的默认作用域限制,特别是针对软删除(Soft Delete)功能。
核心作用
绕过软删除机制:
DeletedAt
字段的模型启用软删除Unscoped()
可以操作已被软删除的记录执行"硬删除":
deleted_at
时间戳查询所有记录:
使用场景
// 普通删除(如果是软删除模型,只是设置deleted_at)
db.Delete(&user)
// 硬删除(真正从数据库删除)
db.Unscoped().Delete(&user)
// 默认查询会排除已软删除的记录
db.Find(&users)
// 查询所有记录(包括已软删除的)
db.Unscoped().Find(&users)
// 将deleted_at设置为NULL来恢复记录
db.Unscoped().Model(&user).Update("DeletedAt", nil)
实现原理
GORM的软删除是通过在SQL查询中自动添加条件实现的:
WHERE deleted_at IS NULL
Unscoped()
后会移除这个条件注意事项
数据安全:
关联删除:
审计要求:
最佳实践
// 1. 硬删除前先确认
if isAdmin {
db.Unscoped().Delete(&sensitiveData)
}
// 2. 批量硬删除时添加条件限制
db.Unscoped().Where("created_at < ?", time.Now().AddDate(-5,0,0)).Delete(&OldRecords{})
// 3. 开发环境可以设置全局禁用软删除
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&TestData{})
与其他方法的对比
方法 | 作用 | 影响 |
---|---|---|
Delete() | 普通删除 | 对软删除模型是逻辑删除 |
Unscoped().Delete() | 硬删除 | 物理删除记录 |
Where().Delete() | 条件删除 | 遵循模型作用域 |
Unscoped().Where().Delete() | 条件硬删除 | 无视软删除限制 |
总结:Unscoped()
是GORM中处理软删除边界的强大工具,但需要谨慎使用,特别是在生产环境中。理解它的工作原理有助于您更好地控制数据删除行为。
ORM 是 Object-Relational Mapping(对象关系映射) 的缩写,它是一种编程技术,用于在面向对象编程语言中将数据库表与程序中的对象自动映射,从而避免直接编写繁琐的 SQL 语句。
对象与表的映射
User
类对应 users
表)。User(name="Alice")
对应表中一行记录)。user.name
对应 users.name
列)。操作抽象化
user.save()
)自动生成并执行底层 SQL(如 INSERT INTO users...
),开发者无需手动拼接 SQL。语言 | ORM 框架 |
---|---|
Python | Django ORM, SQLAlchemy |
Java | Hibernate, MyBatis |
JavaScript | Sequelize, TypeORM |
PHP | Eloquent (Laravel) |
优点
缺点
from sqlalchemy import create_engine, Column, String
from sqlalchemy.ext.declarative import declarative_base
# 1. 定义表结构(通过类)
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
# 2. 创建数据库连接
engine = create_engine('sqlite:///mydb.db')
Base.metadata.create_all(engine)
# 3. 操作数据库(无需写 SQL)
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()
# 插入数据(Create)
new_user = User(name="Alice")
session.add(new_user)
session.commit()
# 查询数据(Read)
users = session.query(User).filter_by(name="Alice").all()
raw SQL
支持)。