青训营笔记6 | 豆包marscode AI刷题 之 实战笔记+图片末尾【GORM】

50 阅读34分钟

GORM学习和实战

官方文档如下

GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.


1.特性

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 PreloadJoins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • Auto Migration 迁移
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

快速入门1 (但是sqlite版本,这是一个嵌入式的)

package main

import (
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
)

type Product struct {
  gorm.Model
  Code  string
  Price uint
}

func main() {
  db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
  if err != nil {
    panic("failed to connect database")
  }

  // 迁移 schema
  db.AutoMigrate(&Product{})

  // 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)
}
  • SQLite:是一种嵌入式的数据库引擎,它的数据库本质上是一个文件。这意味着整个数据库存储在一个本地文件中,应用程序可以通过库文件直接访问这个数据库文件,不需要单独的数据库服务器进程。例如,在一个小型的移动应用或者本地的命令行工具中,SQLite 可以很方便地存储和管理数据。

  • SQLite

    • 适合用于移动应用开发,作为本地数据存储,如安卓和 iOS 应用中的离线数据存储。
    • 小型桌面应用,用于存储用户配置、本地缓存数据等。
    • 简单的命令行工具或者脚本语言中的数据管理,用于临时存储和处理数据。

快速入门2:mysql版本

package main

import (
    "fmt"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// UserInfo 用户信息
type UserInfo struct {
    ID uint
    Name string
    Gender string
    Hobby string
}


func main() {
    db, err := gorm.Open("mysql", "root:wxe13867187633@(127.0.0.1:3306)/gromtest?charset=utf8mb4&parseTime=True&loc=Local")
    if err!= nil{
        panic(err)
    }
    defer db.Close()

    fmt.Println("mysql已链接")

    // 自动迁移
    db.AutoMigrate(&UserInfo{})

    u1 := UserInfo{1, "七米", "男", "篮球"}
    u2 := UserInfo{2, "沙河娜扎", "女", "足球"}
    // 创建记录
    db.Create(&u1)
    db.Create(&u2)
    // 查询
    var u = new(UserInfo)
    db.First(u)
    fmt.Printf("%#v\n", u)

    var uu UserInfo
    db.Find(&uu, "hobby=?", "足球")
    fmt.Printf("%#v\n", uu)

    // 更新
    db.Model(&u).Update("hobby", "双色球")
    // 删除
    db.Delete(&u)
}
  • root:xxxxx:这是数据库的用户名和密码,格式为用户名:密码

  • (127.0.0.1:3306):这是数据库服务器的地址和端口。127.0.0.1是本地主机地址(也就是本机),3306是 MySQL 数据库默认的服务端口。

  • /gromtest:指定了要连接的数据库名称是gromtest

  • ?charset = utf8mb4&parseTime=True&loc = Local:这是连接数据库的一些额外参数。

    • charset = utf8mb4:表示使用utf8mb4字符集,这种字符集可以更好地支持各种语言的字符,包括 emoji 等特殊字符。
    • parseTime=True:用于正确解析数据库中的日期和时间类型。
    • loc = Local:用于设置时区相关的参数,确保时间处理的准确性。

gorm库通过后续的操作(如AutoMigrateCreate等)来指定和操作表。例如,如果你有一个User结构体,通过db.AutoMigrate(&User{})会在gromtest数据库中创建与User结构体对应的表。

运行结果:

PS D:\GOfiles\Gormlearn> go run main.go
mysql已链接
&main.UserInfo{ID:0x1, Name:"七米", Gender:"男", Hobby:"篮球"}
main.UserInfo{ID:0x2, Name:"沙河娜扎", Gender:"女", Hobby:"足球"}

转存失败,建议直接上传图片文件


2.GORM的model模型

约定

主键:GORM 使用一个名为ID 的字段作为每个模型的默认主键。

type User struct {
  ID   string // 名为`ID`的字段会默认作为表的主键
  Name string
}

// 使用`AnimalID`作为主键
type Animal struct {
  AnimalID int64 `gorm:"primary_key"`
  Name     string
  Age      int64
}

表名:默认情况下,GORM 将结构体名称转换为 snake_case 并为表名加上复数形式。 例如,一个 User 结构体在数据库中的表名变为 users 。

type User struct {}


//User 的表名设置为 `profiles`
func (User) TableName() string {
    return "profiles"
}

func (u User) TableName() string{
    if u.Role == "admin"{
        return "admin_users"
    } else{
        return "users"
    }
}

// 禁用默认表名的复数形式,如果置为 true,则 `User` 的默认表名是 `user`
db.SingularTable(true)


// 也可以通过Table()指定表名:

// 使用User结构体创建名为`deleted_users`的表
db.Table("deleted_users").CreateTable(&User{})

var deleted_users []User
db.Table("deleted_users").Find(&deleted_users)
//// SELECT * FROM deleted_users;

db.Table("deleted_users").Where("name = ?", "jinzhu").Delete()
//// DELETE FROM deleted_users WHERE name = 'jinzhu';

列名:GORM 自动将结构体字段名称转换为 snake_case 作为数据库中的列名。

列名由字段名称进行下划线分割来生成

type User struct {
  ID        uint      // column name is `id`
  Name      string    // column name is `name`
  Birthday  time.Time // column name is `birthday`
  CreatedAt time.Time // column name is `created_at`
}

可以使用结构体tag指定列名:

type Animal struct {
  AnimalId    int64     `gorm:"column:beast_id"`         // set column name to `beast_id`
  Birthday    time.Time `gorm:"column:day_of_the_beast"` // set column name to `day_of_the_beast`
  Age         int64     `gorm:"column:age_of_the_beast"` // set column name to `age_of_the_beast`
}

时间戳字段:GORM使用字段 CreatedAt 和 UpdatedAt 来自动跟踪记录的创建和更新时间。

模型是使用普通结构体定义的。 这些结构体可以包含具有基本Go类型、指针或这些类型的别名,甚至是自定义类型(只需要实现 database/sql 包中的ScannerValuer接口)。

考虑以下 user 模型的示例:

type User struct {  
 ID uint // Standard field for the primary key  
 Name string // A regular string field  
 Email *string // A pointer to a string, allowing for null values  
 Age uint8 // An unsigned 8-bit integer  
 Birthday *time.Time // A pointer to time.Time, can be null  
 MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings  
 ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields  
 CreatedAt time.Time // Automatically managed by GORM for creation time  
 UpdatedAt time.Time // Automatically managed by GORM for update time  
 ignored string // fields that aren't exported are ignored  
}

gorm.Model是grom提供的模型

GORM提供了一个预定义的结构体,名为gorm.Model,其中包含常用字段:

gorm.Model 的定义

type Model struct {  
 ID uint `gorm:"primaryKey"`  
 CreatedAt time.Time  
 UpdatedAt time.Time  
 DeletedAt gorm.DeletedAt `gorm:"index"`  
}

其嵌入在您的结构体中: 您可以直接在您的结构体中嵌入 gorm.Model ,以便自动包含这些字段。

示范如下:

// 将 `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`字段注入到`User`模型中
type User struct {
  gorm.Model
  Name string
}

3.高级选项

备注方式达到效果

type User struct {
  Name string `gorm:"<-:create"` // 允许读和创建
  Name string `gorm:"<-:update"` // 允许读和更新
  Name string `gorm:"<-"`        // 允许读和写(创建和更新)
  Name string `gorm:"<-:false"`  // 允许读,禁止写
  Name string `gorm:"->"`        // 只读(除非有自定义配置,否则禁止写)
  Name string `gorm:"->;<-:create"` // 允许读和写
  Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读)
  Name string `gorm:"-"`  // 通过 struct 读写会忽略该字段 !注意: 使用 GORM Migrator 创建表时,不会创建被忽略的字段
  Name string `gorm:"-:all"`        // 通过 struct 读写、迁移会忽略该字段
  Name string `gorm:"-:migration"`  // 通过 struct 迁移会忽略该字段
}

