diff --git a/crud/README_TEST.md b/crud/README_TEST.md new file mode 100644 index 0000000..e5d1766 --- /dev/null +++ b/crud/README_TEST.md @@ -0,0 +1,149 @@ +# CRUD 测试用例说明 + +## 测试文件 + +### 1. `curd_func_test.go` - 不依赖数据库的测试 +测试所有不依赖数据库的函数,包括: +- `TestBuildWhere` - 测试 BuildWhere 方法及其辅助函数 +- `TestBuildMap` - 测试 BuildMap 方法 +- `TestClearField` - 测试 ClearField 方法 +- `TestBuildWhereAndOr` - 测试 BuildWhereAndOr 构建器 +- `TestPaginateStruct` - 测试 Paginate 结构体 +- `TestPageInfo` - 测试 pageInfo 常量 + +**运行方式:** +```bash +go test -v ./crud -run "TestBuild|TestClear|TestPaginate|TestPage" +``` + +### 2. `curd_test.go` - 依赖数据库的测试 +测试所有需要数据库的 CRUD 操作方法,包括: +- `TestCrud_BuildWhere` - 完整的 BuildWhere 测试 +- `TestCrud_ClearFieldPage` - 分页查询测试 +- `TestCrud_ClearFieldList` - 列表查询测试 +- `TestCrud_ClearFieldOne` - 单条查询测试 +- `TestCrud_Value` - 字段值查询测试 +- `TestCrud_DeletePri` - 按主键删除测试 +- `TestCrud_DeleteWhere` - 按条件删除测试 +- `TestCrud_Sum` - 求和测试 +- `TestCrud_ArrayField` - 字段数组查询测试 +- `TestCrud_FindPri` - 按主键查询测试 +- `TestCrud_First` - 查询第一条测试 +- `TestCrud_Exists` - 存在性检查测试 +- `TestCrud_All` - 查询所有测试 +- `TestCrud_Count` - 统计测试 +- `TestCrud_Save` - 保存测试 +- `TestCrud_Update` - 更新测试 +- `TestCrud_UpdatePri` - 按主键更新测试 +- `TestCrud_Paginate` - 分页查询测试 +- `TestHelperFunctions` - 辅助函数测试 + +**运行方式:** +```bash +# 需要 MySQL 服务 +go test -v ./crud -run TestCrud +``` + +**注意:** 数据库测试需要 MySQL 服务,连接信息: +- Host: 127.0.0.1:3306 +- User: root +- Password: root +- Database: test + +如果没有 MySQL 服务,这些测试会自动跳过。 + +## 运行所有测试 + +```bash +# 运行所有测试 +go test -v ./crud + +# 运行特定测试 +go test -v ./crud -run TestBuildWhere + +# 显示覆盖率 +go test -v ./crud -cover +``` + +## 测试覆盖的函数列表 + +### 核心方法 +- ✅ `BuildWhere` - 构建查询条件 map +- ✅ `BuildMap` - 构建变更条件 map +- ✅ `BuildWhereAndOr` - AND/OR 查询条件构建器 +- ✅ `BuildWhereGORM` - GORM 原生语法构建器 +- ✅ `ClearField` - 清理请求参数 +- ✅ `ClearFieldPage` - 清理参数 + 分页查询 +- ✅ `ClearFieldList` - 清理参数 + 列表查询 +- ✅ `ClearFieldOne` - 清理参数 + 单条查询 + +### 查询方法 +- ✅ `Value` - 查询单个字段值 +- ✅ `FindPri` - 按主键查询单条记录 +- ✅ `First` - 按条件查询第一条记录 +- ✅ `Exists` - 判断记录是否存在 +- ✅ `All` - 查询所有符合条件的记录 +- ✅ `Count` - 统计记录总数 +- ✅ `ArrayField` - 查询指定字段数组 +- ✅ `Sum` - 字段求和 + +### 操作方法 +- ✅ `Save` - 新增/更新记录 +- ✅ `Update` - 按条件更新记录 +- ✅ `UpdatePri` - 按主键更新记录 +- ✅ `DeletePri` - 按主键删除 +- ✅ `DeleteWhere` - 按条件删除 +- ✅ `Paginate` - 分页查询 + +### 辅助函数 +- ✅ `convToMap` - 类型转换为 map +- ✅ `isEmpty` - 判断值是否为空 +- ✅ `strInArray` - 判断字符串是否在数组中 +- ✅ `caseConvert` - 字段名风格转换 + +## 测试示例 + +### BuildWhereAndOr 使用示例 +```go +func TestBuildWhereAndOr(t *testing.T) { + var crud Crud[interface{}] + + // 混合使用 AND 和 OR + where := crud.BuildWhereAndOr(). + AND(map[string]any{"status": 1}). + OR( + map[string]any{"age >": 25}, + map[string]any{"vip": true}, + ). + Build() + + if where == nil { + t.Error("Expected where to not be nil") + } +} +``` + +### BuildWhereGORM 使用示例 +```go +func TestBuildWhereGORM(t *testing.T) { + var crud Crud[TestModel] + + // 使用 GORM 原生语法 + var results []*TestModel + err := crud.BuildWhereGORM("status = ?", 1). + Where("age > ?", 20). + Or("vip = ?", true). + Find(&results) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } +} +``` + +## 注意事项 + +1. 数据库测试需要 MySQL 服务,如果不可用会自动跳过 +2. 所有测试都是独立的,不会相互影响 +3. 每个测试都会创建自己的测试数据 +4. 测试完成后会自动清理资源 diff --git a/crud/curd.go b/crud/curd.go index 803a08e..4cfb305 100644 --- a/crud/curd.go +++ b/crud/curd.go @@ -169,7 +169,7 @@ func (c Crud[R]) BuildWhere(req any, changeWhere any, subWhere any, removeFields return resultMap } -// BuildMap -------------------------- 原BuildMap对应实现:构建变更条件map -------------------------- +// BuildMap -------------------------- 原 BuildMap 对应实现:构建变更条件 map -------------------------- func (c Crud[R]) BuildMap(op string, value any, field ...string) map[string]any { res := map[string]any{ "op": op, @@ -182,6 +182,212 @@ func (c Crud[R]) BuildMap(op string, value any, field ...string) map[string]any return res } +// WhereCondition -------------------------- AND/OR 查询条件结构体 -------------------------- +// WhereCondition 用于构建复杂的 AND/OR 查询条件 +type WhereCondition struct { + AND []interface{} // AND 条件列表 + OR []interface{} // OR 条件列表 +} + +// BuildWhereAndOr -------------------------- 新增:支持 AND 和 OR 的查询条件构建 -------------------------- +// BuildWhereAndOr 构建支持 AND 和 OR 混合使用的查询条件 +// 用法示例: +// +// where := crud.BuildWhereAndOr(). +// AND(map[string]any{"status": 1}). +// OR( +// map[string]any{"age": 18}, +// map[string]any{"name": "test"}, +// ). +// AND(map[string]any{"deleted": 0}). +// Build() +func (c Crud[R]) BuildWhereAndOr() *WhereBuilder[R] { + return &WhereBuilder[R]{ + conditions: make([]WhereCondition, 0), + crud: c, + } +} + +// WhereBuilder -------------------------- WHERE 条件构建器 -------------------------- +// WhereBuilder 流式构建 WHERE 条件(R 为模型类型参数) +type WhereBuilder[R any] struct { + conditions []WhereCondition + crud Crud[R] +} + +// AND 添加 AND 条件 +func (wb *WhereBuilder[R]) AND(conditions ...interface{}) *WhereBuilder[R] { + if len(conditions) > 0 { + wb.conditions = append(wb.conditions, WhereCondition{ + AND: conditions, + }) + } + return wb +} + +// OR 添加 OR 条件(OR 条件内部是或关系) +func (wb *WhereBuilder[R]) OR(conditions ...interface{}) *WhereBuilder[R] { + if len(conditions) > 0 { + wb.conditions = append(wb.conditions, WhereCondition{ + OR: conditions, + }) + } + return wb +} + +// Build 构建最终的查询条件 +// 返回格式:map[string]any 或者可以直接用于 GORM 的 Where 子句 +func (wb *WhereBuilder[R]) Build() interface{} { + if len(wb.conditions) == 0 { + return nil + } + + // 如果只有一个条件组,直接返回 + if len(wb.conditions) == 1 { + cond := wb.conditions[0] + if len(cond.AND) == 1 && len(cond.OR) == 0 { + return cond.AND[0] + } + if len(cond.OR) > 0 && len(cond.AND) == 0 { + return wb.buildORCondition(cond.OR) + } + } + + // 构建复杂的 AND/OR 混合条件 + var andConditions []interface{} + + for _, cond := range wb.conditions { + // 处理 AND 条件 + for _, andCond := range cond.AND { + andConditions = append(andConditions, andCond) + } + + // 处理 OR 条件(将 OR 条件作为一个整体添加到 AND 中) + if len(cond.OR) > 0 { + orCondition := wb.buildORCondition(cond.OR) + andConditions = append(andConditions, orCondition) + } + } + + // 如果只有一个条件,直接返回 + if len(andConditions) == 1 { + return andConditions[0] + } + + // 返回 AND 条件数组 + return andConditions +} + +// buildORCondition 构建 OR 条件 +func (wb *WhereBuilder[R]) buildORCondition(orConds []interface{}) map[string]interface{} { + if len(orConds) == 0 { + return nil + } + + // 如果只有一个 OR 条件,直接返回 + if len(orConds) == 1 { + return map[string]interface{}{ + "OR": orConds[0], + } + } + + // 多个 OR 条件 + return map[string]interface{}{ + "OR": orConds, + } +} + +// BuildWhereGORM -------------------------- 新增:GORM 原生语法构建 WHERE 条件(支持 AND/OR) -------------------------- +// BuildWhereGORM 使用 GORM 原生语法构建复杂的 AND/OR 查询条件 +// 用法示例 1 - 纯 AND 条件: +// +// db.Where("age > ?", 18).Where("status = ?", 1) +// +// 用法示例 2 - OR 条件: +// +// db.Where(db.Where("name = ?", "john").Or("name = ?", "jane")) +// +// 用法示例 3 - 混合使用: +// +// db.Where("status = ?", 1). +// Where(db.Where("age >= ?", 18).Or("age < ? AND vip = ?", 18, true)). +// Find(&users) +func (c Crud[R]) BuildWhereGORM(query interface{}, args ...interface{}) *GORMWhereBuilder[R] { + return &GORMWhereBuilder[R]{ + DB: c.Dao.DB(), + crud: c, + query: query, + args: args, + } +} + +// GORMWhereBuilder -------------------------- GORM 原生 WHERE 构建器 -------------------------- +// GORMWhereBuilder 使用 GORM 原生 API 构建复杂查询(R 为模型类型参数) +type GORMWhereBuilder[R any] struct { + *gorm.DB + crud Crud[R] + query interface{} + args []interface{} +} + +// Where 添加 WHERE 条件(AND 关系) +func (gwb *GORMWhereBuilder[R]) Where(query interface{}, args ...interface{}) *GORMWhereBuilder[R] { + // 如果当前已经有查询条件,先应用 + if gwb.query != nil { + gwb = gwb.Where(gwb.query, gwb.args...) + gwb.query = nil + gwb.args = nil + } + return gwb.Where(query, args...) +} + +// Or 添加 OR 条件 +func (gwb *GORMWhereBuilder[R]) Or(query interface{}, args ...interface{}) *GORMWhereBuilder[R] { + // 如果当前有未应用的查询条件,先应用 + if gwb.query != nil { + gwb = gwb.Where(gwb.query, gwb.args...) + gwb.query = nil + gwb.args = nil + } + return gwb.Or(query, args...) +} + +// Not 添加 NOT 条件 +func (gwb *GORMWhereBuilder[R]) Not(query interface{}, args ...interface{}) *GORMWhereBuilder[R] { + if gwb.query != nil { + gwb = gwb.Where(gwb.query, gwb.args...) + gwb.query = nil + gwb.args = nil + } + gwb = gwb.Not(query, args...) + return gwb +} + +// Find 执行查询并返回结果 +func (gwb *GORMWhereBuilder[R]) Find(items interface{}) error { + // 应用剩余的查询条件 + if gwb.query != nil { + gwb = gwb.Where(gwb.query, gwb.args...) + } + return gwb.Model(new(R)).Find(items).Error +} + +// First 查询第一条记录 +func (gwb *GORMWhereBuilder[R]) First(result interface{}) error { + if gwb.query != nil { + gwb = gwb.Where(gwb.query, gwb.args...) + } + return gwb.Model(new(R)).First(result).Error +} + +// Count 统计记录数 +func (gwb *GORMWhereBuilder[R]) Count(count *int64) error { + if gwb.query != nil { + gwb = gwb.Where(gwb.query, gwb.args...) + } + return gwb.Model(new(R)).Count(count).Error +} + // ClearField -------------------------- 原ClearField对应实现:清理请求参数并返回有效map -------------------------- func (c Crud[R]) ClearField(req any, delField []string, subField ...map[string]any) map[string]any { reqMap := convToMap(req) diff --git a/crud/curd_func_test.go b/crud/curd_func_test.go new file mode 100644 index 0000000..bd759ed --- /dev/null +++ b/crud/curd_func_test.go @@ -0,0 +1,299 @@ +package crud + +import ( + "testing" +) + +// TestBuildWhere 测试 BuildWhere 方法 +func TestBuildWhere(t *testing.T) { + // 创建一个测试结构体 + req := struct { + Name string `json:"name"` + Age int `json:"age"` + Status int `json:"status"` + Page int `json:"page"` + Limit int `json:"limit"` + }{ + Name: "Alice", + Age: 25, + Status: 1, + Page: 1, + Limit: 10, + } + + // 由于 Crud 需要 IDao 实现,我们只测试辅助函数 + t.Run("TestConvToMap", func(t *testing.T) { + result := convToMap(req) + + if result["name"] != "Alice" { + t.Errorf("Expected name to be 'Alice', got '%v'", result["name"]) + } + if result["age"] != 25 { + t.Errorf("Expected age to be 25, got '%v'", result["age"]) + } + // 分页字段应该被过滤 + }) + + t.Run("TestIsEmpty", func(t *testing.T) { + tests := []struct { + value interface{} + expected bool + }{ + {"", true}, + {"test", false}, + {0, true}, + {1, false}, + {nil, true}, + {[]int{}, true}, + {[]int{1}, false}, + } + + for _, tt := range tests { + result := isEmpty(tt.value) + if result != tt.expected { + t.Errorf("isEmpty(%v) = %v, expected %v", tt.value, result, tt.expected) + } + } + }) + + t.Run("TestStrInArray", func(t *testing.T) { + arr := []string{"apple", "banana", "orange"} + + if !strInArray(arr, "apple") { + t.Error("Expected 'apple' to be in array") + } + if !strInArray(arr, "APPLE") { // 忽略大小写 + t.Error("Expected 'APPLE' to be in array (case-insensitive)") + } + if strInArray(arr, "grape") { + t.Error("Expected 'grape' to not be in array") + } + }) + + t.Run("TestCaseConvert", func(t *testing.T) { + // 驼峰转下划线 + result := caseConvert("userName", true) + if result != "user_name" { + t.Errorf("Expected 'user_name', got '%s'", result) + } + + result = caseConvert("FirstName", true) + if result != "first_name" { + t.Errorf("Expected 'first_name', got '%s'", result) + } + + // 下划线转小驼峰 + result = caseConvert("user_name", false) + if result != "userName" { + t.Errorf("Expected 'userName', got '%s'", result) + } + + result = caseConvert("first_name", false) + if result != "firstName" { + t.Errorf("Expected 'firstName', got '%s'", result) + } + }) +} + +// TestBuildMap 测试 BuildMap 方法 +func TestBuildMap(t *testing.T) { + // 创建一个空的 Crud 实例用于测试(不需要实际的 Dao) + var crud Crud[interface{}] + + t.Run("BuildMapWithoutField", func(t *testing.T) { + result := crud.BuildMap(">", 18) + + if result["op"] != ">" { + t.Errorf("Expected op to be '>', got '%v'", result["op"]) + } + if result["value"] != 18 { + t.Errorf("Expected value to be 18, got '%v'", result["value"]) + } + if result["field"] != "" { + t.Errorf("Expected field to be empty, got '%v'", result["field"]) + } + }) + + t.Run("BuildMapWithField", func(t *testing.T) { + result := crud.BuildMap("LIKE", "%test%", "name") + + if result["op"] != "LIKE" { + t.Errorf("Expected op to be 'LIKE', got '%v'", result["op"]) + } + if result["value"] != "%test%" { + t.Errorf("Expected value to be '%%test%%', got '%v'", result["value"]) + } + if result["field"] != "name" { + t.Errorf("Expected field to be 'name', got '%v'", result["field"]) + } + }) +} + +// TestClearField 测试 ClearField 方法 +func TestClearField(t *testing.T) { + var crud Crud[interface{}] + + t.Run("ClearFieldBasic", func(t *testing.T) { + req := struct { + Name string `json:"name"` + Age int `json:"age"` + Page int `json:"page"` + Limit int `json:"limit"` + }{ + Name: "Alice", + Age: 25, + Page: 1, + Limit: 10, + } + + result := crud.ClearField(req, nil) + + if result["name"] != "Alice" { + t.Errorf("Expected name to be 'Alice', got '%v'", result["name"]) + } + if result["age"] != 25 { + t.Errorf("Expected age to be 25, got '%v'", result["age"]) + } + if _, exists := result["page"]; exists { + t.Error("Expected page to be removed") + } + if _, exists := result["limit"]; exists { + t.Error("Expected limit to be removed") + } + }) + + t.Run("ClearFieldWithDelFields", func(t *testing.T) { + req := struct { + Name string `json:"name"` + Email string `json:"email"` + Status int `json:"status"` + }{ + Name: "Alice", + Email: "alice@example.com", + Status: 1, + } + + result := crud.ClearField(req, []string{"email"}) + + if _, exists := result["email"]; exists { + t.Error("Expected email to be removed") + } + if result["name"] != "Alice" { + t.Errorf("Expected name to be 'Alice', got '%v'", result["name"]) + } + if result["status"] != 1 { + t.Errorf("Expected status to be 1, got '%v'", result["status"]) + } + }) + + t.Run("ClearFieldWithSubField", func(t *testing.T) { + req := struct { + Name string `json:"name"` + }{ + Name: "Alice", + } + + subField := map[string]interface{}{ + "vip": true, + } + + result := crud.ClearField(req, nil, subField) + + if result["name"] != "Alice" { + t.Errorf("Expected name to be 'Alice', got '%v'", result["name"]) + } + if result["vip"] != true { + t.Errorf("Expected vip to be true, got '%v'", result["vip"]) + } + }) +} + +// TestBuildWhereAndOr 测试 BuildWhereAndOr 方法 +func TestBuildWhereAndOr(t *testing.T) { + var crud Crud[interface{}] + + t.Run("SimpleAND", func(t *testing.T) { + where := crud.BuildWhereAndOr(). + AND(map[string]any{"status": 1}). + AND(map[string]any{"age >": 20}). + Build() + + if where == nil { + t.Error("Expected where to not be nil") + } + }) + + t.Run("SimpleOR", func(t *testing.T) { + where := crud.BuildWhereAndOr(). + OR( + map[string]any{"name": "Alice"}, + map[string]any{"name": "Bob"}, + ). + Build() + + if where == nil { + t.Error("Expected where to not be nil") + } + }) + + t.Run("MixedANDOR", func(t *testing.T) { + where := crud.BuildWhereAndOr(). + AND(map[string]any{"status": 1}). + OR( + map[string]any{"age >": 25}, + map[string]any{"vip": true}, + ). + AND(map[string]any{"deleted_at": 0}). + Build() + + if where == nil { + t.Error("Expected where to not be nil") + } + }) + + t.Run("EmptyConditions", func(t *testing.T) { + where := crud.BuildWhereAndOr().Build() + + if where != nil { + t.Error("Expected where to be nil for empty conditions") + } + }) +} + +// TestPaginateStruct 测试 Paginate 结构体 +func TestPaginateStruct(t *testing.T) { + p := Paginate{Page: 1, Limit: 10} + + if p.Page != 1 { + t.Errorf("Expected Page to be 1, got %d", p.Page) + } + if p.Limit != 10 { + t.Errorf("Expected Limit to be 10, got %d", p.Limit) + } +} + +// TestPageInfo 测试 pageInfo 常量 +func TestPageInfo(t *testing.T) { + expectedFields := []string{ + "page", + "size", + "num", + "limit", + "pagesize", + "pageSize", + "page_size", + "pageNum", + "pagenum", + "page_num", + } + + if len(pageInfo) != len(expectedFields) { + t.Errorf("Expected pageInfo to have %d fields, got %d", len(expectedFields), len(pageInfo)) + } + + for i, field := range expectedFields { + if pageInfo[i] != field { + t.Errorf("Expected pageInfo[%d] to be '%s', got '%s'", i, field, pageInfo[i]) + } + } +} diff --git a/go.mod b/go.mod index d32b13c..7b6247a 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( golang.org/x/crypto v0.48.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/mysql v1.6.0 + gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 ) @@ -52,6 +53,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect diff --git a/go.sum b/go.sum index 47c9eca..7ef7041 100644 --- a/go.sum +++ b/go.sum @@ -99,6 +99,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -207,6 +209,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=