go-data-structures

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Data Structures

Go语言数据结构

Source: Effective Go
This skill covers Go's built-in data structures and allocation primitives.

来源:Effective Go
本技能涵盖Go语言的内置数据结构和内存分配原语。

Allocation: new vs make

内存分配:new 与 make

Go has two allocation primitives:
new
and
make
. They do different things.
Go语言有两个内存分配原语:
new
make
,它们的功能各不相同。

new

new

new(T)
allocates zeroed storage for a new item of type
T
and returns
*T
:
go
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
.
go
type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}
// Ready to use immediately upon allocation
new(T)
为类型
T
的新实例分配零值存储空间,并返回
*T
类型的指针:
go
p := new(SyncedBuffer)  // 类型为*SyncedBuffer,已初始化为零值
var v SyncedBuffer      // 类型为SyncedBuffer,已初始化为零值
零值设计原则:设计数据结构时,应确保其零值无需进一步初始化即可直接使用。例如:
bytes.Buffer
sync.Mutex
go
type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}
// 分配后即可直接使用

make

make

make(T, args)
creates slices, maps, and channels only. It returns an initialized (not zeroed) value of type
T
(not
*T
):
go
make([]int, 10, 100)  // slice: length 10, capacity 100
make(map[string]int)  // map: ready to use
make(chan int)        // channel: ready to use
make(T, args)
仅用于创建切片、映射和通道。它返回类型
T
已初始化(非零值)实例,而非
*T
go
make([]int, 10, 100)  // 切片:长度10,容量100
make(map[string]int)  // 映射:可直接使用
make(chan int)        // 通道:可直接使用

The Difference

两者的区别

go
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.

go
var p *[]int = new([]int)       // *p == nil;几乎无实用价值
var v  []int = make([]int, 100) // v是可直接使用的、包含100个int的切片

// 惯用写法:
v := make([]int, 100)
规则
make
仅适用于映射、切片和通道,且不返回指针。

Composite Literals

复合字面量

Create and initialize structs, arrays, slices, and maps in one expression:
go
// 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.

可通过单个表达式创建并初始化结构体、数组、切片和映射:
go
// 使用位置参数初始化结构体
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语言中,返回局部变量的地址是安全的——该变量的存储空间在函数返回后依然保留。

Arrays

数组

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
go
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.

在Go语言中,数组是值类型(与C语言不同):
  • 将一个数组赋值给另一个数组时,会复制所有元素
  • 将数组传递给函数时,传递的是副本而非指针
  • 数组大小是其类型的一部分:
    [10]int
    [20]int
    是不同的类型
go
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)  // 传递指针以提升效率
建议:大多数情况下优先使用切片而非数组。

Slices

切片

Slices wrap arrays to provide a flexible, powerful interface to sequences.
切片是对数组的封装,为序列数据提供灵活、强大的操作接口。

Slice Basics

切片基础

Slices hold references to an underlying array. Assigning one slice to another makes both refer to the same array:
go
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])
切片持有对底层数组的引用。将一个切片赋值给另一个切片时,两者会指向同一个底层数组:
go
func (f *File) Read(buf []byte) (n int, err error)

// 将数据读取到更大缓冲区的前32个字节中
n, err := f.Read(buf[0:32])

Length and Capacity

长度与容量

  • len(s)
    : current length
  • cap(s)
    : maximum length (from start of slice to end of underlying array)
  • len(s)
    :切片的当前长度
  • cap(s)
    :切片的最大长度(从切片起始位置到底层数组末尾的长度)

The append Function

append函数

go
func append(slice []T, elements ...T) []T
Always assign the result—the underlying array may change:
go
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 ...
go
func append(slice []T, elements ...T) []T
务必接收返回值——因为底层数组可能会发生变化:
go
x := []int{1, 2, 3}
x = append(x, 4, 5, 6)

// 将一个切片追加到另一个切片
y := []int{4, 5, 6}
x = append(x, y...)  // 注意末尾的...

Two-Dimensional Slices

二维切片

Method 1: Independent inner slices (can grow/shrink independently):
go
picture := make([][]uint8, YSize)
for i := range picture {
    picture[i] = make([]uint8, XSize)
}
Method 2: Single allocation (more efficient for fixed sizes):
go
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.
方法1:独立的内层切片(可各自独立增长/缩小):
go
picture := make([][]uint8, YSize)
for i := range picture {
    picture[i] = make([]uint8, XSize)
}
方法2:单次内存分配(对于固定大小的场景更高效):
go
picture := make([][]uint8, YSize)
pixels := make([]uint8, XSize*YSize)
for i := range picture {
    picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
如需了解切片的详细内部实现,请参阅references/SLICES.md

Declaring Empty Slices

声明空切片

Normative: This is required per Go Wiki CodeReviewComments.
When declaring an empty slice, prefer:
go
var t []string
over:
go
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:
go
// 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.

规范要求:根据Go Wiki的CodeReviewComments,这是必须遵循的规范。
声明空切片时,优先使用:
go
var t []string
而非:
go
t := []string{}
前者声明的是nil切片,后者是非nil但长度为0的切片。两者在功能上等价——它们的
len
cap
均为0,但nil切片是更推荐的写法。
JSON编码例外:nil切片会被编码为
null
,而空切片
[]string{}
会被编码为
[]
。当你需要输出JSON数组时,请使用非nil的空切片:
go
// nil切片 → JSON null
var tags []string
json.Marshal(tags)  // "null"

// 空切片 → JSON数组
tags := []string{}
json.Marshal(tags)  // "[]"
接口设计:设计接口时,应避免区分nil切片和非nil的零长度切片,否则可能导致微妙的编程错误。

Maps

映射

Maps associate keys with values. Keys must support equality (
==
).
映射用于关联键和值。键必须支持相等运算符(
==
)。

Creating and Using Maps

创建与使用映射

go
var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
}