创建/更新时间追踪(纳秒、毫秒、秒、Time)

GORM 约定使用 CreatedAtUpdatedAt 追踪创建/更新时间。如果您定义了这种字段,GORM 在创建、更新时会自动填充 当前时间

要使用不同名称的字段,您可以配置 autoCreateTimeautoUpdateTime 标签。

如果您想要保存 UNIX(毫/纳)秒时间戳,而不是 time,您只需简单地将 time.Time 修改为 int 即可

type User struct {
  CreatedAt time.Time // 在创建时,如果该字段值为零值,则使用当前时间填充
  UpdatedAt int       // 在创建时该字段值为零值或者在更新时,使用当前时间戳秒数填充
  Updated   int64 `gorm:"autoUpdateTime:nano"` // 使用时间戳纳秒数填充更新时间
  Updated   int64 `gorm:"autoUpdateTime:milli"` // 使用时间戳毫秒数填充更新时间
  Created   int64 `gorm:"autoCreateTime"`      // 使用时间戳秒数填充创建时间
}
标签名说明
column指定 db 列名
type列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsizeautoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime
size定义列数据类型的大小或长度,例如 size: 256
primaryKey将列定义为主键
unique将列定义为唯一键
default定义列的默认值
precision指定列的精度
scale指定列大小
not null指定列为 NOT NULL
autoIncrement指定列为自动增长
autoIncrementIncrement自动步长,控制连续记录之间的间隔
embedded嵌套字段
embeddedPrefix嵌入字段的列名前缀
autoCreateTime创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndex与 index 相同,但创建的是唯一索引
check创建检查约束,例如 check:age > 13,查看 约束 获取详情
<-设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
->设置字段读的权限,->:false 无读权限
-忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限
comment迁移时为字段添加注释

4.连接数据库

官网文档有多种 这里只是mysql

自定义驱动

GORM 允许通过 DriverName 选项自定义 MySQL 驱动,例如config配置:

import (
  _ "example.com/my_mysql_driver"
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

db, err := gorm.Open(mysql.New(mysql.Config{
  DriverName: "my_mysql_driver",
  DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local", // data source name, 详情参考:https://github.com/go-sql-driver/mysql#dsn-data-source-name
}), &gorm.Config{})

现有的数据库连接

GORM 允许通过一个现有的数据库连接来初始化 *gorm.D

import (
  "database/sql"
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

sqlDB, err := sql.Open("mysql", "mydb_dsn")
gormDB, err := gorm.Open(mysql.New(mysql.Config{
  Conn: sqlDB,
}), &gorm.Config{})

5.CRUD之创建

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}

result := db.Create(&user) // 通过数据的指针来创建


user.ID             // 返回插入数据的主键
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数

注意!!!你无法向 ‘create’ 传递结构体,所以你应该传入数据的指针

我们还可以使用 Create() 创建多项记录:

users := []*User{
    {Name: "Jinzhu", Age: 18, Birthday: time.Now()},
    {Name: "Jackson", Age: 19, Birthday: time.Now()},
}

result := db.Create(users) // pass a slice to insert multiple row

批量插入

要高效地插入大量记录,请将切片传递给Create方法。 GORM 将生成一条 SQL 来插入所有数据,以返回所有主键值,并触发 Hook 方法。 当这些记录可以被分割成多个批次时,GORM会开启一个事务</0>来处理它们。

var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}  
db.Create(&users)

for _, user := range users {  
 user.ID // 1,2,3  
}

你可以通过db.CreateInBatches方法来指定批量插入的批次大小

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  CreateBatchSize: 1000,
})

db := db.Session(&gorm.Session{CreateBatchSize: 1000})

users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}

db.Create(&users)
// INSERT INTO users xxx (5 batches)
// INSERT INTO pets xxx (15 batches)

创建钩子

GORM允许用户通过实现这些接口 BeforeSave, BeforeCreate, AfterSave, AfterCreate来自定义钩子。 这些钩子方法会在创建一条记录时被调用,关于钩子的生命周期请参阅Hooks。

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New()

    if u.Role == "admin" {
        return errors.New("invalid role")
    }
    return
}

创建时可用的 hook

// 开始事务  
BeforeSave  
BeforeCreate  
// 关联前的 save  
// 插入记录至 db  
// 关联后的 save  
AfterCreate  
AfterSave  
// 提交或回滚事务

具体代码示例:

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New()

  if !u.IsValid() {
    err = errors.New("can't save invalid data")
  }
  return
}

func (u *User) AfterCreate(tx *gorm.DB) (err error) {
  if u.ID == 1 {
    tx.Model(u).Update("role", "admin")
  }
  return
}

注意 在 GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。

详见Hook | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

跳过钩子hook

如果你想跳过Hooks方法,可以使用SkipHooks会话模式,例子如下

DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)

DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)

DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)

根据map创建

GORM支持通过 map[string]interface{} 与 []map[string]interface{}{}来创建记录。

db.Model(&User{}).Create(map[string]interface{}{
  "Name": "jinzhu", "Age": 18,
})

// batch insert from `[]map[string]interface{}{}`
db.Model(&User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
})

注意 当使用map来创建时,钩子方法不会执行,关联不会被保存且不会回写主键。

默认值

你可以通过结构体Tag default来定义字段的默认值,示例如下:

type User struct {
  ID   int64
  Name string `gorm:"default:galeone"`
  Age  int64  `gorm:"default:18"`
}

