GORM:Go的ORM 框架

文章目录

特性

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 Preload、Joins 的预加载
  • 事务,嵌套事务,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
  • Generics API for type-safe queries and operations
  • Extendable, flexible plugin API: Database Resolver (multiple databases, read/write splitting) / Prometheus...
  • 每个特性都经过了测试的重重考验
  • 开发者友好

安装

go 复制代码
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

快速入门

基础的增删改查

go 复制代码
package main

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

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")
  }

  ctx := context.Background()

  // Migrate the schema
  db.AutoMigrate(&Product{})

  // Create
  err = gorm.G[Product](db).Create(ctx, &Product{Code: "D42", Price: 100})

  // Read
  product, err := gorm.G[Product](db).Where("id = ?", 1).First(ctx) // find product with integer primary key
  products, err := gorm.G[Product](db).Where("code = ?", "D42").Find(ctx) // find product with code D42

  // Update - update product's price to 200
  err = gorm.G[Product](db).Where("id = ?", product.ID).Update(ctx, "Price", 200)
  // Update - update multiple fields
  err = gorm.G[Product](db).Where("id = ?", product.ID).Updates(ctx, map[string]interface{}{"Price": 200, "Code": "F42"})

  // Delete - delete product
  err = gorm.G[Product](db).Where("id = ?", product.ID).Delete(ctx)
}

模型的定义

GORM 通过将 Go 结构体(Go structs)可以理解为Java里的Do层映射到数据库表来简化数据库交互。 了解如何在GORM中定义模型,是充分利用GORM全部功能的基础。

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

go 复制代码
type User struct {
    ID           uint           // 标准字段,用作主键
    Name         string         // 普通字符串字段
    Email        *string        // 指向字符串的指针,允许为空(NULL)
    Age          uint8          // 无符号8位整数
    Birthday     *time.Time     // 指向时间的指针,可以为空(NULL)
    MemberNumber sql.NullString // 使用 sql.NullString 来处理可为空的字符串
    ActivatedAt  sql.NullTime   // 使用 sql.NullTime 来处理可为空的时间字段
    CreatedAt    time.Time      // GORM 自动管理的创建时间
    UpdatedAt    time.Time      // GORM 自动管理的更新时间
    ignored      string         // 未导出的字段会被 GORM 忽略
}

在此模型中:

  • 具体数字类型如 uint、string和 uint8 直接使用。
  • 指向 *string 和 *time.Time 类型的指针表示可空字段。
  • 来自 database/sql 包的 sql.NullString 和 sql.NullTime 用于具有更多控制的可空字段。
  • CreatedAt 和 UpdatedAt 是特殊字段,当记录被创建或更新时,GORM 会自动向内填充当前时间。
  • Non-exported fields (starting with a small letter) are not mapped

约定

  1. 主键:GORM 使用一个名为ID 的字段作为每个模型的默认主键。
  2. 表名:默认情况下,GORM 将结构体名称转换为 snake_case 并为表名加上复数形式。

例如,一个 User 结构体在数据库中对应的表名会变成 users,而 GormUserName 则会变成 gorm_user_names。

  1. 列名:GORM 自动将结构体字段名称转换为 snake_case 作为数据库中的列名。
  2. 时间戳字段:GORM使用字段 CreatedAt 和 UpdatedAt 来自动跟踪记录的创建和更新时间。
    遵循这些约定可以大大减少您需要编写的配置或代码量。 但是,GORM也具有灵活性,允许根据自己的需求自定义这些设置。

示例

  1. 列名转换
结构体字段 数据库列名 说明
ID id 默认主键
Name name 转换为 snake_case
Email email 转换为 snake_case
Age age 转换为 snake_case
CreatedAt created_at 时间戳字段
UpdatedAt updated_at 时间戳字段
  1. 表名转换
结构体名称 数据库表名 说明
User users 结构体名转 snake_case + 复数
GormUserName gorm_user_names 结构体名转 snake_case + 复数
Product products 结构体名转 snake_case + 复数
OrderItem order_items 结构体名转 snake_case + 复数

自定义约定示例

如果需要自定义表名,可以实现TableName方法:

go 复制代码
func (User) TableName() string {
    return "user_profiles" // 自定义表名
}

或者通过配置全局修改:

go 复制代码
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
    NamingStrategy: schema.NamingStrategy{
        TablePrefix:     "tbl_", // 表名前缀
        SingularTable:   true,   // 使用单数表名
        NoLowerCase:     false,  // 使用小写
        NameReplacer:    strings.NewReplacer("ID", "Id"), // 替换特定名称
    },
})