offset := timeZone["EST"]  // -18000
go
var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
}

offset := timeZone["EST"]  // -18000

Testing for Presence

检查键是否存在

An absent key returns the zero value. Use the "comma ok" idiom to distinguish:
go
seconds, ok := timeZone[tz]
if !ok {
    log.Println("unknown time zone:", tz)
}

// Or combined:
if seconds, ok := timeZone[tz]; ok {
    return seconds
}
不存在的键会返回对应类型的零值。使用"逗号ok"惯用法来区分键不存在和键值为零值的情况:
go
seconds, ok := timeZone[tz]
if !ok {
    log.Println("unknown time zone:", tz)
}

// 或合并写法:
if seconds, ok := timeZone[tz]; ok {
    return seconds
}

Deleting Entries

删除映射条目

go
delete(timeZone, "PDT")  // Safe even if key doesn't exist
go
delete(timeZone, "PDT")  // 即使键不存在,该操作也是安全的

Implementing a Set

实现集合

Use
map[T]bool
:
go
attended := map[string]bool{"Ann": true, "Joe": true}

if attended[person] {  // false if not in map
    fmt.Println(person, "was at the meeting")
}

使用
map[T]bool
来实现集合:
go
attended := map[string]bool{"Ann": true, "Joe": true}

if attended[person] {  // 若不在映射中则返回false
    fmt.Println(person, "was at the meeting")
}

Printing

格式化输出

The
fmt
package provides rich formatted printing.
fmt
包提供了丰富的格式化输出功能。

Basic Functions

基础函数

FunctionOutput
Printf
Formatted to stdout
Sprintf
Returns formatted string
Fprintf
Formatted to io.Writer
Print/Println
Default format
go
fmt.Printf("Hello %d\n", 23)
fmt.Println("Hello", 23)
s := fmt.Sprintf("Hello %d", 23)
函数输出目标
Printf
格式化输出到标准输出
Sprintf
返回格式化后的字符串
Fprintf
格式化输出到io.Writer
Print/Println
使用默认格式输出
go
fmt.Printf("Hello %d\n", 23)
fmt.Println("Hello", 23)
s := fmt.Sprintf("Hello %d", 23)

The %v Format

%v 格式符

%v
prints any value with a reasonable default:
go
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
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"}
%v
会以合理的默认格式打印任意值:
go
fmt.Printf("%v\n", timeZone)
// map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
对于结构体:
  • %v
    :仅打印字段值
  • %+v
    :打印字段名和对应值
  • %#v
    :打印完整的Go语言语法表示
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"}

Other Useful Formats

其他实用格式符

FormatPurpose
%T
Type of value
%q
Quoted string
%x
Hex (strings, bytes, ints)
格式符用途
%T
打印值的类型
%q
打印带引号的字符串
%x
以十六进制打印(字符串、字节、整数)

The Stringer Interface

Stringer接口

Define
String() string
to control default formatting:
go
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:
go
// 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))
}

通过定义
String() string
方法,可以控制类型的默认格式化输出:
go
func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
警告:不要在方法中使用
%s
格式化当前接收器,否则会导致无限递归:
go
// 错误写法:无限递归
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m)
}

// 正确写法:转换为基础类型
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m))
}

Constants and iota

常量与iota

Constants are created at compile time and can only be numbers, characters, strings, or booleans.
常量在编译时创建,只能是数字、字符、字符串或布尔值。

iota Enumerator

iota 枚举器

iota
creates enumerated constants:
go
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:
go
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)
}

iota
用于创建枚举常量:
go
type ByteSize float64

const (
    _           = iota // 忽略第一个值(0)
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
)
结合
String()
方法实现自动格式化:
go
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)
}

Copying

复制注意事项

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.
go
// 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
go
// 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()
}

最佳实践建议:来自Go Wiki的CodeReviewComments。
为避免意外的别名问题,当复制来自其他包的结构体时需格外小心。例如,
bytes.Buffer
包含一个
[]byte
切片。如果复制
Buffer
实例,副本中的切片可能会与原实例的底层数组产生别名,导致后续方法调用出现意外结果。
go
// 危险操作:复制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
  • 包含上述类型的自定义类型
go
// 错误写法:复制互斥锁
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()
}

Quick Reference

快速参考

TopicKey Point
new(T)
Returns
*T
, zeroed
make(T)
Slices, maps, channels only; returns
T
, initialized
ArraysValues, not references; size is part of type
SlicesReference underlying array; use
append
MapsKey must support
==
; use comma-ok for presence
CopyingDon't copy
T
if methods are on
*T
; beware aliasing
%v
Default format for any value
%+v
Struct with field names
%#v
Full Go syntax
iota
Enumerated constants
主题核心要点
new(T)
返回
*T
类型的零值实例
make(T)
仅适用于切片、映射、通道;返回
T
类型的已初始化实例
数组值类型,非引用类型;大小是类型的一部分
切片引用底层数组;使用
append
进行追加操作
映射键必须支持
==
;使用逗号ok写法检查键是否存在
复制若方法关联到
*T
,不要复制
T
;警惕别名问题
%v
任意值的默认格式化符
%+v
打印带字段名的结构体
%#v
打印完整的Go语言语法表示
iota
用于定义枚举常量

See Also

相关链接

  • go-style-core - Core Go style principles
  • go-control-flow - Control structures including range
  • go-interfaces - Interface patterns and embedding
  • go-concurrency - Channels and goroutines
  • go-style-core - Go语言核心编码风格原则
  • go-control-flow - 控制结构(包括range循环)
  • go-interfaces - 接口模式与嵌入
  • go-concurrency - 通道与goroutine