这些默认值会被当作结构体字段的零值插入到数据库中

注意,当结构体的字段默认值是零值的时候比如 0''false,这些字段值将不会被保存到数据库中,你可以使用指针类型或者Scanner/Valuer来避免这种情况。如下:

type User struct {
  gorm.Model
  Name string
  Age  *int           `gorm:"default:18"`
  Active sql.NullBool `gorm:"default:true"`
}

或如:

// 使用 Scanner/Valuer
type User struct {
    ID int64
    Name sql.NullString `gorm:"default:'小王子'"` // sql.NullString 实现了Scanner/Valuer接口
    Age  int64
}
user := User{Name: sql.NullString{"", true}, Age:18}
db.Create(&user)  // 此时数据库中该条记录name字段的值就是''

注意,若要让字段在数据库中拥有默认值则必须使用defaultTag来为结构体字段设置默认值。如果想要在数据库迁移的时候跳过默认值,可以使用 default:(-),示例如下:

type User struct {
  ID        string `gorm:"default:uuid_generate_v3()"` // db func
  FirstName string
  LastName  string
  Age       uint8
  FullName  string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname,' ',lastname));default:(-);"`
}

6.CRUD之查询

一般查询

1. 根据主键查询第一条记录

db.First(&user)
//SELECT * FROM users ORDER BY id LIMIT 1;

2. 随机获取一条记录,没有指定排序字段

db.Take(&user)
// SELECT * FROM users LIMIT 1;

3.获取最后一条记录

db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

4.查询所有记录

db.Find(&user)
// SELECT * FROM users;

5.其他

result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil

// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)

根据主键检索

如果主键是数字类型,您可以使用 内联条件 来检索对象。 当使用字符串时,需要额外的注意来避免SQL注入;查看 Security 部分来了解详情。

db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;

db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;

db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);

主键是字符串,例如,查询如下:

db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";

当目标对象有一个主键值时,将使用主键构建查询条件,例如:

var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;

var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;

根据条件检索

(1)string 条件 //即where条件
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE模糊查询
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00'
(2)Struct & Map查询
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// 主键的切片
db.Where([]int64{20, 21, 22}).Find(&users)
//// SELECT * FROM users WHERE id IN (20, 21, 22);

注意 当通过结构体进行查询时,GORM将会只通过非零值字段查询,这意味着如果你的字段值为0''false或者其他零值时,将不会被用于构建查询条件,例如:

db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
//// SELECT * FROM users WHERE name = "jinzhu";

你可以使用指针或实现 Scanner/Valuer 接口来避免这个问题.


7.CRUD之更新

更新所有字段

db.First(&user)

user.Name = "七米"
user.Age = 99
db.Save(&user)

////  UPDATE `users` SET `created_at` = '2020-02-16 12:52:20', `updated_at` = '2020-02-16 12:54:55', `deleted_at` = NULL, `name` = '七米', `age` = 99, `active` = true  WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = 1

更新修改字段

如果你只希望更新指定字段,可以使用Update或者Updates

// 更新单个属性,如果它有变化
db.Model(&user).Update("name", "hello")
//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// 根据给定的条件更新单个属性
db.Model(&user).Where("active = ?", true).Update("name", "hello")
//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;

// 使用 map 更新多个属性,只会更新其中有变化的属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
//// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

// 使用 struct 更新多个属性,只会更新其中有变化且为非零值的字段
db.Model(&user).Updates(User{Name: "hello", Age: 18})
//// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

// 警告:当使用 struct 更新时,GORM只会更新那些非零值的字段
// 对于下面的操作,不会发生任何更新,"", 0, false 都是其类型的零值
db.Model(&user).Updates(User{Name: "", Age: 0, Active: false})

更新选定字段

如果你想更新或忽略某些字段,你可以使用 SelectOmit

db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
//// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
//// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

无Hooks更新

上面的更新操作会自动运行 model 的 BeforeUpdateAfterUpdate 方法,更新 UpdatedAt 时间戳, 在更新时保存其 Associations, 如果你不想调用这些方法,你可以使用 UpdateColumn, UpdateColumns

// 更新单个属性,类似于 `Update`
db.Model(&user).UpdateColumn("name", "hello")
//// UPDATE users SET name='hello' WHERE id = 111;

// 更新多个属性,类似于 `Updates`
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
//// UPDATE users SET name='hello', age=18 WHERE id = 111;

批量更新

批量更新时Hooks(钩子函数)不会运行。

db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
//// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);

// 使用 struct 更新时,只会更新非零值字段,若想更新所有字段,请使用map[string]interface{}
db.Model(User{}).Updates(User{Name: "hello", Age: 18})
//// UPDATE users SET name='hello', age=18;

// 使用 `RowsAffected` 获取更新记录总数
db.Model(User{}).Updates(User{Name: "hello", Age: 18}).RowsAffected

使用SQL表达式更新

先查询表中的第一条数据保存至user变量。

var user User
db.First(&user)
db.Model(&user).Update("age", gorm.Expr("age * ? + ?", 2, 100))
//// UPDATE `users` SET `age` = age * 2 + 100, `updated_at` = '2020-02-16 13:10:20'  WHERE `users`.`id` = 1;

db.Model(&user).Updates(map[string]interface{}{"age": gorm.Expr("age * ? + ?", 2, 100)})
//// UPDATE "users" SET "age" = age * '2' + '100', "updated_at" = '2020-02-16 13:05:51' WHERE `users`.`id` = 1;

db.Model(&user).UpdateColumn("age", gorm.Expr("age - ?", 1))
//// UPDATE "users" SET "age" = age - 1 WHERE "id" = '1';

db.Model(&user).Where("age > 10").UpdateColumn("age", gorm.Expr("age - ?", 1))
//// UPDATE "users" SET "age" = age - 1 WHERE "id" = '1' AND quantity > 10;

8.CRUD之删除

删除记录

警告 删除记录时,请确保主键字段有值,GORM 会通过主键去删除记录,如果主键为空,GORM 会删除该 model 的所有记录。

// 删除现有记录
db.Delete(&email)
//// DELETE from emails where id=10;

// 为删除 SQL 添加额外的 SQL 操作
db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email)
//// DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN);

批量删除

删除全部匹配的记录

db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
//// DELETE from emails where email LIKE "%jinzhu%";

db.Delete(Email{}, "email LIKE ?", "%jinzhu%")
//// DELETE from emails where email LIKE "%jinzhu%";

软删除

如果一个 model 有 DeletedAt 字段,他将自动获得软删除的功能! 当调用 Delete 方法时, 记录不会真正的从数据库中被删除, 只会将DeletedAt 字段的值会被设置为当前时间

db.Delete(&user)
//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;

