go-data-structures by cxuu/golang-skills
npx skills add https://github.com/cxuu/golang-skills --skill go-data-structures来源 : Effective Go
本技能涵盖 Go 的内置数据结构和分配原语。
Go 有两个分配原语:new 和 make。它们的功能不同。
new(T) 为类型 T 的新项分配归零的存储空间,并返回 *T:
p := new(SyncedBuffer) // 类型 *SyncedBuffer,已归零
var v SyncedBuffer // 类型 SyncedBuffer,已归零
零值设计:设计数据结构时,应使零值无需进一步初始化即可使用。例如:bytes.Buffer、sync.Mutex。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
// 分配后立即可用
make(T, args) 仅创建切片、映射和通道。它返回类型 T(而非 *T)的已初始化(非归零)值:
make([]int, 10, 100) // 切片:长度 10,容量 100
make(map[string]int) // 映射:立即可用
make(chan int) // 通道:立即可用
var p *[]int = new([]int) // *p == nil;很少有用
var v []int = make([]int, 100) // v 是一个可用的包含 100 个整数的切片
// 惯用写法:
v := make([]int, 100)
规则:make 仅适用于映射、切片和通道,并且不返回指针。
在一个表达式中创建并初始化结构体、数组、切片和映射:
// 使用位置字段的结构体
f := File{fd, name, nil, 0}
// 使用命名字段的结构体(顺序无关紧要,缺失的字段 = 零值)
f := &File{fd: fd, name: name}
// 零值
f := &File{} // 等价于 new(File)
// 数组、切片、映射
a := [...]string{Enone: "no error", Eio: "Eio", Einval: "invalid"}
s := []string{Enone: "no error", Eio: "Eio", Einval: "invalid"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid"}
注意:在 Go 中返回局部变量的地址是安全的——函数返回后存储空间仍然存在。
在 Go 中,数组是值(与 C 语言不同):
将一个数组赋值给另一个数组会复制所有元素
将数组传递给函数会传递一个副本,而不是指针
大小是类型的一部分:[10]int 和 [20]int 是不同的类型
func Sum(a *[3]float64) (sum float64) { for _, v := range *a { sum += v } return }
array := [...]float64{7.0, 8.5, 9.1} x := Sum(&array) // 传递指针以提高效率
建议:在大多数情况下,使用切片代替数组。
切片包装数组,为序列提供灵活、强大的接口。
切片持有对底层数组的引用。将一个切片赋值给另一个切片会使两者引用同一个数组:
func (f *File) Read(buf []byte) (n int, err error)
// 读取到较大缓冲区的前 32 个字节
n, err := f.Read(buf[0:32])
len(s):当前长度cap(s):最大长度(从切片起始到底层数组末尾)func append(slice []T, elements ...T) []T
始终赋值结果——底层数组可能改变:
x := []int{1, 2, 3}
x = append(x, 4, 5, 6)
// 将切片追加到切片
y := []int{4, 5, 6}
x = append(x, y...) // 注意 ...
方法 1:独立的内部切片(可以独立增长/收缩):
picture := make([][]uint8, YSize)
for i := range picture {
picture[i] = make([]uint8, XSize)
}
方法 2:单次分配(对于固定大小更高效):
picture := make([][]uint8, YSize)
pixels := make([]uint8, XSize*YSize)
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
关于切片内部结构的详细信息,请参阅 references/SLICES.md。
规范性:根据 Go Wiki CodeReviewComments,这是必需的。
声明空切片时,优先使用:
var t []string
而不是:
t := []string{}
前者声明一个 nil 切片,而后者是 非 nil 但长度为零的切片。它们在功能上是等效的——它们的 len 和 cap 都是零——但 nil 切片是首选的风格。
JSON 编码的例外情况: nil 切片编码为 null,而空切片 []string{} 编码为 []。当需要 JSON 数组时,使用非 nil 切片:
// nil 切片 → JSON null
var tags []string
json.Marshal(tags) // "null"
// 空切片 → JSON 数组
tags := []string{}
json.Marshal(tags) // "[]"
接口设计: 设计接口时,避免区分 nil 切片和非 nil 零长度切片,因为这可能导致细微的编程错误。
映射将键与值关联起来。键必须支持相等性比较(==)。
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
}
offset := timeZone["EST"] // -18000
不存在的键返回零值。使用 "逗号 ok" 惯用法来区分:
seconds, ok := timeZone[tz]
if !ok {
log.Println("unknown time zone:", tz)
}
// 或组合使用:
if seconds, ok := timeZone[tz]; ok {
return seconds
}
delete(timeZone, "PDT") // 即使键不存在也是安全的
使用 map[T]bool:
attended := map[string]bool{"Ann": true, "Joe": true}
if attended[person] { // 如果不在映射中则为 false
fmt.Println(person, "was at the meeting")
}
fmt 包提供了丰富的格式化打印功能。
| 函数 | 输出 |
|---|---|
Printf | 格式化到 stdout |
Sprintf | 返回格式化字符串 |
Fprintf | 格式化到 io.Writer |
Print/Println | 默认格式 |
fmt.Printf("Hello %d\n", 23)
fmt.Println("Hello", 23)
s := fmt.Sprintf("Hello %d", 23)
%v 以合理的默认格式打印任何值:
fmt.Printf("%v\n", timeZone)
// map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
对于结构体:
%v:仅值
%+v:包含字段名
%#v:完整的 Go 语法
type T struct { a int b float64 c string } t := &T{7, -2.35, "abc\tdef"}
fmt.Printf("%v\n", t) // &{7 -2.35 abc def} fmt.Printf("%+v\n", t) // &{a:7 b:-2.35 c:abc def} fmt.Printf("%#v\n", t) // &main.T{a:7, b:-2.35, c:"abc\tdef"}
| 格式 | 用途 |
|---|---|
%T | 值的类型 |
%q | 带引号的字符串 |
%x | 十六进制(字符串、字节、整数) |
定义 String() string 来控制默认格式化:
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
警告:不要在接收器上调用带有 %s 的 Sprintf——会导致无限递归:
// 错误:无限递归
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m)
}
// 正确:转换为基本类型
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m))
}
常量在编译时创建,只能是数字、字符、字符串或布尔值。
iota 创建枚举常量:
type ByteSize float64
const (
_ = iota // 忽略第一个值 (0)
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
)
与 String() 结合使用以实现自动格式化:
func (b ByteSize) String() string {
switch {
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
// ... 等等
}
return fmt.Sprintf("%.2fB", b)
}
建议性:这是来自 Go Wiki CodeReviewComments 的最佳实践建议。
为避免意外的别名,从另一个包复制结构体时要小心。例如,bytes.Buffer 包含一个 []byte 切片。如果复制一个 Buffer,副本中的切片可能会成为原始数组中数组的别名,导致后续的方法调用产生意想不到的效果。
// 危险:复制 bytes.Buffer
var buf1 bytes.Buffer
buf1.WriteString("hello")
buf2 := buf1 // buf2 的内部切片可能成为 buf1 数组的别名!
buf2.WriteString(" world") // 可能会意外地影响 buf1
通用规则: 如果类型 T 的方法与指针类型 *T 关联,则不要复制类型 T 的值。
这适用于标准库和第三方包中的许多类型:
bytes.Buffer
sync.Mutex、sync.WaitGroup、sync.Cond
包含上述类型的类型
// 错误:复制互斥锁 var mu sync.Mutex mu2 := mu // 复制互斥锁几乎总是一个错误
// 正确:使用指针或小心嵌入 type SafeCounter struct { mu sync.Mutex count int }
// 通过指针传递,而不是通过值 func increment(sc *SafeCounter) { sc.mu.Lock() sc.count++ sc.mu.Unlock() }
| 主题 | 要点 |
|---|---|
new(T) | 返回 *T,已归零 |
make(T) | 仅适用于切片、映射、通道;返回 T,已初始化 |
| 数组 | 是值,不是引用;大小是类型的一部分 |
| 切片 | 引用底层数组;使用 append |
| 映射 | 键必须支持 ==;使用逗号-ok 测试存在性 |
| 复制 | 如果方法定义在 *T 上,不要复制 T;注意别名问题 |
%v | 任何值的默认格式 |
%+v | 带字段名的结构体 |
%#v | 完整的 Go 语法 |
iota | 枚举常量 |
每周安装次数
129
代码仓库
GitHub 星标数
34
首次出现
2026年1月27日
安全审计
已安装于
github-copilot122
gemini-cli121
codex121
kimi-cli120
amp120
opencode120
Source : Effective Go
This skill covers Go's built-in data structures and allocation primitives.
Go has two allocation primitives: new and make. They do different things.
new(T) allocates zeroed storage for a new item of type T and returns *T:
p := new(SyncedBuffer) // type *SyncedBuffer, zeroed
var v SyncedBuffer // type SyncedBuffer, zeroed
Zero-value design : Design data structures so the zero value is useful without further initialization. Examples: bytes.Buffer, sync.Mutex.
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
// Ready to use immediately upon allocation
make(T, args) creates slices, maps, and channels only. It returns an initialized (not zeroed) value of type T (not *T):
make([]int, 10, 100) // slice: length 10, capacity 100
make(map[string]int) // map: ready to use
make(chan int) // channel: ready to use
var p *[]int = new([]int) // *p == nil; rarely useful
var v []int = make([]int, 100) // v is a usable slice of 100 ints
// Idiomatic:
v := make([]int, 100)
Rule : make applies only to maps, slices, and channels and does not return a pointer.
Create and initialize structs, arrays, slices, and maps in one expression:
// Struct with positional fields
f := File{fd, name, nil, 0}
// Struct with named fields (order doesn't matter, missing = zero)
f := &File{fd: fd, name: name}
// Zero value
f := &File{} // equivalent to new(File)
// Arrays, slices, maps
a := [...]string{Enone: "no error", Eio: "Eio", Einval: "invalid"}
s := []string{Enone: "no error", Eio: "Eio", Einval: "invalid"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid"}
Note : It's safe to return the address of a local variable in Go—the storage survives after the function returns.
Arrays are values in Go (unlike C):
Assigning one array to another copies all elements
Passing an array to a function passes a copy, not a pointer
The size is part of the type: [10]int and [20]int are distinct
func Sum(a *[3]float64) (sum float64) { for _, v := range *a { sum += v } return }
array := [...]float64{7.0, 8.5, 9.1} x := Sum(&array) // Pass pointer for efficiency
Recommendation : Use slices instead of arrays in most cases.
Slices wrap arrays to provide a flexible, powerful interface to sequences.
Slices hold references to an underlying array. Assigning one slice to another makes both refer to the same array:
func (f *File) Read(buf []byte) (n int, err error)
// Read into first 32 bytes of larger buffer
n, err := f.Read(buf[0:32])
len(s): current lengthcap(s): maximum length (from start of slice to end of underlying array)func append(slice []T, elements ...T) []T
Always assign the result —the underlying array may change:
x := []int{1, 2, 3}
x = append(x, 4, 5, 6)
// Append a slice to a slice
y := []int{4, 5, 6}
x = append(x, y...) // Note the ...
Method 1 : Independent inner slices (can grow/shrink independently):
picture := make([][]uint8, YSize)
for i := range picture {
picture[i] = make([]uint8, XSize)
}
Method 2 : Single allocation (more efficient for fixed sizes):
picture := make([][]uint8, YSize)
pixels := make([]uint8, XSize*YSize)
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
For detailed slice internals, see references/SLICES.md.
Normative : This is required per Go Wiki CodeReviewComments.
When declaring an empty slice, prefer:
var t []string
over:
t := []string{}
The former declares a nil slice , while the latter is non-nil but zero-length. They are functionally equivalent—their len and cap are both zero—but the nil slice is the preferred style.
Exception for JSON encoding: A nil slice encodes to null, while an empty slice []string{} encodes to []. Use non-nil when you need a JSON array:
// nil slice → JSON null
var tags []string
json.Marshal(tags) // "null"
// empty slice → JSON array
tags := []string{}
json.Marshal(tags) // "[]"
Interface design: When designing interfaces, avoid making a distinction between a nil slice and a non-nil zero-length slice, as this can lead to subtle programming errors.
Maps associate keys with values. Keys must support equality (==).
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
}
offset := timeZone["EST"] // -18000
An absent key returns the zero value. Use the "comma ok" idiom to distinguish:
seconds, ok := timeZone[tz]
if !ok {
log.Println("unknown time zone:", tz)
}
// Or combined:
if seconds, ok := timeZone[tz]; ok {
return seconds
}
delete(timeZone, "PDT") // Safe even if key doesn't exist
Use map[T]bool:
attended := map[string]bool{"Ann": true, "Joe": true}
if attended[person] { // false if not in map
fmt.Println(person, "was at the meeting")
}
The fmt package provides rich formatted printing.
| Function | Output |
|---|---|
Printf | Formatted to stdout |
Sprintf | Returns formatted string |
Fprintf | Formatted to io.Writer |
Print/Println | Default format |
fmt.Printf("Hello %d\n", 23)
fmt.Println("Hello", 23)
s := fmt.Sprintf("Hello %d", 23)
%v prints any value with a reasonable default:
fmt.Printf("%v\n", timeZone)
// map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
For structs:
%v: values only
%+v: with field names
%#v: full Go syntax
type T struct { a int b float64 c string } t := &T{7, -2.35, "abc\tdef"}
fmt.Printf("%v\n", t) // &{7 -2.35 abc def} fmt.Printf("%+v\n", t) // &{a:7 b:-2.35 c:abc def} fmt.Printf("%#v\n", t) // &main.T{a:7, b:-2.35, c:"abc\tdef"}
| Format | Purpose |
|---|---|
%T | Type of value |
%q | Quoted string |
%x | Hex (strings, bytes, ints) |
Define String() string to control default formatting:
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
Warning : Don't call Sprintf with %s on the receiver—infinite recursion:
// Bad: infinite recursion
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m)
}
// Good: convert to basic type
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m))
}
Constants are created at compile time and can only be numbers, characters, strings, or booleans.
iota creates enumerated constants:
type ByteSize float64
const (
_ = iota // ignore first value (0)
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
)
Combine with String() for automatic formatting:
func (b ByteSize) String() string {
switch {
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
// ... etc
}
return fmt.Sprintf("%.2fB", b)
}
Advisory : This is a best practice recommendation from Go Wiki CodeReviewComments.
To avoid unexpected aliasing, be careful when copying a struct from another package. For example, bytes.Buffer contains a []byte slice. If you copy a Buffer, the slice in the copy may alias the array in the original, causing subsequent method calls to have surprising effects.
// Dangerous: copying a bytes.Buffer
var buf1 bytes.Buffer
buf1.WriteString("hello")
buf2 := buf1 // buf2's internal slice may alias buf1's array!
buf2.WriteString(" world") // May affect buf1 unexpectedly
General rule: Do not copy a value of type T if its methods are associated with the pointer type *T.
This applies to many types in the standard library and third-party packages:
bytes.Buffer
sync.Mutex, sync.WaitGroup, sync.Cond
Types containing the above
// Bad: copying a mutex var mu sync.Mutex mu2 := mu // Copying a mutex is almost always a bug
// Good: use pointers or embed carefully type SafeCounter struct { mu sync.Mutex count int }
// Pass by pointer, not by value func increment(sc *SafeCounter) { sc.mu.Lock() sc.count++ sc.mu.Unlock() }
| Topic | Key Point |
|---|---|
new(T) | Returns *T, zeroed |
make(T) | Slices, maps, channels only; returns T, initialized |
| Arrays | Values, not references; size is part of type |
| Slices | Reference underlying array; use append |
| Maps | Key must support ==; use comma-ok for presence |
| Copying | Don't copy if methods are on ; beware aliasing |
Weekly Installs
129
Repository
GitHub Stars
34
First Seen
Jan 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot122
gemini-cli121
codex121
kimi-cli120
amp120
opencode120
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
T*T%v | Default format for any value |
%+v | Struct with field names |
%#v | Full Go syntax |
iota | Enumerated constants |