commit 810cdd1fda2dcea4e0fd6c34512f51c61802caab Author: Star <> Date: Tue May 12 12:26:10 2026 +0800 Initialize office module (re-engineered from gojs/office by AI) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5c7525 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.geminiignore +.gemini +.ai/ +env.json +env.yml +env.yaml +.log.meta.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a7a6844 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# CHANGELOG + +## v1.0.0 (2026-05-12) + +- 从 `gojs/office` 迁移并重构为纯 Go 实现。 +- 基础设施对齐:使用 `apigo.cc/go/cast` 和 `apigo.cc/go/file`。 +- 移除 `gojs` 和 `goja` 相关依赖。 +- 优化 `SetData` 和 `GetData` 的逻辑,支持自动扩展列。 +- 提供更简洁的 `Open` 和 `New` 入口。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8314c0b --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# office + +极简、高效的 Go Excel 处理库,基于 `excelize` 构建,符合 `@go` 设计哲学。 + +## 特性 + +- **统一 API**: 提供极简的 `Open`, `Save`, `Get`, `Set` 等操作。 +- **自动对齐**: 自动处理工作表的创建和索引。 +- **对象映射**: 支持将 `[]map[string]any` 直接写入 Excel 或从 Excel 读取。 +- **高性能**: 尽量减少内存分配和冗余操作。 + +## 快速开始 + +### 安装 + +```bash +go get apigo.cc/go/office +``` + +### 基础用法 + +```go +import "apigo.cc/go/office" + +// 创建并写入 +xls := office.New() +table := [][]any{ + {"Name", "Age"}, + {"Alice", 25}, + {"Bob", 30}, +} +xls.Set("Sheet1", table, "A1", "") +xls.Save("example.xlsx") + +// 读取 +xls2, _ := office.Open("example.xlsx") +data, _ := xls2.Get("Sheet1", "A1", "") +``` + +### 对象列表操作 + +```go +data := []map[string]any{ + {"Name": "Alice", "Age": 25}, + {"Name": "Bob", "Age": 30}, +} +xls.SetData("Users", data, "A1", "") +``` + +## API 参考 + +### 核心函数 + +- `New() *Excel`: 创建新的 Excel 对象。 +- `Open(filename string, password ...string) (*Excel, error)`: 打开现有文件。 + +### Excel 方法 + +- `Save(filename ...string) error`: 保存文件。 +- `Bytes() ([]byte, error)`: 获取字节切片。 +- `Set(sheetName string, table [][]any, start, end string) error`: 写入二维数据。 +- `Get(sheetName string, start, end string) ([][]any, error)`: 读取二维数据。 +- `SetData(sheetName string, data []map[string]any, start, end string) error`: 写入对象列表。 +- `GetData(sheetName string, start, end string) ([]map[string]any, error)`: 读取对象列表。 +- `Sheets() []string`: 获取工作表列表。 +- `RemoveSheet(sheetName string) error`: 删除工作表。 + +### 工具函数 + +- `MakeCellID(col, row int) string`: 生成单元格 ID (如 "A1")。 +- `ParseCellID(cell string) (col, row int)`: 解析单元格 ID。 +- `MakeColID(col int) string`: 生成列 ID (如 "A")。 diff --git a/excel.go b/excel.go new file mode 100644 index 0000000..235e864 --- /dev/null +++ b/excel.go @@ -0,0 +1,358 @@ +package office + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + + "apigo.cc/go/cast" + "apigo.cc/go/file" + "github.com/xuri/excelize/v2" +) + +// Excel 封装了 Excel 文件的核心操作,提供极简且高效的 API。 +type Excel struct { + filename string + password string + excel *excelize.File +} + +// New 创建一个新的 Excel 对象。 +func New() *Excel { + return &Excel{ + excel: excelize.NewFile(), + } +} + +// Open 打开一个现有的 Excel 文件。 +// 如果文件不存在,将创建一个新的 Excel 对象。 +func Open(filename string, password ...string) (*Excel, error) { + pwd := "" + if len(password) > 0 { + pwd = password[0] + } + + xls := &Excel{ + filename: filename, + password: pwd, + } + + if file.Exists(filename) { + f, err := excelize.OpenFile(filename, excelize.Options{Password: pwd}) + if err != nil { + return nil, err + } + xls.excel = f + } else { + xls.excel = excelize.NewFile() + } + + return xls, nil +} + +// Save 保存 Excel 文件。 +// 如果提供了 filename,将另存为该文件名。 +func (xls *Excel) Save(filename ...string) error { + if len(filename) > 0 && filename[0] != "" { + xls.filename = filename[0] + } + if xls.filename == "" { + return fmt.Errorf("no filename specified") + } + return xls.excel.SaveAs(xls.filename, excelize.Options{Password: xls.password}) +} + +// Bytes 将 Excel 内容写入字节切片。 +func (xls *Excel) Bytes() ([]byte, error) { + buf, err := xls.excel.WriteToBuffer() + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// Set 设置指定单元格范围的值。 +// table: 二维数组,代表行和列。 +// start: 起始单元格 ID(如 "A1"),默认为 "A1"。 +// end: 结束单元格 ID(如 "C3"),用于限制写入范围。 +func (xls *Excel) Set(sheetName string, table [][]any, start, end string) error { + sheet := xls.getOrCreateSheet(sheetName) + startX, startY := ParseCellID(start) + endX, endY := ParseCellID(end) + + for y, row := range table { + if endY > 0 && startY+y > endY { + break + } + for x, v := range row { + if endX > 0 && startX+x > endX { + break + } + cellID := MakeCellID(startX+x, startY+y) + if err := xls.excel.SetCellValue(sheet, cellID, v); err != nil { + return err + } + } + } + return nil +} + +// Get 获取指定范围的单元格数据。 +func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) { + sheet := xls.getOrCreateSheet(sheetName) + rows, err := xls.excel.GetRows(sheet) + if err != nil { + return nil, err + } + + startX, startY := ParseCellID(start) + endX, endY := ParseCellID(end) + + result := make([][]any, 0) + for y, row := range rows { + if startY > 0 && y < startY { + continue + } + if endY > 0 && y > endY { + break + } + + rowData := make([]any, 0) + for x, v := range row { + if startX > 0 && x < startX { + continue + } + if endX > 0 && x > endX { + break + } + + // 尝试根据单元格类型转换数据 + cellID := MakeCellID(x, y) + cellType, _ := xls.excel.GetCellType(sheet, cellID) + + switch cellType { + case excelize.CellTypeNumber: + if isFloat(v) { + rowData = append(rowData, cast.To[float64](v)) + } else { + rowData = append(rowData, cast.To[int64](v)) + } + case excelize.CellTypeBool: + rowData = append(rowData, cast.To[bool](v)) + case excelize.CellTypeDate: + // TODO: 更好的日期处理 + rowData = append(rowData, v) + default: + rowData = append(rowData, v) + } + } + result = append(result, rowData) + } + return result, nil +} + +// RemoveSheet 删除工作表。 +func (xls *Excel) RemoveSheet(sheetName string) error { + return xls.excel.DeleteSheet(sheetName) +} + +// Sheets 返回所有工作表名称。 +func (xls *Excel) Sheets() []string { + return xls.excel.GetSheetList() +} + +// SetColWidth 设置列宽。 +func (xls *Excel) SetColWidth(sheetName string, startCol, endCol string, width float64) error { + return xls.excel.SetColWidth(xls.getOrCreateSheet(sheetName), startCol, endCol, width) +} + +// SetColWidths 批量设置列宽。 +func (xls *Excel) SetColWidths(sheetName string, widths []float64) error { + sheet := xls.getOrCreateSheet(sheetName) + for i, w := range widths { + col := MakeColID(i) + if err := xls.excel.SetColWidth(sheet, col, col, w); err != nil { + return err + } + } + return nil +} + +// SetCellStyle 设置单元格样式。 +func (xls *Excel) SetCellStyle(sheetName string, start, end string, styleID int) error { + return xls.excel.SetCellStyle(xls.getOrCreateSheet(sheetName), start, end, styleID) +} + +// MakeStyle 创建样式并返回样式 ID。 +func (xls *Excel) MakeStyle(style *excelize.Style) (int, error) { + return xls.excel.NewStyle(style) +} + +// SetPanes 设置冻结窗格。 +func (xls *Excel) SetPanes(sheetName string, panes *excelize.Panes) error { + return xls.excel.SetPanes(xls.getOrCreateSheet(sheetName), panes) +} + +// SetAutoFilter 设置自动筛选。 +func (xls *Excel) SetAutoFilter(sheetName string, start, end string, options []excelize.AutoFilterOptions) error { + return xls.excel.AutoFilter(xls.getOrCreateSheet(sheetName), start+":"+end, options) +} + +// SetData 将对象列表写入 Excel。 +func (xls *Excel) SetData(sheetName string, data []map[string]any, start, end string) error { + table, err := xls.Get(sheetName, start, end) + if err != nil { + return err + } + + fieldIndex := map[string]int{} + fieldNum := 0 + if len(table) > 0 { + fieldNum = len(table[0]) + for i, v := range table[0] { + fieldIndex[cast.To[string](v)] = i + } + } else { + table = append(table, []any{}) + } + + for i, item := range data { + for len(table) <= i+1 { + table = append(table, make([]any, fieldNum)) + } + for k, v := range item { + idx, ok := fieldIndex[k] + if !ok { + idx = fieldNum + fieldIndex[k] = idx + fieldNum++ + // 扩展所有行以匹配新的列数 + for r := range table { + for len(table[r]) < fieldNum { + table[r] = append(table[r], "") + } + } + table[0][idx] = k + } + table[i+1][idx] = v + } + } + return xls.Set(sheetName, table, start, end) +} + +// GetData 从 Excel 中读取对象列表。 +func (xls *Excel) GetData(sheetName string, start, end string) ([]map[string]any, error) { + rows, err := xls.Get(sheetName, start, end) + if err != nil { + return nil, err + } + + if len(rows) == 0 { + return nil, nil + } + + fields := make([]string, 0) + for i, v := range rows[0] { + name := cast.To[string](v) + if name == "" { + name = MakeColID(i) + } + fields = append(fields, name) + } + + data := make([]map[string]any, 0) + for i := 1; i < len(rows); i++ { + rowMap := map[string]any{} + for j, field := range fields { + if j < len(rows[i]) { + rowMap[field] = rows[i][j] + } else { + rowMap[field] = nil + } + } + data = append(data, rowMap) + } + return data, nil +} + +// 辅助方法:获取或创建工作表 +func (xls *Excel) getOrCreateSheet(name string) string { + if name == "" { + name = "Sheet1" + } + // 如果是数字索引 + if matched, _ := regexp.MatchString(`^\d+$`, name); matched { + idx := cast.To[int](name) + return xls.excel.GetSheetName(idx) + } + + idx, _ := xls.excel.GetSheetIndex(name) + if idx == -1 { + xls.excel.NewSheet(name) + } + return name +} + +// MakeCellID 根据行列索引生成单元格 ID(如 0, 0 -> "A1")。 +func MakeCellID(col, row int) string { + return MakeColID(col) + strconv.Itoa(row+1) +} + +// MakeColID 根据列索引生成列 ID(如 0 -> "A", 26 -> "AA")。 +func MakeColID(col int) string { + colName := "" + for col >= 0 { + colName = string(rune(col%26+65)) + colName + col = col/26 - 1 + } + return colName +} + +// ParseCellID 解析单元格 ID(如 "A1" -> 0, 0)。 +func ParseCellID(cell string) (col, row int) { + if cell == "" { + return 0, 0 + } + + // 找到第一个数字的位置 + numIdx := -1 + for i, r := range cell { + if r >= '0' && r <= '9' { + numIdx = i + break + } + } + + if numIdx == -1 { + // 只有字母,当做列处理 + return parseCol(cell), 0 + } + + col = parseCol(cell[:numIdx]) + row, _ = strconv.Atoi(cell[numIdx:]) + row-- // 转为 0 索引 + if row < 0 { + row = 0 + } + return col, row +} + +func parseCol(colStr string) int { + col := 0 + for _, r := range colStr { + if r >= 'A' && r <= 'Z' { + col = col*26 + int(r-'A'+1) + } else if r >= 'a' && r <= 'z' { + col = col*26 + int(r-'a'+1) + } + } + return col - 1 +} + +var floatMatcher = regexp.MustCompile(`^[\d.]{1,18}$`) + +func isFloat(v any) bool { + s := cast.To[string](v) + return bytes.ContainsRune([]byte(s), '.') && floatMatcher.MatchString(s) +} diff --git a/excel_test.go b/excel_test.go new file mode 100644 index 0000000..20ff8a6 --- /dev/null +++ b/excel_test.go @@ -0,0 +1,119 @@ +package office + +import ( + "testing" + + "apigo.cc/go/file" +) + +func TestExcel_Basic(t *testing.T) { + filename := "test_basic.xlsx" + defer file.Remove(filename) + + xls := New() + table := [][]any{ + {"Name", "Age", "City"}, + {"Alice", 25, "New York"}, + {"Bob", 30, "London"}, + } + + err := xls.Set("Sheet1", table, "A1", "") + if err != nil { + t.Fatalf("Set failed: %v", err) + } + + err = xls.Save(filename) + if err != nil { + t.Fatalf("Save failed: %v", err) + } + + xls2, err := Open(filename) + if err != nil { + t.Fatalf("Open failed: %v", err) + } + + data, err := xls2.Get("Sheet1", "A1", "C3") + if err != nil { + t.Fatalf("Get failed: %v", err) + } + + if len(data) != 3 || len(data[1]) != 3 { + t.Errorf("Unexpected data length: %v", data) + } + + if castTo[string](data[1][0]) != "Alice" { + t.Errorf("Expected Alice, got %v", data[1][0]) + } +} + +func TestExcel_Data(t *testing.T) { + filename := "test_data.xlsx" + defer file.Remove(filename) + + xls := New() + data := []map[string]any{ + {"ID": 1, "Value": "A"}, + {"ID": 2, "Value": "B"}, + } + + err := xls.SetData("Data", data, "A1", "") + if err != nil { + t.Fatalf("SetData failed: %v", err) + } + + err = xls.Save(filename) + if err != nil { + t.Fatalf("Save failed: %v", err) + } + + xls2, err := Open(filename) + if err != nil { + t.Fatalf("Open failed: %v", err) + } + + readData, err := xls2.GetData("Data", "A1", "") + if err != nil { + t.Fatalf("GetData failed: %v", err) + } + + if len(readData) != 2 { + t.Errorf("Expected 2 rows, got %d", len(readData)) + } + + if castTo[string](readData[0]["Value"]) != "A" { + t.Errorf("Expected A, got %v", readData[0]["Value"]) + } +} + +func TestIDGeneration(t *testing.T) { + tests := []struct { + col, row int + expected string + }{ + {0, 0, "A1"}, + {25, 0, "Z1"}, + {26, 0, "AA1"}, + {0, 9, "A10"}, + } + + for _, tt := range tests { + got := MakeCellID(tt.col, tt.row) + if got != tt.expected { + t.Errorf("MakeCellID(%d, %d) = %s; want %s", tt.col, tt.row, got, tt.expected) + } + + c, r := ParseCellID(tt.expected) + if c != tt.col || r != tt.row { + t.Errorf("ParseCellID(%s) = (%d, %d); want (%d, %d)", tt.expected, c, r, tt.col, tt.row) + } + } +} + +// Helper for test assertions +func castTo[T any](v any) T { + var res T + if val, ok := v.(T); ok { + return val + } + return res +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3d5cbeb --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module apigo.cc/go/office + +go 1.25.0 + +require ( + apigo.cc/go/cast v1.3.0 + apigo.cc/go/file v1.3.0 + github.com/xuri/excelize/v2 v2.10.1 +) + +require ( + apigo.cc/go/encoding v1.3.0 // indirect + apigo.cc/go/rand v1.3.0 // indirect + apigo.cc/go/safe v1.3.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/richardlehane/mscfb v1.0.6 // indirect + github.com/richardlehane/msoleps v1.0.6 // indirect + github.com/tiendc/go-deepcopy v1.7.2 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..98d44dd --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +apigo.cc/go/cast v1.3.0 h1:ZTcLYijkqZjSWSCSpJUWMfzJYeJKbwKxquKkPrFsROQ= +apigo.cc/go/cast v1.3.0/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= +apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs= +apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo= +apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg= +apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY= +apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU= +apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= +apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok= +apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= +github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= +github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= +github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= +github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= +github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=