// 批量删除
db.Where("age = ?", 20).Delete(&User{})
//// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;

// 查询记录时会忽略被软删除的记录
db.Where("age = 20").Find(&user)
//// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;

// Unscoped 方法可以查询被软删除的记录
db.Unscoped().Where("age = 20").Find(&users)
//// SELECT * FROM users WHERE age = 20;

物理删除

// Unscoped 方法可以物理删除记录
db.Unscoped().Delete(&order)
//// DELETE FROM orders WHERE id=10;

9.开启博客之路

快速进入状态

go mod init 是Gromlearn

package main

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

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

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

    err := r.Run(":9000")
    if err != nil {
        panic("route start defeat!")
    }
}

转存失败,建议直接上传图片文件

转存失败,建议直接上传图片文件

我们可以看到启动了服务后,输出了许多运行信息,如果你第一次看,可能会有些懵,在这里我们对运行信息做一个初步的概括分析,分为以下四大块:

  • 默认 Engine 实例:当前默认使用了官方所提供的 Logger 和 Recovery 中间件创建了 Engine 实例。
  • 运行模式:当前为调试模式,并建议若在生产环境时切换为发布模式。
  • 路由注册:注册了 GET /ping 的路由,并输出其调用方法的方法名。
  • 运行信息:本次启动时监听 9000端口。

gin一点源码分析

对源码进行大体分析,一探究竟,简单的解剖一下里面的秘密,整体分析流程如下:

转存失败,建议直接上传图片文件

gin.Default()创建默认Engine实例,引入两个中间件logger和recovery。

gin.New 该方法会进行Engine实例初始化动作并返回

func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine,
        UseRawPath:             false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJsonPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine
    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

RouterGroup:路由组

RedirectTrailingSlash:是否自动重定向

RedirectFixedPath:是否尝试修复当前请求路径

HandleMethodNotAllowed:判断当前路由是否允许调用其他方法

ForwardedByClientIP:如果开启,则尽可能的返回真实的客户端 IP,先从 X-Forwarded-For 取值,如果没有再从 X-Real-Ip。

UseRawPath:如果开启,则会使用 url.RawPath 来获取请求参数,不开启则还是按 url.Path 去获取。

UnescapePathValues:是否对路径值进行转义

MaxMultipartMemory:相对应 http.Request ParseMultipartForm 方法,用于控制最大的文件上传大小。

trees:多个压缩字典树(Radix Tree),每个树都对应着一种 HTTP Method。

delims:用于 HTML 模板的左右定界符。

r.GET()

其注册的具体内容

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)
    handlers = group.combineHandlers(handlers)
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

1.计算路由的绝对路径

2.合并现有和新注册的 Handler,并创建一个函数链 HandlersChain。

3.追加:将当前注册的路由规则(含 HTTP Method、Path、Handlers)追加到对应的树中。

r.Run()

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}

该方法会通过解析地址,再调用 http.ListenAndServe 将 Engine 实例作为 Handler 注册进去,然后启动服务,开始对外提供 HTTP 服务。

在 gin 中,Engine 这一个结构体确确实实是实现了 ServeHTTP 方法的,也就是符合 http.Handler 接口标准,代码如下:

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)
    engine.pool.Put(c)
}
  • 从 sync.Pool 对象池中获取一个上下文对象。
  • 重新初始化取出来的上下文对象。
  • 处理外部的 HTTP 请求。
  • 处理完毕,将取出的上下文对象返回给对象池。

10.进行项目设计

项目结构

先将项目的标准目录结构创建起来,便于后续的开发,最终目录结构如下:

blog-service
├── configs
├── docs
├── global
├── internal
│   ├── dao
│   ├── middleware
│   ├── model
│   ├── routers
│   └── service
├── pkg
├── storage
├── scripts
└── third_party
  • configs:配置文件。

  • docs:文档集合。

  • global:全局变量。

  • internal:内部模块。

    • dao:数据访问层(Database Access Object),所有与数据相关的操作都会在 dao 层进行,例如 MySQL、ElasticSearch 等。
    • middleware:HTTP 中间件。
    • model:模型层,用于存放 model 对象。
    • routers:路由相关逻辑处理。
    • service:项目核心业务逻辑。
  • pkg:项目相关的模块包。

  • storage:项目生成的临时文件。

  • scripts:各类构建,安装,分析等操作的脚本。

  • third_party:第三方的资源工具,例如 Swagger UI。

创建数据库

CREATE DATABASE
IF
    NOT EXISTS blog_service DEFAULT CHARACTER 
    SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;

通过上述 SQL 语句,如果名为 blog_service 的数据库不存在,那么就在 MySQL 中创建它,并将其默认字符编码设置为 utf8mb4,默认排序规则设置为 utf8mb4_general_ci

注解

  • DEFAULT CHARACTER SET utf8mb4:这部分是在设置数据库的默认字符编码。utf8mb4 是一种 Unicode 字符编码格式,它是 utf8 的超集,能够支持更多的 Unicode 字符,包括一些特殊字符和 emoji 表情等。通过设置为 utf8mb4 作为默认字符编码,可以确保在这个数据库中存储的文本数据(如文章内容、用户评论等)能够正确地处理各种字符,减少因字符编码不匹配而导致的乱码等问题。

  • DEFAULT COLLATE utf8mb4_general_ci:这里是在设置数据库的默认排序规则。COLLATE 用于指定字符数据的排序方式。utf8mb4_general_ci 是一种比较常用的 utf8mb4 字符编码下的排序规则,其中 ci 表示不区分大小写(Case Insensitive)。也就是说,在对数据库中的字符数据进行排序操作时(比如在执行 ORDER BY 语句时),会按照不区分大小写的方式进行排序,这在很多实际应用场景中是比较方便的,例如在查询用户列表时,无论用户输入的是大写还是小写的用户名,都能按照统一的规则进行排序和展示。

  • 在数据库相关的 SQL 语句中,“default character”(完整的是 “default character set”)是用于指定数据库、表或者列的默认字符集。

    以 “CREATE DATABASE blog_service DEFAULT CHARACTER SET utf8mb4” 为例,“default character set” 部分传达了要为数据库 “blog_service” 定义默认字符集的意图,而 “set” 关键字引导出具体的字符集(utf8mb4),这个字符集将被应用作为该数据库的默认字符编码方式。这就好像是一个指令的两个部分,前面部分说明要做什么(设置默认字符集),后面部分通过 “set” 关键字具体执行这个操作(指定 utf8mb4 为字符集)。

创建成功

创建表

所有表都会包含如下公共字段:

  `created_on` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
  `created_by` varchar(100) DEFAULT '' COMMENT '创建人',
  `modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
  `modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
  `deleted_on` int(10) unsigned DEFAULT '0' COMMENT '删除时间',
  `is_del` tinyint(3) unsigned DEFAULT '0' COMMENT '是否删除 0 为未删除、1 为已删除',