gorm.Model

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

go 复制代码
// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}
  • ID :每个记录的唯一标识符(主键)。
  • CreatedAt :在创建记录时自动设置为当前时间。
  • UpdatedAt:每当记录更新时,自动更新为当前时间。
  • DeletedAt:用于软删除(将记录标记为已删除,而实际上并未从数据库中删除)。
    使用方式如下:
go 复制代码
package model

import (
    "database/sql"
    "time"

    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    ID           uint           // 标准字段,用作主键
    Name         string         // 普通字符串字段
    Email        *string        // 指向字符串的指针,允许为空(NULL)
    Age          uint8          // 无符号8位整数
    Birthday     *time.Time     // 指向时间的指针,可以为空(NULL)
    MemberNumber sql.NullString // 使用 sql.NullString 来处理可为空的字符串
    ActivatedAt  sql.NullTime   // 使用 sql.NullTime 来处理可为空的时间字段
    ignored      string         // 未导出的字段会被 GORM 忽略
}

字段级权限控制

可导出的字段在使用 GORM 进行 CRUD 时拥有全部的权限,此外,GORM 允许您用标签控制字段级别的权限。这样您就可以让一个字段的权限是只读、只写、只创建、只更新或者被忽略

go 复制代码
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 读写会忽略该字段
  Name string `gorm:"-:all"`        // 通过 struct 读写、迁移会忽略该字段
  Name string `gorm:"-:migration"`  // 通过 struct 迁移会忽略该字段
}

嵌入结构体

  1. 方式1:直接嵌入
go 复制代码
type Author struct {
  Name  string
  Email string
}

type Blog struct {
  Author
  ID      int
  Upvotes int32
}
// equals
type Blog struct {
  ID      int64
  Name    string
  Email   string
  Upvotes int32
}
  1. 通过标签 embedded嵌入
go 复制代码
type Author struct {
    Name  string
    Email string
}

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded"`
  Upvotes int32
}
// 等效于
type Blog struct {
  ID    int64
  Name  string
  Email string
  Upvotes  int32
}
  1. 使用标签 embeddedPrefix 来为 db 中的字段名添加前缀
go 复制代码
type Blog struct {
  ID      int
  Author  Author `gorm:"embedded;embeddedPrefix:author_"`
  Upvotes int32
}
// 等效于
type Blog struct {
  ID          int64
  AuthorName string
  AuthorEmail string
  Upvotes     int32
}

声明模型时,标签是可选的,GORM支持以下标签:标签不区分大小写,但建议使用驼峰式命名法(camelCase)。如果使用多个标签,则应使用分号(;)分隔。对于解析器具有特殊含义的字符可以使用反斜杠(1)进行转义,以便将其用作参数值。