(1)创建标签表

创建标签表,表字段主要为标签的名称、状态以及公共字段。

CREATE TABLE `blog_tag` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT '' COMMENT '标签名称',
  # 此处请写入公共字段
  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0 为禁用、1 为启用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签管理';

井号处写公共字段,执行如下

转存失败,建议直接上传图片文件

ENGINE=InnoDB

  • ENGINE是指存储引擎。存储引擎是数据库管理系统用于存储、处理和检索数据的底层软件组件。MySQL 支持多种存储引擎,如MyISAMInnoDB等。
  • InnoDB是目前 MySQL 中最常用的存储引擎之一。它支持事务处理(ACID 特性),具有行级锁定、外键约束等高级功能,适合用于处理大量的读写操作,尤其是在涉及事务操作的应用场景(如电商系统中的订单处理、金融系统中的交易记录等)中表现出色。

DEFAULT CHARSET=utf8mb4

  • 这部分与前面提到的设置字符集相关内容类似。DEFAULT CHARSET表示设置默认的字符编码。
  • utf8mb4是指定的字符集,它是一种广泛使用的 Unicode 字符编码格式,能够处理包括各种语言文字以及 emoji 表情等多种字符形式,确保在表中存储的文本数据可以正确地进行编码和解码,避免出现字符乱码等问题。

COMMENT关键字用于为表添加注释。注释内容在 SQL 语句中主要起到说明的作用,对数据库的功能和用途没有直接的影响。

(2)创建文章表

创建文章表,表字段主要为文章的标题、封面图、内容概述以及公共字段。

CREATE TABLE `blog_article` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(100) DEFAULT '' COMMENT '文章标题',
  `desc` varchar(255) DEFAULT '' COMMENT '文章简述',
  `cover_image_url` varchar(255) DEFAULT '' COMMENT '封面图片地址',
  `content` longtext COMMENT '文章内容',
  # 此处请写入公共字段
  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0 为禁用、1 为启用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章管理';

转存失败,建议直接上传图片文件

(3)创建文章标签关联表

创建文章标签关联表,这个表主要用于记录文章和标签之间的 1:N 的关联关系。