标签名 说明
column 指定 db 列名
type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 boolintuintfloatstringtimebytes,并且可以和其他标签一起使用,例如: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 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 [索引](#标签名 说明 column 指定 db 列名 type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes,并且可以和其他标签一起使用,例如:not null、size、autoIncrement … 像 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 迁移时为字段添加注释) 获取详情
uniqueIndex index 相同,但创建的是唯一索引
check 创建检查约束,例如 check:age > 13,查看 [约束](#标签名 说明 column 指定 db 列名 type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes,并且可以和其他标签一起使用,例如:not null、size、autoIncrement … 像 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 迁移时为字段添加注释) 获取详情
<- 设置字段写入的权限,<-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
-> 设置字段读的权限,->:false 无读权限
- 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限
comment 迁移时为字段添加注释

连接数据库

最基础的用法

go 复制代码
import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main() {
  // 参考 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{})
}

CRUD

go 复制代码
package main

import (
        "fmt"
        "time"

        "gorm.io/driver/mysql"
        "gorm.io/gorm"
        "gorm.io/gorm/clause"
)

// 定义符合 GORM 约定的 User 模型(嵌入 gorm.Model)
type User struct {
        gorm.Model // 自动包含 ID, CreatedAt, UpdatedAt, DeletedAt

        Name         string         `gorm:"size:255"` // 可指定字段长度
        Email        *string        `gorm:"unique"`   // 唯一索引
        Age          uint8          `gorm:"default:18"` // 默认值
        Birthday     *time.Time     // 指向时间的指针
        MemberNumber sql.NullString // 处理可为空的字符串
        ActivatedAt  sql.NullTime   // 处理可为空的时间字段
        ignored      string         // 未导出字段会被 GORM 忽略
}

func main() {
        // 1. 连接 MySQL 数据库
        dsn := "root:root@tcp(127.0.0.1:3306)/gorm_demo?charset=utf8mb4&parseTime=True&loc=Local"
        db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
        if err != nil {
                panic("数据库连接失败: " + err.Error())
        }

        // 2. 自动迁移(创建表)
        db.AutoMigrate(&User{})

        // 3. 创建操作 (Create)
        fmt.Println("\n=== 创建用户 ===")
        user1 := User{
                Name:      "张三",
                Email:     &[]string{"zhangsan@example.com"}[0],
                Age:       30,
                Birthday:  &time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
                MemberNumber: sql.NullString{String: "M12345", Valid: true},
                ActivatedAt:  sql.NullTime{Time: time.Now(), Valid: true},
        }
        if err := db.Create(&user1).Error; err != nil {
                panic("创建用户失败: " + err.Error())
        }
        fmt.Printf("创建成功! ID: %d\n", user1.ID)

        // 4. 查询操作 (Read)
        fmt.Println("\n=== 查询用户 ===")
        var user2 User
        // 查询单个记录
        if err := db.First(&user2, user1.ID).Error; err != nil {
                panic("查询用户失败: " + err.Error())
        }
        fmt.Printf("查询到用户: %s (ID: %d, 年龄: %d)\n", user2.Name, user2.ID, user2.Age)

        // 查询多个记录
        var users []User
        db.Where("age > ?", 25).Find(&users)
        fmt.Printf("年龄大于25的用户: %d 个\n", len(users))

        // 使用条件查询
        var user3 User
        db.Where("name = ?", "张三").First(&user3)
        fmt.Printf("条件查询: %s (ID: %d)\n", user3.Name, user3.ID)

        // 5. 更新操作 (Update)
        fmt.Println("\n=== 更新用户 ===")
        user2.Age = 31
        if err := db.Save(&user2).Error; err != nil {
                panic("更新用户失败: " + err.Error())
        }
        fmt.Printf("更新成功! 年龄: %d\n", user2.Age)

        // 更新特定字段
        db.Model(&user2).Update("age", 32)
        fmt.Printf("更新特定字段: 年龄: %d\n", user2.Age)

        // 6. 删除操作 (Delete)
        fmt.Println("\n=== 删除用户 ===")
        // 软删除(默认)
        if err := db.Delete(&user2).Error; err != nil {
                panic("软删除失败: " + err.Error())
        }
        fmt.Printf("软删除成功! ID: %d\n", user2.ID)

        // 硬删除(需要设置 DeleteMode 为 HardDelete)
        if err := db.Unscoped().Delete(&user2).Error; err != nil {
                panic("硬删除失败: " + err.Error())
        }
        fmt.Printf("硬删除成功! ID: %d\n", user2.ID)

        // 7. 其他常用操作
        fmt.Println("\n=== 其他常用操作 ===")
        // 创建或更新(如果存在则更新,不存在则创建)
        user4 := User{Name: "李四", Age: 25}
        db.FirstOrCreate(&user4, User{Name: "李四"})
        fmt.Printf("创建或更新: %s (ID: %d)\n", user4.Name, user4.ID)

        // 通过 ID 查询
        var user5 User
        db.First(&user5, 1) // 通过 ID 查询
        fmt.Printf("通过 ID 查询: %s\n", user5.Name)

        // 分页查询
        var usersPage []User
        db.Limit(2).Offset(0).Find(&usersPage)
        fmt.Printf("分页查询: %d 个用户\n", len(usersPage))

        // 计数
        var count int64
        db.Model(&User{}).Where("age > ?", 25).Count(&count)
        fmt.Printf("年龄大于25的用户总数: %d\n", count)
}

在使用 GORM 的 AutoMigrate 功能时,无需手动创建数据库表,只需定义好 Go 结构体(模型),然后调用 AutoMigrate 方法,GORM 会自动根据结构体定义创建数据库表。

方式2:

go 复制代码
package main

import (
        "fmt"
        "time"

        "gorm.io/driver/mysql"
        "gorm.io/gorm"
        "gorm.io/gorm/clause"
)

// 自定义表名模型(演示如何指定表名)
type EntityRobot struct {
        ID         uint           `gorm:"primary_key"`
        EntityID   uint           `gorm:"index:idx_entity_biz"`
        BizType    string         `gorm:"size:50;index:idx_entity_biz"`
        Params     map[string]interface{} `gorm:"type:json"` // 使用JSON存储参数
        CreatedAt  time.Time
        UpdatedAt  time.Time
}

// 实现TableName方法指定表名
func (EntityRobot) TableName() string {
        return "entity_robot" // 自定义表名,与数据库表名一致
}

func main() {
        // 1. 数据库连接
        dsn := "root:root@tcp(127.0.0.1:3306)/gorm_demo?charset=utf8mb4&parseTime=True&loc=Local"
        db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
        if err != nil {
                panic("数据库连接失败: " + err.Error())
        }

        // 2. 自动迁移(创建表)
        db.AutoMigrate(&EntityRobot{})

        // 3. 创建测试数据
        robot := EntityRobot{
                EntityID: 1001,
                BizType:  "user_profile",
                Params:   map[string]interface{}{"name": "张三", "age": 30},
        }
        db.Create(&robot)

        // 4. 演示您的示例:指定表名 + 条件更新
        entityId := uint(1001)
        bizType := "user_profile"
        params := map[string]interface{}{
                "params": map[string]interface{}{"name": "李四", "age": 32},
        }

        // 使用 Table() 指定表名 + Where 条件 + Update
        result := db.Table("entity_robot").
                Where("entity_id = ? AND biz_type = ?", entityId, bizType).
                Updates(params)

        // 检查更新结果
        fmt.Printf("更新影响行数: %d\n", result.RowsAffected)
        if result.Error != nil {
                panic("更新失败: " + result.Error.Error())
        }

        // 5. 验证更新结果
        var updatedRobot EntityRobot
        db.Where("entity_id = ? AND biz_type = ?", entityId, bizType).First(&updatedRobot)
        fmt.Printf("更新后参数: %+v\n", updatedRobot.Params)

        // 6. 其他常用操作演示

        // 6.1 创建(带JSON参数)
        newRobot := EntityRobot{
                EntityID: 1002,
                BizType:  "order",
                Params:   map[string]interface{}{"order_id": "ORD1001", "amount": 199.9},
        }
        db.Create(&newRobot)

        // 6.2 查询(带条件)
        var robots []EntityRobot
        db.Where("biz_type = ?", "order").Find(&robots)
        fmt.Printf("查询到 %d 个订单记录\n", len(robots))

        // 6.3 软删除(默认)
        db.Delete(&newRobot)

        // 6.4 硬删除(需要使用 Unscoped)
        db.Unscoped().Delete(&newRobot)

        // 6.5 批量更新(更新多个记录)
        db.Table("entity_robot").
                Where("biz_type = ?", "user_profile").
                Updates(map[string]interface{}{
                        "params": map[string]interface{}{"status": "active"},
                })

        // 6.6 原生SQL查询
        var rawResult []struct {
                EntityID uint
                BizType  string
        }
        db.Raw("SELECT entity_id, biz_type FROM entity_robot WHERE params->>'$.age' > ?", 30).Scan(&rawResult)
        fmt.Printf("查询到 %d 条年龄>30的记录\n", len(rawResult))
}

这种方式也是我们项目中常用的一个操作

相关推荐
源代码•宸4 小时前
Golang语法进阶(定时器)
开发语言·经验分享·后端·算法·golang·timer·ticker
a程序小傲5 小时前
得物Java面试被问:边缘计算的数据同步和计算卸载
java·开发语言·数据库·后端·面试·golang·边缘计算
nbsaas-boot17 小时前
Go vs Java 的三阶段切换路线图
java·开发语言·golang
modelmd20 小时前
Go 编程语言指南 练习题目分享
开发语言·学习·golang
福大大架构师每日一题1 天前
2026年1月TIOBE编程语言排行榜,Go语言排名第16,Rust语言排名13。C# 当选 2025 年度编程语言。
golang·rust·c#
拔剑纵狂歌1 天前
helm-cli安装资源时序报错问题问题
后端·docker·云原生·容器·golang·kubernetes·腾讯云
bing.shao1 天前
AI在电商上架图片领域的应用
开发语言·人工智能·golang
源代码•宸1 天前
Leetcode—712. 两个字符串的最小ASCII删除和【中等】
开发语言·后端·算法·leetcode·职场和发展·golang·dp
源代码•宸1 天前
Golang语法进阶(Context)
开发语言·后端·算法·golang·context·withvalue·withcancel