CREATE TABLE `blog_article_tag` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `article_id` int(11) NOT NULL COMMENT '文章 ID',
  `tag_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '标签 ID',
  # 此处请写入公共字段
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联';

建成如下

转存失败,建议直接上传图片文件

创建model

(1)创建公共model

在 internal/model 目录下创建 model.go 文件,写入如下代码:

type Model struct {
    ID         uint32 `gorm:"primary_key" json:"id"`
    CreatedBy  string `json:"created_by"`
    ModifiedBy string `json:"modified_by"`
    CreatedOn  uint32 `json:"created_on"`
    ModifiedOn uint32 `json:"modified_on"`
    DeletedOn  uint32 `json:"deleted_on"`
    IsDel      uint8  `json:"is_del"`
}
(2)创建标签model

在 internal/model 目录下创建 article.go 文件,写入如下代码:

type Tag struct {
    *Model
    Name  string `json:"name"`
    State uint8  `json:"state"`
}

func (t Tag) TableName() string {
    return "blog_tag"
}
(3)创建文章标签 model

在 internal/model 目录下创建 article_tag.go 文件,写入如下代码:

type ArticleTag struct {
    *Model
    TagID     uint32 `json:"tag_id"`
    ArticleID uint32 `json:"article_id"`
}

func (a ArticleTag) TableName() string {
    return "blog_article_tag"
}

转存失败,建议直接上传图片文件


路由

在完成数据库的设计后,我们需要对业务模块的管理接口进行设计,而在这一块最核心的就是增删改查的 RESTful API 设计和编写,在 RESTful API 中 HTTP 方法对应的行为动作分别如下:

  • GET:读取/检索动作。
  • POST:新增/新建动作。
  • PUT:更新动作,用于更新一个完整的资源,要求为幂等。
  • PATCH:更新动作,用于更新某一个资源的一个组成部分,也就是只需要更新该资源的某一项,就应该使用 PATCH 而不是 PUT,可以不幂等。
  • DELETE:删除动作。
(1)标签管理
功能HTTP 方法路径
新增标签POST/tags
删除指定标签DELETE/tags/:id
更新指定标签PUT/tags/:id
获取标签列表GET/tags
(2)文章管理
功能HTTP 方法路径
新增文章POST/articles
删除指定文章DELETE/articles/:id
更新指定文章PUT/articles/:id
获取指定文章GET/articles/:id
获取文章列表GET/articles
(3)路由管理

在确定了业务接口设计后,需要对业务接口进行一个基础编码,确定其方法原型,把当前工作区切换到项目目录的 internal/routers 下,并新建 router.go 文件,写入代码:

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

    apiv1 := r.Group("/api/v1")
    {
        apiv1.POST("/tags")
        apiv1.DELETE("/tags/:id")
        apiv1.PUT("/tags/:id")
        apiv1.PATCH("/tags/:id/state")
        apiv1.GET("/tags")

        apiv1.POST("/articles")
        apiv1.DELETE("/articles/:id")
        apiv1.PUT("/articles/:id")
        apiv1.PATCH("/articles/:id/state")
        apiv1.GET("/articles/:id")
        apiv1.GET("/articles")
    }

    return r
}

如图:

转存失败,建议直接上传图片文件


处理程序

接下来编写对应路由的处理方法,我们在项目目录下新建 internal/routers/api/v1 文件夹,并新建 tag.go(标签)和 article.go(文章)文件,写入代码下述代码。

(1)tag.go 文件
type Tag struct {}

func NewTag() Tag {
    return Tag{}
}

func (t Tag) Get(c *gin.Context) {}
func (t Tag) List(c *gin.Context) {}
func (t Tag) Create(c *gin.Context) {}
func (t Tag) Update(c *gin.Context) {}
func (t Tag) Delete(c *gin.Context) {}
(2)article.go 文件
type Article struct{}

func NewArticle() Article {
    return Article{}
}

func (a Article) Get(c *gin.Context) {}
func (a Article) List(c *gin.Context) {}
func (a Article) Create(c *gin.Context) {}
func (a Article) Update(c *gin.Context) {}
func (a Article) Delete(c *gin.Context) {}
(3) 路由管理

在编写好路由的 Handler 方法后,我们只需要将其注册到对应的路由规则上就好了,打开项目目录下 internal/routers 的 router.go 文件,修改如下:

  ...
  article := v1.NewArticle()
  tag := v1.NewTag()
  apiv1 := r.Group("/api/v1")
  {
      apiv1.POST("/tags", tag.Create)
      apiv1.DELETE("/tags/:id", tag.Delete)
      apiv1.PUT("/tags/:id", tag.Update)
      apiv1.PATCH("/tags/:id/state", tag.Update)
      apiv1.GET("/tags", tag.List)

      apiv1.POST("/articles", article.Create)
      apiv1.DELETE("/articles/:id", article.Delete)
      apiv1.PUT("/articles/:id", article.Update)
      apiv1.PATCH("/articles/:id/state", article.Update)
      apiv1.GET("/articles/:id", article.Get)
      apiv1.GET("/articles", article.List)
  }

启动接入

在完成了模型、路由的代码编写后,我们修改前面章节所编写的 main.go 文件,把它改造为这个项目的启动文件,修改代码如下:

func main() {
    router := routers.NewRouter()
    s := &http.Server{
        Addr:           ":9000",
        Handler:        router,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe()
}

我们通过自定义 http.Server,设置了监听的 TCP Endpoint、处理的程序、允许读取/写入的最大时间、请求头的最大字节数等基础参数,最后调用 ListenAndServe 方法开始监听。

转存失败,建议直接上传图片文件

运行

转存失败,建议直接上传图片文件


11.编写公共组件

转存失败,建议直接上传图片文件

错误码标准化

(1)公共错误码

我们需要在在项目目录下的 pkg/errcode 目录新建 common_code.go 文件,用于预定义项目中的一些公共错误码,便于引导和规范大家的使用,如下:

var (
    Success                   = NewError(0, "成功")
    ServerError               = NewError(10000000, "服务内部错误")
    InvalidParams             = NewError(10000001, "入参错误")
    NotFound                  = NewError(10000002, "找不到")
    UnauthorizedAuthNotExist  = NewError(10000003, "鉴权失败,找不到对应的 AppKey 和 AppSecret")
    UnauthorizedTokenError    = NewError(10000004, "鉴权失败,Token 错误")
    UnauthorizedTokenTimeout  = NewError(10000005, "鉴权失败,Token 超时")
    UnauthorizedTokenGenerate = NewError(10000006, "鉴权失败,Token 生成失败")
    TooManyRequests           = NewError(10000007, "请求过多")
)
(2)错误处理

接下来我们在项目目录下的 pkg/errcode 目录新建 errcode.go 文件,编写常用的一些错误处理公共方法,标准化我们的错误输出,如下:

type Error struct {
    code int `json:"code"`
    msg string `json:"msg"`
    details []string `json:"details"`
}

var codes = map[int]string{}

func NewError(code int, msg string) *Error {
    if _, ok := codes[code]; ok {
        panic(fmt.Sprintf("错误码 %d 已经存在,请更换一个", code))
    }
    codes[code] = msg
    return &Error{code: code, msg: msg}
}

func (e *Error) Error() string {
    return fmt.Sprintf("错误码:%d, 错误信息:%s", e.Code(), e.Msg())
}

func (e *Error) Code() int {
    return e.code
}

func (e *Error) Msg() string {
    return e.msg
}

func (e *Error) Msgf(args []interface{}) string {
    return fmt.Sprintf(e.msg, args...)
}

func (e *Error) Details() []string {
    return e.details
}

func (e *Error) WithDetails(details ...string) *Error {
    newError := *e
    newError.details = []string{}
    for _, d := range details {
        newError.details = append(newError.details, d)
    }

    return &newError
}

func (e *Error) StatusCode() int {
    switch e.Code() {
    case Success.Code():
        return http.StatusOK
    case ServerError.Code():
        return http.StatusInternalServerError
    case InvalidParams.Code():
        return http.StatusBadRequest
    case UnauthorizedAuthNotExist.Code():
        fallthrough
    case UnauthorizedTokenError.Code():
        fallthrough
    case UnauthorizedTokenGenerate.Code():
        fallthrough
    case UnauthorizedTokenTimeout.Code():
        return http.StatusUnauthorized
    case TooManyRequests.Code():
        return http.StatusTooManyRequests
    }

    return http.StatusInternalServerError
}

在错误码方法的编写中,我们声明了 Error 结构体用于表示错误的响应结果,并利用 codes 作为全局错误码的存储载体,便于查看当前注册情况,并在调用 NewError 创建新的 Error 实例的同时进行排重的校验。

另外相对特殊的是 StatusCode 方法,它主要用于针对一些特定错误码进行状态码的转换,因为不同的内部错误码在 HTTP 状态码中都代表着不同的意义,我们需要将其区分开来,便于客户端以及监控/报警等系统的识别和监听。


配置管理

(1)安装viper

为了完成文件配置的读取,我们需要借助第三方开源库 viper,在项目根目录下执行以下安装命令:

$ go get -u github.com/spf13/viper@v1.4.0

Viper 是适用于 Go 应用程序的完整配置解决方案,是目前 Go 语言中比较流行的文件配置解决方案,它支持处理各种不同类型的配置需求和配置格式。

(2)配置

在项目目录下的 configs 目录新建 config.yaml 文件,写入以下配置:

Server:
  RunMode: debug
  HttpPort: 8000
  ReadTimeout: 60
  WriteTimeout: 60
App:
  DefaultPageSize: 10
  MaxPageSize: 100
  LogSavePath: storage/logs
  LogFileName: app
  LogFileExt: .log
Database:
  DBType: mysql
  Username: root  # 填写你的数据库账号
  Password: rootroot  # 填写你的数据库密码
  Host: 127.0.0.1:3306
  DBName: blog_service
  TablePrefix: blog_
  Charset: utf8
  ParseTime: True
  MaxIdleConns: 10
  MaxOpenConns: 30

在配置文件中,我们分别针对如下内容进行了默认配置:

  • Server:服务配置,设置 gin 的运行模式、默认的 HTTP 监听端口、允许读取和写入的最大持续时间。
  • App:应用配置,设置默认每页数量、所允许的最大每页数量以及默认的应用日志存储路径。
  • Database:数据库配置,主要是连接实例所必需的基础参数。
(3)编写组件

在完成了配置文件的确定和编写后,我们需要针对读取配置的行为进行封装,便于应用程序的使用,我们在项目目录下的 pkg/setting 目录下新建 setting.go 文件,写入如下代码:

type Setting struct {
    vp *viper.Viper
}

func NewSetting() (*Setting, error) {
    vp := viper.New()
    vp.SetConfigName("config")
    vp.AddConfigPath("configs/")
    vp.SetConfigType("yaml")
    err := vp.ReadInConfig()
    if err != nil {
        return nil, err
    }

    return &Setting{vp}, nil
}

在这里我们编写了 NewSetting 方法,用于初始化本项目的配置的基础属性,设定配置文件的名称为 config,配置类型为 yaml,并且设置其配置路径为相对路径 configs/,以此确保在项目目录下执行运行时能够成功启动。

另外 viper 是允许设置多个配置路径的,这样子可以尽可能的尝试解决路径查找的问题,也就是可以不断地调用 AddConfigPath 方法,这块在后续会再深入介绍。

接下来我们新建 section.go 文件,用于声明配置属性的结构体并编写读取区段配置的配置方法,如下:

type ServerSettingS struct {
    RunMode      string
    HttpPort     string
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
}

type AppSettingS struct {
    DefaultPageSize int
    MaxPageSize     int
    LogSavePath     string
    LogFileName     string
    LogFileExt      string
}

type DatabaseSettingS struct {
    DBType       string
    UserName     string
    Password     string
    Host         string
    DBName       string
    TablePrefix  string
    Charset      string
    ParseTime    bool
    MaxIdleConns int
    MaxOpenConns int
}

func (s *Setting) ReadSection(k string, v interface{}) error {
    err := s.vp.UnmarshalKey(k, v)
    if err != nil {
        return err
    }

    return nil
}
(4)包全局变量

在读取了文件的配置信息后,还是不够的,因为我们需要将配置信息和应用程序关联起来,我们才能够去使用它,因此在项目目录下的 global 目录下新建 setting.go 文件,写入如下代码:

var (
    ServerSetting   *setting.ServerSettingS
    AppSetting      *setting.AppSettingS
    DatabaseSetting *setting.DatabaseSettingS
)

我们针对最初预估的三个区段配置,进行了全局变量的声明,便于在接下来的步骤将其关联起来,并且提供给应用程序内部调用。

另外全局变量的初始化,是会随着应用程序的不断演进不断改变的,因此并不是一成不变,也就是这里展示的并不一定是最终的结果。

(5)初始化配置读取

在完成了所有的预备行为后,我们回到项目根目录下的 main.go 文件,修改代码如下:

func init() {
    err := setupSetting()
    if err != nil {
        log.Fatalf("init.setupSetting err: %v", err)
    }
}

func main() {...}

func setupSetting() error {
    setting, err := setting.NewSetting()
    if err != nil {
        return err
    }
    err = setting.ReadSection("Server", &global.ServerSetting)
    if err != nil {
        return err
    }
    err = setting.ReadSection("App", &global.AppSetting)
    if err != nil {
        return err
    }
    err = setting.ReadSection("Database", &global.DatabaseSetting)
    if err != nil {
        return err
    }

    global.ServerSetting.ReadTimeout *= time.Second
    global.ServerSetting.WriteTimeout *= time.Second
    return nil
}

我们新增了一个 init 方法,在 Go 语言中,init 方法常用于应用程序内的一些初始化操作,它在 main 方法之前自动执行,它的执行顺序是:全局变量初始化 =》init 方法 =》main 方法,但并不是建议滥用,因为如果 init 过多,你可能会迷失在各个库的 init 方法中,会非常麻烦。

而在我们的应用程序中,该 init 方法主要作用是进行应用程序的初始化流程控制,整个应用代码里也只会有一个 init 方法,因此我们在这里调用了初始化配置的方法,达到配置文件内容映射到应用配置结构体的作用。

(6)修改服务端配置

接下来我们只需要在启动文件 main.go 中把已经映射好的配置和 gin 的运行模式进行设置,这样的话,在程序重新启动时后就可以生效,如下:

func main() {
    gin.SetMode(global.ServerSetting.RunMode)
    router := routers.NewRouter()
    s := &http.Server{
        Addr:           ":" + global.ServerSetting.HttpPort,
        Handler:        router,
        ReadTimeout:    global.ServerSetting.ReadTimeout,
        WriteTimeout:   global.ServerSetting.WriteTimeout,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe()
}

验证如下

转存失败,建议直接上传图片文件

日志如下 证明结构体配置正确

转存失败,建议直接上传图片文件

PS D:\GOfiles\Gormlearn> go run main.go
2024/11/28 15:08:30 global.ServerSetting: &{RunMode:debug HttpPort:9000 ReadTimeout:1m0s WriteTimeout:1m0s}
2024/11/28 15:08:30 global.AppSetting: &{DefaultPageSize:10 MaxPageSize:100 LogSavePath:storage/logs LogFileName:app LogFileExt:.log}
2024/11/28 15:08:30 global.DatabaseSetting: &{DBType:mysql UserName:root Password:wxe13867187633 Host:127.0.0.1:3306 DBName:blog_service TablePrefix:blog_ Charset:utf8 ParseTime:true MaxIdleConns:10 MaxOpenConns:30}
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /api/v1/tags              --> Gormlearn/internal/routers/api/v1.Tag.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id          --> Gormlearn/internal/routers/api/v1.Tag.Delete-fm (3 handlers)
[GIN-debug] PUT    /api/v1/tags/:id          --> Gormlearn/internal/routers/api/v1.Tag.Update-fm (3 handlers)
[GIN-debug] PATCH  /api/v1/tags/:id/state    --> Gormlearn/internal/routers/api/v1.Tag.Update-fm (3 handlers)
[GIN-debug] GET    /api/v1/tags              --> Gormlearn/internal/routers/api/v1.Tag.List-fm (3 handlers)
[GIN-debug] POST   /api/v1/articles          --> Gormlearn/internal/routers/api/v1.Article.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id      --> Gormlearn/internal/routers/api/v1.Article.Delete-fm (3 handlers)
[GIN-debug] PUT    /api/v1/articles/:id      --> Gormlearn/internal/routers/api/v1.Article.Update-fm (3 handlers)
[GIN-debug] PATCH  /api/v1/articles/:id/state --> Gormlearn/internal/routers/api/v1.Article.Update-fm (3 handlers)
[GIN-debug] GET    /api/v1/articles/:id      --> Gormlearn/internal/routers/api/v1.Article.Get-fm (3 handlers)
[GIN-debug] GET    /api/v1/articles          --> Gormlearn/internal/routers/api/v1.Article.List-fm (3 handlers)

数据库链接

(1)gorm安装

我们在本项目中数据库相关的数据操作将使用第三方的开源库 gorm,它是目前 Go 语言中最流行的 ORM 库(从 Github Star 来看),同时它也是一个功能齐全且对开发人员友好的 ORM 库,目前在 Github 上相当的活跃,具有一定的保障,安装命令如下:

$ go get -u github.com/jinzhu/gorm@v1.9.12
(2)编写组件

我们打开项目目录 internal/model 下的 model.go 文件,新增 NewDBEngine 方法,如下:

import (
    ...
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

type Model struct {...}

func NewDBEngine(databaseSetting *setting.DatabaseSettingS) (*gorm.DB, error) {
    s := "%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local"
    db, err := gorm.Open(databaseSetting.DBType, fmt.Sprintf(s,
        databaseSetting.UserName,
        databaseSetting.Password,
        databaseSetting.Host,
        databaseSetting.DBName,
        databaseSetting.Charset,
        databaseSetting.ParseTime,
    ))
    if err != nil {
        return nil, err
    }

    if global.ServerSetting.RunMode == "debug" {
        db.LogMode(true)
    }
    db.SingularTable(true)
    db.DB().SetMaxIdleConns(databaseSetting.MaxIdleConns)
    db.DB().SetMaxOpenConns(databaseSetting.MaxOpenConns)

    return db, nil
}

我们通过上述代码,编写了一个针对创建 DB 实例的 NewDBEngine 方法,同时增加了 gorm 开源库的引入和 MySQL 驱动库 github.com/jinzhu/gorm/dialects/mysql 的初始化(不同类型的 DBType 需要引入不同的驱动库,否则会存在问题)。


增删改查最终实现

部分代码如下

package main

import (
    "github.com/gin-gonic/gin"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/schema"
    "net/http"
)

var DB *gorm.DB

func initMySQL() (err error) {
    dsn := "root:root@tcp(127.0.0.1:13306)/bubble?charset=utf8mb4&parseTime=True&loc=Local"
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
        NamingStrategy: schema.NamingStrategy{
            SingularTable: true,
        },
    })
    if err != nil {
        return
    }
    return nil
}

type Todo struct {
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Status bool   `json:"status"`
}

func main() {
    // 连接数据库
    err := initMySQL()
    if err != nil {
        panic(err)
    }
    // 模型绑定
    err = DB.AutoMigrate(&Todo{})
    if err != nil {
        return
    }

    r := gin.Default()

    v1Group := r.Group("/v1")
    {
        // 添加
        v1Group.POST("/todo", func(c *gin.Context) {
            // 1.从请求中取出数据
            var todo Todo
            if err = c.ShouldBindJSON(&todo); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            }
            // 2.存入数据库
            if err = DB.Create(&todo).Error; err != nil {
                c.JSON(http.StatusOK, gin.H{"error": err.Error()})
                return
            } else {
                // 3.返回响应
                c.JSON(http.StatusOK, gin.H{"data": todo})
            }

        })
        // 查看所有的待办事项
        v1Group.GET("/todo", func(c *gin.Context) {
            var todoList []Todo
            if err = DB.Find(&todoList).Error; err != nil {
                c.JSON(http.StatusOK, gin.H{"error": err.Error()})
                return
            } else {
                c.JSON(http.StatusOK, gin.H{"data": todoList})
            }
        })
        // 查看某个待办事项
        v1Group.GET("/todo/:id", func(c *gin.Context) {
            var todo Todo
            if err = DB.First(&todo, c.Param("id")).Error; err != nil {
                c.JSON(http.StatusOK, gin.H{"error": err.Error()})
                return
            } else {
                c.JSON(http.StatusOK, gin.H{"data": todo})
            }
        })
        // 更新某一个待办事项
        v1Group.PUT("/todo/:id", func(c *gin.Context) {
            id, ok := c.Params.Get("id")
            if !ok {
                c.JSON(http.StatusOK, gin.H{"error": "无效的id"})
            }
            var todo Todo
            if err = DB.Where("id = ?", id).First(&todo).Error; err != nil {
                c.JSON(http.StatusOK, gin.H{"error": err.Error()})
                return
            }
            c.ShouldBindJSON(&todo)
            if err = DB.Save(&todo).Error; err != nil {
                c.JSON(http.StatusOK, gin.H{"error": err.Error()})
                return
            } else {
                c.JSON(http.StatusOK, gin.H{"data": todo})
            }
        })
        // 删除某个待办事项
        v1Group.DELETE("/todo/:id", func(c *gin.Context) {
            var todo Todo
            id, ok := c.Params.Get("id")
            if !ok {
                c.JSON(http.StatusOK, gin.H{"error": "无效的id"})
            }
            if err = DB.Where("id = ?", id).Delete(&Todo{}).Error; err != nil {
                c.JSON(http.StatusOK, gin.H{"error": err.Error()})
                return
            } else {
                c.JSON(http.StatusOK, gin.H{"data": todo})
            }

        })
    }

    err = r.Run(":8080")
    if err != nil {
        return
    }
}

image.png 部分日志

PS D:\GOfiles\Gormlearn> go run main.go
2024/11/28 15:08:30 global.ServerSetting: &{RunMode:debug HttpPort:9000 ReadTimeout:1m0s WriteTimeout:1m0s}
2024/11/28 15:08:30 global.AppSetting: &{DefaultPageSize:10 MaxPageSize:100 LogSavePath:storage/logs LogFileName:app LogFileExt:.log}
2024/11/28 15:08:30 global.DatabaseSetting: &{DBType:mysql UserName:root Password:wxe13867187633 Host:127.0.0.1:3306 DBName:blog_service TablePrefix:blog_ Charset:utf8 ParseTime:true MaxIdleConns:10 MaxOpenConns:30}
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /api/v1/tags              --> Gormlearn/internal/routers/api/v1.Tag.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id          --> Gormlearn/internal/routers/api/v1.Tag.Delete-fm (3 handlers)
[GIN-debug] PUT    /api/v1/tags/:id          --> Gormlearn/internal/routers/api/v1.Tag.Update-fm (3 handlers)
[GIN-debug] PATCH  /api/v1/tags/:id/state    --> Gormlearn/internal/routers/api/v1.Tag.Update-fm (3 handlers)
[GIN-debug] GET    /api/v1/tags              --> Gormlearn/internal/routers/api/v1.Tag.List-fm (3 handlers)
[GIN-debug] POST   /api/v1/articles          --> Gormlearn/internal/routers/api/v1.Article.Create-fm (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id      --> Gormlearn/internal/routers/api/v1.Article.Delete-fm (3 handlers)
[GIN-debug] PUT    /api/v1/articles/:id      --> Gormlearn/internal/routers/api/v1.Article.Update-fm (3 handlers)
[GIN-debug] PATCH  /api/v1/articles/:id/state --> Gormlearn/internal/routers/api/v1.Article.Update-fm (3 handlers)
[GIN-debug] GET    /api/v1/articles/:id      --> Gormlearn/internal/routers/api/v1.Article.Get-fm (3 handlers)
[GIN-debug] GET    /api/v1/articles          --> Gormlearn/internal/routers/api/v1.Article.List-fm (3 handlers)

复杂实现 image.png 简单实现之一 image.png