跳转到内容

附录 D Go 速查表

这份表给写过别的语言、要快速想起 Go 怎么写的人:按节查,不从头读。每个条目是一行说明加一段代码,只讲怎么写,不讲为什么;需要原理的条目在说明末尾注明正文章节。代码以 Go 1.23 为准,用到更新的特性时在说明里标出版本,本机用 Go 1.27 运行。go 代码块都是完整的单文件程序,存成 main.go 后用 go run main.go 运行,每段都能直接运行,注释里写的是运行结果。用 // 文件名 分成几段的代码块属于一个模块,按标注的文件名放进模块目录里再运行;在模块里运行时,go.mod 的 go 行不能低于条目标的版本。map 的遍历顺序每次都可能不同,用 fmt.Println 打印整个 map 时会按键排序,所以这类输出是固定的。标“第三方”的工具和包需要另外安装。

官方下载地址:https://go.dev/dl/

查看版本;go env 后面跟变量名,只看这几个环境变量

终端窗口
go version
go env GOROOT GOPATH GOTOOLCHAIN

在代码里查看运行时版本和平台

package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.Version()) // go1.27.0
fmt.Println(runtime.GOOS, runtime.GOARCH) // windows amd64
}

Windows 上用 winget 安装

终端窗口
winget install --id GoLang.Go --exact

Linux 上用官方压缩包:解压到 /usr/local/go,再把它的 bin 加进 PATH;macOS 用官网的 .pkg 安装包

终端窗口
curl -LO https://go.dev/dl/go1.27.0.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.27.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

多版本不用另装管理工具:go 命令自带工具链切换,GOTOOLCHAIN 指定版本时自动下载那个版本来运行

终端窗口
GOTOOLCHAIN=go1.26.0 go version

项目里固定版本:go.mod 的 go 行是最低要求,也决定能用哪些语言特性;本机工具链比它旧时,go 命令自动下载够新的版本

终端窗口
go get go@1.26.0
go get toolchain@go1.27.0

只用本机装的版本,不自动下载

终端窗口
go env -w GOTOOLCHAIN=local

运行单文件:先编译再运行,产物不留在当前目录(1.24 起进构建缓存),详见第 2 章

终端窗口
go run main.go

最小的程序:package main 加 func main,可执行程序必须在 main 包里

package main
import "fmt"
func main() {
fmt.Println("你好") // 你好
}

运行当前目录的 main 包,目录里的 .go 文件一起编译

终端窗口
go run .

没有交互式环境(REPL),用 go run 单文件代替,详见第 2 章;只试几行也可以用官方在线环境 https://go.dev/play/

终端窗口
go run scratch.go

给程序传参数:os.Args[0] 是程序本身,参数从下标 1 开始

// 运行:go run main.go a b
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println(os.Args[1:]) // [a b]
fmt.Println(len(os.Args) - 1) // 2
}

解析命令行选项:标准库 flag;选项要写在位置参数前面

// 运行:go run main.go -name 小明 -times 2 extra
package main
import (
"flag"
"fmt"
)
func main() {
name := flag.String("name", "世界", "要问候的人")
times := flag.Int("times", 1, "次数")
flag.Parse()
for range *times {
fmt.Printf("你好,%s\n", *name) // 打印两次"你好,小明"
}
fmt.Println(flag.Args()) // [extra]
}

编译成可执行文件:-o 指定文件名,运行时一起链接进去,目标机器不用装 Go,详见第 2 章

终端窗口
go build -o app .
./app # Windows 上写 go build -o app.exe . 再运行 .\app.exe

交叉编译:设 GOOS 和 GOARCH 两个环境变量(bash 写法),详见第 2 章

终端窗口
GOOS=linux GOARCH=arm64 go build -o app-linux .

安装命令行程序到 GOBIN(默认是 $GOPATH/bin):当前模块的,或者按“路径@版本”装别人的

终端窗口
go install .
go install golang.org/x/tools/cmd/goimports@latest

读环境变量,不存在或为空时给默认值:cmp.Or 返回第一个非零值

package main
import (
"cmp"
"fmt"
"os"
)
func main() {
fmt.Println(cmp.Or(os.Getenv("APP_MODE"), "dev")) // dev
}

以非零退出码结束:os.Exit,已经登记的 defer 不会执行

package main
import (
"fmt"
"os"
)
func main() {
defer fmt.Println("不会打印")
fmt.Fprintln(os.Stderr, "配置文件不存在")
os.Exit(1)
}

新建模块:在项目目录里生成 go.mod;模块路径一般写代码仓库的地址,详见第 11 章

终端窗口
mkdir demo && cd demo
go mod init example.com/demo

go.mod:模块路径、Go 版本、依赖(本机 Go 1.27 生成,加了一个依赖之后)

go.mod
module example.com/demo
go 1.27.0
require github.com/google/go-cmp v0.7.0

加依赖(第三方):go get 写进 go.mod,校验和写进 go.sum;不写版本时取最新的正式版

终端窗口
go get github.com/google/go-cmp

指定版本、升级全部依赖、移除依赖

终端窗口
go get github.com/google/go-cmp@v0.6.0
go get -u ./...
go get github.com/google/go-cmp@none

整理依赖:按代码里实际的 import 增删 require,补全 go.sum

终端窗口
go mod tidy

没有单独的锁文件:go.mod 按最小版本选择记下确切版本,go.sum 记模块内容的哈希,两个都提交,详见第 11 章

终端窗口
git add go.mod go.sum

按 go.mod 下载全部依赖;build、run、test 缺什么会自动下载,一般不用单独运行

终端窗口
go mod download

列出所有依赖;-u 同时列出可升级的版本

终端窗口
go list -m all
go list -m -u all

没有 package.json 那样的 scripts,用 go 的子命令代替;项目要用的命令行工具用 tool 指令记进 go.mod(1.24 起),再用 go tool 运行

终端窗口
go get -tool golang.org/x/tools/cmd/stringer
go tool stringer -type=Color

同时改几个本地模块:用工作区文件 go.work 把它们放在一起

终端窗口
go work init ./app ./lib

函数里用 := 声明并推断类型,类型定下就不能变;var 写出类型,详见第 3 章

package main
import "fmt"
func main() {
name := "Ada"
var count int
count += 1
fmt.Println(name, count) // Ada 1
}

声明了不赋值就是零值:数字是 0,字符串是空串,布尔是 false,指针是 nil,详见第 6 章

package main
import "fmt"
func main() {
var n int
var s string
var ok bool
var p *int
fmt.Printf("%d %q %v %v\n", n, s, ok, p == nil) // 0 "" false true
}

多重赋值、交换两个变量

package main
import "fmt"
func main() {
x, y := 1, 2
x, y = y, x
fmt.Println(x, y) // 2 1
}

:= 左边至少要有一个新变量,已有的变量只是被赋值

package main
import (
"fmt"
"strconv"
)
func main() {
a, err := strconv.Atoi("1")
b, err := strconv.Atoi("2")
fmt.Println(a+b, err) // 3 <nil>
}

const 是编译期常量,只能是数字、字符串、布尔;无类型常量用在哪里就取哪种类型,详见第 4 章

package main
import "fmt"
const timeout = 5
const prefix = "log-"
func main() {
var ms int64 = timeout * 1000
var sec float64 = timeout
fmt.Println(prefix, ms, sec) // log- 5000 5
}

一组相关的常量用 const 块加 iota,从 0 开始每行加 1,详见第 15 章

package main
import "fmt"
const (
Small = iota
Medium
Large
)
func main() {
fmt.Println(Small, Medium, Large) // 0 1 2
}

没有只读变量和只读字段,用 const 或不导出的字段加只读方法代替,详见第 4 章

package main
import "fmt"
type Config struct{ port int }
func (c Config) Port() int { return c.port }
func main() {
c := Config{port: 8080}
fmt.Println(c.Port()) // 8080
}

作用域是块级的;内层块用 := 声明同名变量会遮住外层的,外层的值不变

package main
import "fmt"
func main() {
x := 1
if x > 0 {
x := 2
fmt.Println(x) // 2
}
fmt.Println(x) // 1
}

声明了没用的局部变量、导入了没用的包都是编译错误(declared and not used);不要的返回值赋给 _

package main
import (
"fmt"
"strconv"
)
func main() {
_, err := strconv.Atoi("x")
fmt.Println(err != nil) // true
}

包级变量在 main 之前初始化,包里的函数都能读写

package main
import "fmt"
var counter int
func bump() { counter++ }
func main() {
bump()
bump()
fmt.Println(counter) // 2
}

字符串转整数:strconv.Atoi 返回值和错误;指定进制用 ParseInt

package main
import (
"fmt"
"strconv"
)
func main() {
n, err := strconv.Atoi("42")
fmt.Println(n, err) // 42 <nil>
_, err = strconv.Atoi("42px")
fmt.Println(err) // strconv.Atoi: parsing "42px": invalid syntax
h, _ := strconv.ParseInt("ff", 16, 64)
fmt.Println(h) // 255
}

整数转字符串用 strconv.Itoa(string(65) 得到的是 “A”,见“常见坑”)

package main
import (
"fmt"
"strconv"
)
func main() {
fmt.Println(strconv.Itoa(42) + "!") // 42!
}

小数和字符串互转:ParseFloat、FormatFloat;strconv 和 fmt 不看区域设置,小数点始终是点号

package main
import (
"fmt"
"strconv"
)
func main() {
f, _ := strconv.ParseFloat("3.5", 64)
fmt.Println(f * 3) // 10.5
fmt.Println(strconv.FormatFloat(f, 'f', 2, 64)) // 3.50
}

没有隐式的数值转换:int 和 float64 混算要显式转换;浮点转整数向零截断,向下取整用 math.Floor

package main
import (
"fmt"
"math"
)
func main() {
n := 7
fmt.Println(float64(n) / 2) // 3.5
x := -3.9
fmt.Println(int(x), math.Floor(x)) // -3 -4
}

整数除法向零截断,% 的符号跟被除数;有一边是浮点数才得小数

package main
import "fmt"
func main() {
fmt.Println(7/2, -7/2, -7%3) // 3 -3 -1
fmt.Println(7 / 2.0) // 3.5
}

乘方用 math.Pow,参数和结果都是 float64;2 的整数次方用移位

package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Pow(2, 10)) // 1024
fmt.Println(1 << 10) // 1024
}

math.Round 是四舍五入(0.5 远离零);“四舍六入五成双”用 math.RoundToEven

package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Round(2.5), math.RoundToEven(2.5)) // 3 2
}

整数溢出静默回绕,运行时不报错

package main
import (
"fmt"
"math"
)
func main() {
var x int32 = math.MaxInt32
x++
fmt.Println(x) // -2147483648
}

大整数用 math/big

package main
import (
"fmt"
"math/big"
)
func main() {
n := new(big.Int).Exp(big.NewInt(2), big.NewInt(100), nil)
fmt.Println(n) // 1267650600228229401496703205376
}

float64 有二进制误差;标准库没有十进制小数类型,金额用整数存“分”代替

package main
import "fmt"
func main() {
a, b := 0.1, 0.2
fmt.Println(a+b == 0.3) // false
cents := 10 + 20
fmt.Println(cents == 30) // true
}

bool 和整数不能互转,条件必须是 bool;字符串转布尔用 strconv.ParseBool

package main
import (
"fmt"
"strconv"
)
func main() {
count := 3
fmt.Println(count != 0) // true
b, _ := strconv.ParseBool("TRUE")
fmt.Println(b) // true
}

判断类型:%T 打印类型名;接口里的值用类型断言取出,带 ok 的写法失败时不 panic,详见第 3 章

package main
import "fmt"
func main() {
var v any = 42
fmt.Printf("%T\n", v) // int
if n, ok := v.(int); ok {
fmt.Println(n + 1) // 43
}
_, isStr := v.(string)
fmt.Println(isStr) // false
}

数字字面量:下划线分隔、十六进制、八进制、二进制

package main
import "fmt"
func main() {
fmt.Println(1_000_000, 0xFF, 0o17, 0b1010) // 1000000 255 15 10
}

byte 是 uint8,rune 是 int32,表示一个码点,可以和整数互转,详见第 14 章

package main
import "fmt"
func main() {
fmt.Println('A', string(rune('A'+2))) // 65 C
}

拼接少量用 +,拼接一列用 strings.Join

package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println("a" + "b") // ab
fmt.Println(strings.Join([]string{"2026", "10", "08"}, "-")) // 2026-10-08
}

没有插值语法,用 fmt.Sprintf 代替:%v 通用、%q 带引号、%5d 宽度、%-5s 左对齐、%05d 补零、%.2f 小数位

package main
import "fmt"
func main() {
name, age := "小明", 18
fmt.Println(fmt.Sprintf("%s 今年 %d 岁", name, age)) // 小明 今年 18 岁
fmt.Printf("%v|%q|%5d|%-5s|%05d|%.2f|\n", []int{1, 2}, "hi", 42, "左", 42, 3.14159)
// [1 2]|"hi"| 42|左 |00042|3.14|
}

循环里拼很多段用 strings.Builder

package main
import (
"fmt"
"strings"
)
func main() {
var sb strings.Builder
for i := range 3 {
fmt.Fprintf(&sb, "%d,", i)
}
fmt.Println(strings.TrimSuffix(sb.String(), ",")) // 0,1,2
}

下标和切片按字节算;含中文时先转成 []rune 再按字符取,详见第 14 章

package main
import "fmt"
func main() {
s := "Go语言"
fmt.Println(s[:2]) // Go
r := []rune(s)
fmt.Println(string(r[2:]), len(r)) // 语言 4
}

查找和比较:Contains、Index、HasPrefix;忽略大小写比较用 EqualFold

package main
import (
"fmt"
"strings"
)
func main() {
s := "hello world"
fmt.Println(strings.Contains(s, "wor")) // true
fmt.Println(strings.Index(s, "o")) // 4
fmt.Println(strings.Index(s, "z")) // -1
fmt.Println(strings.HasPrefix(s, "he")) // true
fmt.Println(strings.EqualFold("Hello", "hello")) // true
}

替换:ReplaceAll 替换全部,Replace 的最后一个参数是次数

package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ReplaceAll("a-b-c", "-", "/")) // a/b/c
fmt.Println(strings.Replace("a-b-c", "-", "/", 1)) // a/b-c
}

用正则替换和提取:regexp,模式写在反引号里不用转义反斜杠

package main
import (
"fmt"
"regexp"
)
func main() {
re := regexp.MustCompile(`\d+`)
s := "订单 12 和 345"
fmt.Println(re.ReplaceAllString(s, "#")) // 订单 # 和 #
fmt.Println(re.FindAllString(s, -1)) // [12 345]
}

分割:Split 保留空项;Fields 按连续空白分割,不留空项

package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("%q\n", strings.Split("x,y,,z", ",")) // ["x" "y" "" "z"]
fmt.Printf("%q\n", strings.Fields(" a b c ")) // ["a" "b" "c"]
}

只在第一个分隔符处切开:strings.Cut 返回前后两段和有没有找到

package main
import (
"fmt"
"strings"
)
func main() {
key, value, ok := strings.Cut("port=8080", "=")
fmt.Println(key, value, ok) // port 8080 true
}

逐段遍历、不建切片:strings.SplitSeq、strings.Lines 返回迭代器(1.24 起),Lines 给出的每行带着换行符

package main
import (
"fmt"
"strings"
)
func main() {
for part := range strings.SplitSeq("a,b", ",") {
fmt.Println(part) // 依次打印 a、b
}
for line := range strings.Lines("x\ny\n") {
fmt.Printf("%q\n", line) // 依次打印 "x\n"、"y\n"
}
}

大小写转换

package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("hello"), strings.ToLower("World")) // HELLO world
}

去空白、去前后缀、重复

package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("[%s]\n", strings.TrimSpace(" hi ")) // [hi]
fmt.Println(strings.TrimSuffix("report.csv", ".csv")) // report
fmt.Println(strings.TrimLeft("007", "0")) // 7
fmt.Println(strings.Repeat("-", 5)) // -----
}

多行字符串用反引号:内容原样保留,不处理转义,也不去掉缩进

package main
import "fmt"
func main() {
sql := `SELECT *
FROM users`
fmt.Println(sql)
// SELECT *
// FROM users
}

len 数的是 UTF-8 字节;数码点用 utf8.RuneCountInString;range 字符串得到字节下标和 rune,详见第 14 章

package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "中😀"
fmt.Println(len(s), utf8.RuneCountInString(s)) // 7 2
for i, r := range s {
fmt.Println(i, string(r)) // 依次打印 0 中、3 😀
}
}

切片:字面量创建,append 加到末尾,一定要接住它的返回值

package main
import "fmt"
func main() {
nums := []int{3, 1, 2}
nums = append(nums, 4, 5)
fmt.Println(nums, len(nums)) // [3 1 2 4 5] 5
}

插入、按下标删除:slices.Insert、slices.Delete

package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{1, 2, 3}
nums = slices.Insert(nums, 0, 0)
nums = slices.Delete(nums, 1, 2) // 删下标 1 的元素
fmt.Println(nums) // [0 2 3]
}

索引、修改、切片表达式(不含结束位置)、包含;最后一个元素写 s[len(s)-1]

package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{10, 20, 30, 40}
nums[0] = 11
fmt.Println(nums[len(nums)-1], nums[1:3]) // 40 [20 30]
fmt.Println(slices.Contains(nums, 20), slices.Index(nums, 30)) // true 2
}

make 建指定长度的切片,元素是零值;数组 [3]int 长度固定,长度是类型的一部分

package main
import "fmt"
func main() {
zeros := make([]int, 3)
var arr [3]int
fmt.Println(zeros, arr, len(arr)) // [0 0 0] [0 0 0] 3
}

排序:slices.Sort 原地排;自定义顺序用 slices.SortFunc 加 cmp.Compare,多个条件用 cmp.Or 串起来

package main
import (
"cmp"
"fmt"
"slices"
"strings"
)
func main() {
nums := []int{10, 9, 1}
slices.Sort(nums)
fmt.Println(nums) // [1 9 10]
words := []string{"banana", "Apple", "cherry"}
slices.SortFunc(words, func(a, b string) int {
return cmp.Or(cmp.Compare(len(b), len(a)), strings.Compare(a, b))
})
fmt.Println(words) // [banana cherry Apple]
}

没有推导式,标准库也没有 Map、Filter,用 for 循环代替,详见第 13 章

package main
import "fmt"
func main() {
var squares []int
for _, n := range []int{1, 2, 3, 4} {
if n%2 == 0 {
squares = append(squares, n*n)
}
}
fmt.Println(squares) // [4 16]
}

查找和汇总:slices.IndexFunc、slices.Max、slices.Min

package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{3, 8, 12}
fmt.Println(slices.IndexFunc(nums, func(n int) bool { return n > 5 })) // 1
fmt.Println(slices.Max(nums), slices.Min(nums)) // 12 3
}

映射 map:字面量创建,赋值是新增或覆盖,delete 删除,len 取个数

package main
import "fmt"
func main() {
ages := map[string]int{"小明": 18, "小红": 20}
ages["小刚"] = 19
delete(ages, "小红")
fmt.Println(len(ages), ages) // 2 map[小刚:19 小明:18]
}

取值:键不存在时得到零值;用“值, ok”区分“不存在”和“值就是零”,详见第 6 章

package main
import "fmt"
func main() {
ages := map[string]int{"小明": 18}
fmt.Println(ages["小李"]) // 0
if _, ok := ages["小李"]; !ok {
fmt.Println("没有小李") // 没有小李
}
}

range 遍历 map 的顺序每次都可能不同;要固定顺序就先把键排序(maps.Keys 返回迭代器),详见第 12 章

package main
import (
"fmt"
"maps"
"slices"
)
func main() {
ages := map[string]int{"b": 2, "a": 1, "c": 3}
for _, k := range slices.Sorted(maps.Keys(ages)) {
fmt.Println(k, ages[k]) // 依次打印 a 1、b 2、c 3
}
}

没有集合类型,用 map[T]struct{} 代替,空结构体不占内存,详见第 12 章

package main
import "fmt"
func main() {
seen := map[string]struct{}{}
for _, w := range []string{"a", "b", "a"} {
seen[w] = struct{}{}
}
_, ok := seen["a"]
fmt.Println(len(seen), ok) // 2 true
}

计数和分组:用 map 累加

package main
import "fmt"
func main() {
counts := map[string]int{}
for _, r := range "banana" {
counts[string(r)]++
}
fmt.Println(counts) // map[a:3 b:1 n:2]
groups := map[int][]string{}
for _, w := range []string{"a", "bb", "cc", "d"} {
groups[len(w)] = append(groups[len(w)], w)
}
fmt.Println(groups) // map[1:[a d] 2:[bb cc]]
}

切片赋值只复制切片头,两边共用底层数组;要独立的一份用 slices.Clone,拼接用 slices.Concat,详见第 5 章

package main
import (
"fmt"
"slices"
)
func main() {
a := []int{1, 2}
alias := a
alias[1] = 7
b := slices.Clone(a)
b[0] = 9
fmt.Println(a, b, slices.Concat(a, b)) // [1 7] [9 7] [1 7 9 7]
}

比较内容用 slices.Equal、maps.Equal;切片和 map 不能用 ==,只能和 nil 比

package main
import (
"fmt"
"maps"
"slices"
)
func main() {
fmt.Println(slices.Equal([]int{1, 2}, []int{1, 2})) // true
fmt.Println(maps.Equal(map[string]int{"a": 1}, map[string]int{"a": 1})) // true
}

if / else if / else:条件不加括号,花括号不能省

package main
import "fmt"
func main() {
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B") // B
} else {
fmt.Println("C")
}
}

if 可以先写一条初始化语句,里面声明的变量只在 if/else 里可见

package main
import (
"fmt"
"strconv"
)
func main() {
if n, err := strconv.Atoi("42"); err == nil {
fmt.Println(n + 1) // 43
}
}

没有三元表达式,用 if/else 代替,详见第 1 章

package main
import "fmt"
func main() {
n := 7
parity := "偶数"
if n%2 == 1 {
parity = "奇数"
}
fmt.Println(parity) // 奇数
}

计数循环:for 三段式;也可以 range 一个整数

package main
import "fmt"
func main() {
for i := 0; i < 10; i += 3 {
fmt.Println(i) // 依次打印 0 3 6 9
}
for i := range 3 {
fmt.Println(i) // 依次打印 0 1 2
}
}

没有 while,用只带条件的 for 代替;for {} 是无限循环

package main
import "fmt"
func main() {
n := 1
for n < 100 {
n *= 2
}
fmt.Println(n) // 128
}

range 遍历切片得到下标和元素,不要的那个写 _

package main
import "fmt"
func main() {
for i, s := range []string{"a", "b"} {
fmt.Println(i, s) // 依次打印 0 a、1 b
}
}

break、continue

package main
import "fmt"
func main() {
n := 0
for {
n++
if n%2 == 0 {
continue
}
if n > 5 {
break
}
fmt.Println(n) // 依次打印 1 3 5
}
}

跳出多层循环:给外层循环加标签,写 break 标签

package main
import "fmt"
func main() {
rows := [][]int{{1, 2}, {3, 4}}
outer:
for _, row := range rows {
for _, x := range row {
if x == 3 {
break outer
}
fmt.Println(x) // 依次打印 1 2
}
}
}

switch 默认不贯穿,不用写 break;一个 case 可以列多个值;要贯穿写 fallthrough

package main
import "fmt"
func main() {
code := 404
switch code {
case 200, 201:
fmt.Println("成功")
case 404:
fmt.Println("找不到") // 找不到
default:
fmt.Println("其他")
}
}

不带表达式的 switch 相当于 if-else 链

package main
import "fmt"
func main() {
score := 85
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B") // B
default:
fmt.Println("C")
}
}

类型 switch:按接口里值的具体类型分支;没有能解构的模式匹配,详见第 15 章

package main
import "fmt"
func describe(v any) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("整数 %d", x)
case string:
return "字符串 " + x
case nil:
return "nil"
default:
return fmt.Sprintf("其他 %T", x)
}
}
func main() {
fmt.Println(describe(1)) // 整数 1
fmt.Println(describe("a")) // 字符串 a
fmt.Println(describe(2.5)) // 其他 float64
}

没有 do...while,用 for 加末尾的判断和 break 代替

package main
import "fmt"
func main() {
tries := 0
for {
tries++
if tries >= 3 {
break
}
}
fmt.Println(tries) // 3
}

定义函数:类型写在名字后面,相邻的同类型参数可以合写

package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
fmt.Println(add(1, 2)) // 3
}

多返回值:最后一个通常是 error,调用方解构接收,详见第 7 章

package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数为零")
}
return a / b, nil
}
func main() {
q, err := divide(7, 2)
fmt.Println(q, err) // 3 <nil>
_, err = divide(1, 0)
fmt.Println(err) // 除数为零
}

具名返回值:结果变量在函数开头就是零值,空的 return 把它们交回去

package main
import (
"fmt"
"slices"
)
func minMax(nums []int) (lo, hi int) {
lo, hi = slices.Min(nums), slices.Max(nums)
return
}
func main() {
fmt.Println(minMax([]int{3, 1, 4})) // 1 4
}

可变参数 ...T,在函数里是切片;传切片时在后面加 ...

package main
import "fmt"
func total(nums ...int) int {
sum := 0
for _, n := range nums {
sum += n
}
return sum
}
func main() {
more := []int{4, 5}
fmt.Println(total(1, 2, 3), total(more...)) // 6 9
}

没有默认参数和命名参数,用选项结构体代替,字段的零值表示用默认值,详见第 7 章

package main
import (
"cmp"
"fmt"
)
type GreetOptions struct {
Greeting string
}
func greet(name string, opt GreetOptions) string {
return cmp.Or(opt.Greeting, "你好") + "," + name
}
func main() {
fmt.Println(greet("小明", GreetOptions{})) // 你好,小明
fmt.Println(greet("小明", GreetOptions{Greeting: "早上好"})) // 早上好,小明
}

没有重载,同样的功能按类型起不同的名字,或者接收 ...any,详见第 7 章

package main
import (
"fmt"
"strconv"
)
func main() {
fmt.Println(strconv.FormatInt(42, 2), strconv.FormatFloat(1.5, 'f', 1, 64)) // 101010 1.5
}

匿名函数可以赋给变量;函数类型写成 func(参数) 返回值

package main
import "fmt"
func apply(f func(int) int, x int) int { return f(x) }
func main() {
square := func(x int) int { return x * x }
fmt.Println(square(5), apply(square, 3)) // 25 9
}

闭包:函数字面量记住并能修改外层变量,详见第 7 章

package main
import "fmt"
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
next := makeCounter()
next()
fmt.Println(next()) // 2
}

函数是普通的值:可以放进 map;包里的函数也能直接当值传

package main
import (
"fmt"
"strings"
)
func main() {
ops := map[string]func(int) int{
"double": func(x int) int { return x * 2 },
"neg": func(x int) int { return -x },
}
fmt.Println(ops["double"](5), ops["neg"](3)) // 10 -3
upper := strings.ToUpper
fmt.Println(upper("go")) // GO
}

包装函数:用高阶函数给函数加功能

package main
import "fmt"
func logged(name string, fn func(int, int) int) func(int, int) int {
return func(a, b int) int {
fmt.Printf("调用 %s(%d, %d)\n", name, a, b)
return fn(a, b)
}
}
func main() {
add := logged("add", func(a, b int) int { return a + b })
fmt.Println(add(1, 2)) // 先打印"调用 add(1, 2)",再打印 3
}

参数一律按值传递;要让函数改调用方的变量就传指针,详见第 5 章

package main
import "fmt"
func reset(n *int) { *n = 0 }
func main() {
x := 5
reset(&x)
fmt.Println(x) // 0
}

没有类,用结构体加方法代替:数据放在结构体里,方法通过接收者挂到类型上,详见第 8 章。

定义结构体,按字段名创建;没写的字段是零值

package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "小明", Age: 18}
var empty User
fmt.Println(u.Name, u.Age, empty.Age) // 小明 18 0
}

构造函数是普通函数,惯例叫 NewXxx,常返回指针;方法的接收者写在 func 和方法名之间

package main
import "fmt"
type Account struct {
owner string
balance int
}
func NewAccount(owner string) *Account {
return &Account{owner: owner}
}
func (a *Account) Deposit(amount int) { a.balance += amount }
func main() {
acc := NewAccount("小明")
acc.Deposit(100)
fmt.Println(acc.owner, acc.balance) // 小明 100
}

值接收者拿到的是副本,改字段要用指针接收者,详见第 8 章

package main
import "fmt"
type Counter struct{ n int }
func (c Counter) IncValue() { c.n++ } // 改的是副本
func (c *Counter) IncPointer() { c.n++ }
func main() {
var c Counter
c.IncValue()
c.IncPointer()
fmt.Println(c.n) // 1
}

没有属性语法:字段直接读写,要校验就写方法;首字母大写的字段和方法才能被别的包访问,详见第 11 章

package main
import (
"errors"
"fmt"
)
type Temperature struct{ celsius float64 }
func (t *Temperature) SetCelsius(v float64) error {
if v < -273.15 {
return errors.New("低于绝对零度")
}
t.celsius = v
return nil
}
func (t Temperature) Fahrenheit() float64 { return t.celsius*9/5 + 32 }
func main() {
var t Temperature
fmt.Println(t.SetCelsius(-300)) // 低于绝对零度
t.SetCelsius(100)
fmt.Println(t.Fahrenheit()) // 212
}

字符串表示:实现 String() string 方法,fmt 打印时会用它

package main
import "fmt"
type Point struct{ X, Y int }
func (p Point) String() string { return fmt.Sprintf("(%d, %d)", p.X, p.Y) }
func main() {
p := Point{1, 2}
fmt.Println(p) // (1, 2)
fmt.Printf("点 %v\n", p) // 点 (1, 2)
}

没写 String 时:%v 只打印字段值,%+v 带字段名,%#v 打印成 Go 语法

package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{"小明", 18}
fmt.Printf("%v\n%+v\n%#v\n", u, u, u)
// {小明 18}
// {Name:小明 Age:18}
// main.User{Name:"小明", Age:18}
}

没有继承,用嵌入代替:把一个类型作为匿名字段放进结构体,它的字段和方法提升到外层,详见第 8 章

package main
import "fmt"
type Animal struct{ Name string }
func (a Animal) Describe() string { return "我是" + a.Name }
type Dog struct {
Animal
Breed string
}
func main() {
d := Dog{Animal: Animal{Name: "旺财"}, Breed: "柴犬"}
fmt.Println(d.Name, d.Describe()) // 旺财 我是旺财
}

外层的同名方法会遮住提升来的方法,但被嵌入的类型调不回外层,没有虚方法,详见第 8 章

package main
import "fmt"
type Animal struct{}
func (Animal) Sound() string { return "..." }
func (a Animal) Speak() string { return "叫声:" + a.Sound() }
type Dog struct{ Animal }
func (Dog) Sound() string { return "汪" }
func main() {
d := Dog{}
fmt.Println(d.Sound()) // 汪
fmt.Println(d.Speak()) // 叫声:...
}

结构体是值:赋值和传参整块复制;字段都能比较时可以用 ==,详见第 5 章

package main
import "fmt"
type Point struct{ X, Y int }
func main() {
a := Point{1, 2}
b := a
b.X = 9
fmt.Println(a, b, a == Point{1, 2}) // {1 2} {9 2} true
}

没有 enum,用自定义类型加 iota 常量代替,再给它写 String 方法,详见第 15 章

package main
import "fmt"
type Color int
const (
Red Color = iota
Green
Blue
)
func (c Color) String() string { return [...]string{"Red", "Green", "Blue"}[c] }
func main() {
c := Green
fmt.Println(c, int(c)) // Green 1
}

只用一次的数据用匿名结构体

package main
import "fmt"
func main() {
pair := struct {
Name string
Age int
}{"小明", 18}
fmt.Printf("%+v\n", pair) // {Name:小明 Age:18}
}

定义接口;类型有接口要求的全部方法就自动实现了它,不用声明,详见第 8 章

package main
import "fmt"
type Shape interface {
Area() float64
}
type Square struct{ Side float64 }
func (s Square) Area() float64 { return s.Side * s.Side }
func main() {
total := 0.0
for _, s := range []Shape{Square{2}, Square{3}} {
total += s.Area()
}
fmt.Println(total) // 13
}

接口可以嵌入别的接口;惯用的接口都很小,一两个方法

package main
import "fmt"
type Named interface{ Name() string }
type Closer interface{ Close() error }
type NamedCloser interface {
Named
Closer
}
type File struct{ name string }
func (f File) Name() string { return f.name }
func (f File) Close() error {
fmt.Println("已关闭", f.name)
return nil
}
func main() {
var nc NamedCloser = File{"a.txt"}
fmt.Println(nc.Name()) // a.txt
nc.Close() // 已关闭 a.txt
}

类型断言:从接口里取出具体类型,或者判断它是否还实现了另一个接口

package main
import (
"bytes"
"fmt"
"io"
"os"
)
func main() {
var w io.Writer = new(bytes.Buffer)
fmt.Fprint(w, "hi")
if s, ok := w.(fmt.Stringer); ok {
fmt.Println(s.String()) // hi
}
_, isFile := w.(*os.File)
fmt.Println(isFile) // false
}

在编译期确认类型实现了接口:把它赋给接口类型的空白变量,少了方法就编译失败

package main
import "fmt"
type Shape interface{ Area() float64 }
type Square struct{ Side float64 }
func (s *Square) Area() float64 { return s.Side * s.Side }
var _ Shape = (*Square)(nil)
func main() {
fmt.Println((&Square{3}).Area()) // 9
}

实现标准接口 io.Writer,就能交给 fmt.Fprintf 这些函数用

package main
import "fmt"
type CountingWriter struct{ N int }
func (c *CountingWriter) Write(p []byte) (int, error) {
c.N += len(p)
return len(p), nil
}
func main() {
var cw CountingWriter
fmt.Fprintf(&cw, "你好%d", 42)
fmt.Println(cw.N) // 8
}

any 是 interface{} 的别名,什么值都能装,取出来要断言,详见第 3 章

package main
import "fmt"
func main() {
items := []any{1, "a", 2.5}
fmt.Println(items...) // 1 a 2.5
fmt.Printf("%T\n", items[2]) // float64
}

指针接收者的方法只属于 *T:把 T 的值赋给接口会编译失败(method Speak has pointer receiver),要传 &T,详见第 8 章

package main
import "fmt"
type Speaker interface{ Speak() string }
type Dog struct{}
func (d *Dog) Speak() string { return "汪" }
func main() {
var s Speaker = &Dog{}
fmt.Println(s.Speak()) // 汪
}

别人的类型只要方法对得上就能直接当接口用;方法名不同时包一层,详见第 8 章

package main
import "fmt"
type Speaker interface{ Speak() string }
type Duck struct{}
func (Duck) Speak() string { return "嘎" }
type Robot struct{}
func (Robot) Beep() string { return "哔" }
type RobotSpeaker struct{ Robot }
func (r RobotSpeaker) Speak() string { return r.Beep() }
func main() {
for _, s := range []Speaker{Duck{}, RobotSpeaker{}} {
fmt.Println(s.Speak()) // 依次打印 嘎、哔
}
}

泛型函数:类型参数写在方括号里,调用时一般能推断出来,详见第 9 章

package main
import (
"fmt"
"strconv"
)
func Map[T, U any](items []T, f func(T) U) []U {
out := make([]U, 0, len(items))
for _, v := range items {
out = append(out, f(v))
}
return out
}
func main() {
fmt.Printf("%q\n", Map([]int{1, 2, 3}, strconv.Itoa)) // ["1" "2" "3"]
}

泛型类型:类型名后面写类型参数,方法的接收者写成 *Stack[T]

package main
import "fmt"
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
func main() {
var s Stack[string]
s.Push("a")
s.Push("b")
fmt.Println(s.Pop()) // b true
}

约束 cmp.Ordered:能用 <、> 比较的类型

package main
import (
"cmp"
"fmt"
)
func Larger[T cmp.Ordered](a, b T) T {
if a >= b {
return a
}
return b
}
func main() {
fmt.Println(Larger(3, 7), Larger("pear", "apple")) // 7 pear
}

类型集合:~int | ~float64 列出允许的类型,~ 让底层类型相同的自定义类型也算进去,详见第 9 章

package main
import "fmt"
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
type Cents int
func main() {
fmt.Println(Sum([]float64{1.5, 2.5}), Sum([]Cents{100, 250})) // 4 350
}

comparable 约束:允许 ==,能当 map 的键

package main
import "fmt"
func Unique[T comparable](items []T) []T {
seen := map[T]struct{}{}
var out []T
for _, v := range items {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
out = append(out, v)
}
}
return out
}
func main() {
fmt.Println(Unique([]int{1, 1, 2, 3, 2})) // [1 2 3]
}

多个类型参数

package main
import "fmt"
type Pair[K comparable, V any] struct {
Key K
Val V
}
func main() {
p := Pair[string, int]{"小明", 18}
fmt.Printf("%+v\n", p) // {Key:小明 Val:18}
}

推断不出类型参数时显式写出;类型参数的零值用 var zero T 取得

package main
import "fmt"
func Zero[T any]() T {
var zero T
return zero
}
func main() {
fmt.Printf("%q %v\n", Zero[string](), Zero[int]()) // "" 0
}

运行时拿得到类型参数的实际类型:reflect.TypeFor[T](),详见第 9 章

package main
import (
"fmt"
"reflect"
)
func NameOf[T any]() string { return reflect.TypeFor[T]().String() }
func main() {
fmt.Println(NameOf[int](), NameOf[[]string]()) // int []string
}

泛型类型别名(1.24 起)

package main
import "fmt"
type Set[T comparable] = map[T]struct{}
func main() {
s := Set[string]{"a": {}}
s["b"] = struct{}{}
fmt.Println(len(s)) // 2
}

泛型方法(1.27 起):方法可以声明自己的类型参数;接口里的方法仍然不能有,详见第 9 章

package main
import (
"fmt"
"strconv"
)
type Box[T any] struct{ Val T }
func (b Box[T]) Map[U any](f func(T) U) Box[U] {
return Box[U]{f(b.Val)}
}
func main() {
b := Box[int]{21}.Map(func(n int) string { return strconv.Itoa(n * 2) })
fmt.Printf("%q\n", b.Val) // "42"
}

没有异常,错误是普通的返回值:调用后立刻检查 err != nil,详见第 10 章

package main
import (
"fmt"
"strconv"
)
func main() {
n, err := strconv.Atoi("abc")
if err != nil {
fmt.Printf("出错:%v\n", err) // 出错:strconv.Atoi: parsing "abc": invalid syntax
return
}
fmt.Println(n)
}

创建错误:固定文字用 errors.New,带格式用 fmt.Errorf

package main
import "fmt"
func checkAge(age int) error {
if age < 0 {
return fmt.Errorf("年龄不能为负:%d", age)
}
return nil
}
func main() {
fmt.Println(checkAge(-1)) // 年龄不能为负:-1
fmt.Println(checkAge(18)) // <nil>
}

哨兵错误:包级的错误变量,调用方用 errors.Is 判断,被 %w 包装过也认得出,详见第 10 章

package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("找不到")
func find(id int) error {
return fmt.Errorf("查询用户 %d:%w", id, ErrNotFound)
}
func main() {
err := find(7)
fmt.Println(err) // 查询用户 7:找不到
fmt.Println(errors.Is(err, ErrNotFound)) // true
}

%w 包装时保留原错误,errors.Unwrap 取回;用 %v 只拼进文字,errors.Is 就认不出了

package main
import (
"errors"
"fmt"
)
func main() {
base := errors.New("磁盘满了")
wrapped := fmt.Errorf("保存失败:%w", base)
flat := fmt.Errorf("保存失败:%v", base)
fmt.Println(errors.Unwrap(wrapped) == base) // true
fmt.Println(errors.Is(flat, base)) // false
}

自定义错误类型:实现 Error() string;errors.AsType 沿包装链取出这个类型(1.26 起)

package main
import (
"errors"
"fmt"
)
type ConfigError struct {
Path string
Reason string
}
func (e *ConfigError) Error() string { return e.Path + ":" + e.Reason }
func load() error {
return fmt.Errorf("启动失败:%w", &ConfigError{"app.json", "端口超出范围"})
}
func main() {
err := load()
fmt.Println(err) // 启动失败:app.json:端口超出范围
if ce, ok := errors.AsType[*ConfigError](err); ok {
fmt.Println(ce.Path) // app.json
}
}

把几个错误合成一个:errors.Join,打印时每个占一行

package main
import (
"errors"
"fmt"
)
func main() {
err := errors.Join(errors.New("名字为空"), errors.New("年龄为负"))
fmt.Println(err)
// 名字为空
// 年龄为负
}

没有 finally,用 defer 代替:函数返回前执行,正常返回、出错返回、panic 都一样,详见第 10 章

package main
import (
"errors"
"fmt"
)
func process() error {
fmt.Println("打开")
defer fmt.Println("清理")
return errors.New("处理失败")
}
func main() {
fmt.Println(process())
// 打开
// 清理
// 处理失败
}

资源清理:打开成功后紧跟 defer x.Close();多个 defer 后进先出

package main
import (
"fmt"
"os"
)
func main() {
f, err := os.CreateTemp("", "demo-*.txt")
if err != nil {
fmt.Println(err)
return
}
defer os.Remove(f.Name()) // 后执行
defer f.Close() // 先执行
fmt.Fprintln(f, "你好")
fmt.Println("已写入") // 已写入
}

defer 的参数在登记时就求值,不是在执行时

package main
import "fmt"
func main() {
x := 1
defer fmt.Println("defer 看到", x) // 最后打印"defer 看到 1"
x = 2
fmt.Println("现在是", x) // 现在是 2
}

panic 只用于程序错误;在 defer 里用 recover 接住,转成错误返回,详见第 10 章

package main
import "fmt"
func safeDivide(a, b int) (q int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("恢复:%v", r)
}
}()
return a / b, nil
}
func main() {
fmt.Println(safeDivide(1, 0)) // 0 恢复:runtime error: integer divide by zero
}

nil 是指针、切片、map、通道、函数和接口的零值;int、string、结构体没有 nil,详见第 6 章

package main
import "fmt"
func main() {
var p *int
var s []int
var m map[string]int
var e error
fmt.Println(p == nil, s == nil, m == nil, e == nil) // true true true true
}

没有可空类型,用指针代替:nil 表示“没设置”,详见第 6 章

package main
import "fmt"
type Profile struct {
Nickname *string
}
func main() {
var pr Profile
if pr.Nickname == nil {
fmt.Println("没设置昵称") // 没设置昵称
}
}

new(表达式) 直接得到指向这个值的指针(1.26 起),给指针字段赋值不用先声明变量

package main
import "fmt"
type Profile struct {
Nickname *string
Age *int
}
func main() {
pr := Profile{Nickname: new("小明"), Age: new(18)}
fmt.Println(*pr.Nickname, *pr.Age) // 小明 18
}

没有 ?.,用 if 判断 nil 代替,详见第 6 章

package main
import "fmt"
type Profile struct{ Name string }
type User struct{ Profile *Profile }
func main() {
u := User{}
name := "无"
if u.Profile != nil {
name = u.Profile.Name
}
fmt.Println(name) // 无
}

没有 ??;把零值当“没有”时,用 cmp.Or 取第一个非零值

package main
import (
"cmp"
"fmt"
)
func main() {
var input string
fmt.Println(cmp.Or(input, "默认")) // 默认
}

“值, ok”:map 取值、类型断言、通道接收都用第二个返回值说明有没有拿到

package main
import "fmt"
func main() {
m := map[string]int{"a": 0}
v, ok := m["a"]
_, ok2 := m["b"]
fmt.Println(v, ok, ok2) // 0 true false
}

nil 切片可以直接 append、len、range;nil map 能读不能写,写入就 panic

package main
import "fmt"
func main() {
var s []int
s = append(s, 1)
var m map[string]int
fmt.Println(s, len(m), m["x"]) // [1] 0 0
defer func() { fmt.Println(recover()) }() // assignment to entry in nil map
m["x"] = 1
}

解引用 nil 指针会 panic,详见第 6 章

package main
import "fmt"
func main() {
var p *int
defer func() { fmt.Println(recover()) }() // runtime error: invalid memory address or nil pointer dereference
fmt.Println(*p)
}

接口里装着 nil 指针时,接口本身不等于 nil;没有错误就直接 return nil,详见第 6 章

package main
import "fmt"
type MyErr struct{}
func (*MyErr) Error() string { return "出错" }
func mayFail() error {
var e *MyErr
return e
}
func main() {
fmt.Println(mayFail() == nil) // false
}

方法的接收者可以是 nil 指针,在方法里判断

package main
import "fmt"
type Node struct {
Val int
Next *Node
}
func (n *Node) Len() int {
if n == nil {
return 0
}
return 1 + n.Next.Len()
}
func main() {
var list *Node
fmt.Println(list.Len()) // 0
list = &Node{1, &Node{2, nil}}
fmt.Println(list.Len()) // 2
}

导入标准库:多个包用括号分组,用的时候写包路径的最后一段

package main
import (
"fmt"
"path/filepath"
)
func main() {
fmt.Println(filepath.Base("/tmp/report.csv")) // report.csv
}

别名导入;空白导入只为执行那个包的初始化(例如注册图片解码器)

package main
import (
"fmt"
_ "image/png"
str "strings"
)
func main() {
fmt.Println(str.Repeat("ab", 2)) // abab
}

自己的包:一个目录一个包,按“模块路径/目录”导入;go.mod 里写着 module example.com/shop,详见第 11 章

pricing/pricing.go
package pricing
const taxRatePercent = 13 // 小写:只在 pricing 包里可见
func WithTax(price int) int { // 大写:导出
return price + price*taxRatePercent/100
}
// main.go
package main
import (
"fmt"
"example.com/shop/pricing"
)
func main() {
fmt.Println(pricing.WithTax(100)) // 113
}

可见性只看首字母:大写导出,小写只在本包可见,没有 public、private 关键字;在 main.go 里写 pricing.taxRatePercent,构建报错,详见第 11 章(这里和后面报错里的 .\main.go 是 Windows 上实测的路径写法,Linux 和 macOS 上是 ./main.go)

.\main.go:10:22: undefined: pricing.taxRatePercent

同一个包的多个文件直接互相使用,不用导入

util.go
package main
func double(n int) int { return n * 2 }
// main.go
package main
import "fmt"
func main() {
fmt.Println(double(21)) // 42
}

internal 目录里的包只能被它上一级目录及以下的代码导入,详见第 11 章

shop/
├── go.mod
├── internal/tax/tax.go 只有 shop/ 下的代码能导入 example.com/shop/internal/tax
└── pricing/pricing.go

shop 以外的模块导入它,构建报错

main.go:6:2: use of internal package example.com/shop/internal/tax not allowed

包之间不能循环导入,构建报 import cycle not allowed,详见第 11 章

package example.com/shop/a
imports example.com/shop/b from a.go
imports example.com/shop/a from b.go: import cycle not allowed

包级的 init 函数在 main 之前自动执行

package main
import "fmt"
var config map[string]string
func init() {
config = map[string]string{"mode": "dev"}
}
func main() {
fmt.Println(config["mode"]) // dev
}

把文件内容编进程序://go:embed

// main.go(同目录下有 hello.txt,内容是"你好")
package main
import (
_ "embed"
"fmt"
)
//go:embed hello.txt
var hello string
func main() {
fmt.Println(hello) // 你好
}

查看包和函数的文档

终端窗口
go doc strings.Cut

range 能遍历切片、数组、字符串、map、通道、整数和迭代器函数;遍历通道直到它被关闭

package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println(v) // 依次打印 1 2
}
}

没有生成器和 yield 关键字,用 iter.Seq 函数代替:它接收一个 yield 回调,for range 直接遍历它,详见第 13 章

package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func main() {
for v := range Countdown(3) {
fmt.Println(v) // 依次打印 3 2 1
}
}

无穷序列:消费者 break 时 yield 返回 false,迭代器要随之返回,详见第 13 章

package main
import (
"fmt"
"iter"
)
func Fib() iter.Seq[int] {
return func(yield func(int) bool) {
a, b := 0, 1
for {
if !yield(a) {
return
}
a, b = b, a+b
}
}
}
func main() {
var out []int
for n := range Fib() {
if n > 20 {
break
}
out = append(out, n)
}
fmt.Println(out) // [0 1 1 2 3 5 8 13]
}

两个值的迭代器 iter.Seq2:像遍历切片那样一次给出两个值

package main
import (
"fmt"
"iter"
"strings"
)
func NumberedLines(text string) iter.Seq2[int, string] {
return func(yield func(int, string) bool) {
for i, line := range strings.Split(text, "\n") {
if !yield(i+1, line) {
return
}
}
}
}
func main() {
for no, line := range NumberedLines("a\nb") {
fmt.Println(no, line) // 依次打印 1 a、2 b
}
}

标准库的迭代器:slices.Values、slices.All、maps.Keys、maps.All;slices.Collect 收成切片

package main
import (
"fmt"
"maps"
"slices"
)
func main() {
m := map[string]int{"b": 2, "a": 1}
fmt.Println(slices.Sorted(maps.Keys(m))) // [a b]
for i, v := range slices.All([]string{"x", "y"}) {
fmt.Println(i, v) // 依次打印 0 x、1 y
}
fmt.Println(slices.Collect(maps.Values(map[string]int{"k": 7}))) // [7]
}

标准库没有 Filter、Map,自己写几行,接成惰性的流水线,详见第 13 章

package main
import (
"fmt"
"iter"
"slices"
)
func Filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if keep(v) && !yield(v) {
return
}
}
}
}
func main() {
evens := Filter(slices.Values([]int{1, 2, 3, 4}), func(n int) bool { return n%2 == 0 })
fmt.Println(slices.Collect(evens)) // [2 4]
}

自定义类型提供遍历:惯例是写一个返回迭代器的 All 方法

package main
import (
"fmt"
"iter"
"slices"
)
type Bag struct{ items []string }
func (b *Bag) All() iter.Seq[string] { return slices.Values(b.items) }
func main() {
b := &Bag{items: []string{"x", "y"}}
for it := range b.All() {
fmt.Println(it) // 依次打印 x、y
}
}

拉取式:iter.Pull 把迭代器变成 next 函数,用完调用 stop,详见第 13 章

package main
import (
"fmt"
"iter"
"slices"
)
func main() {
next, stop := iter.Pull(slices.Values([]int{10, 20}))
defer stop()
v1, ok1 := next()
v2, _ := next()
_, ok3 := next()
fmt.Println(v1, ok1, v2, ok3) // 10 true 20 false
}

分块:slices.Chunk

package main
import (
"fmt"
"slices"
)
func main() {
for c := range slices.Chunk([]int{1, 2, 3, 4, 5}, 2) {
fmt.Println(c) // 依次打印 [1 2]、[3 4]、[5]
}
}

go 启动 goroutine;sync.WaitGroup 的 Go 方法启动并计数(1.25 起),Wait 等它们全部结束,详见第 16 章

package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
results := make([]int, 3)
for i := range 3 {
wg.Go(func() {
results[i] = (i + 1) * 10
})
}
wg.Wait()
fmt.Println(results) // [10 20 30]
}

没有 async/await,用 goroutine 加阻塞写法代替:函数照常写,要并发就放进 goroutine;下面两次 fetch 同时等待,总耗时约 100 毫秒而不是 200 毫秒,详见第 19 章

package main
import (
"fmt"
"sync"
"time"
)
func fetch(n int) int {
time.Sleep(100 * time.Millisecond)
return n * 10
}
func main() {
var a, b int
var wg sync.WaitGroup
wg.Go(func() { a = fetch(1) })
wg.Go(func() { b = fetch(2) })
wg.Wait()
fmt.Println(a, b) // 10 20
}

通道传结果:发送 ch <- v,接收 <-ch;无缓冲的通道要两边都到了才交接,详见第 20 章

package main
import "fmt"
func main() {
ch := make(chan string)
go func() { ch <- "完成" }()
fmt.Println(<-ch) // 完成
}

发送方用完关闭通道,接收方 range 读到关闭为止;只由发送方关闭,详见第 20 章

package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
defer close(ch)
for i := 1; i <= 3; i++ {
ch <- i
}
}()
for v := range ch {
fmt.Println(v) // 依次打印 1 2 3
}
}

select 同时等几个通道,哪个先就绪走哪个;配 time.After 做超时,详见第 20 章

package main
import (
"fmt"
"time"
)
func main() {
slow := make(chan string, 1)
go func() {
time.Sleep(time.Second)
slow <- "结果"
}()
select {
case r := <-slow:
fmt.Println(r)
case <-time.After(100 * time.Millisecond):
fmt.Println("超时") // 超时
}
}

取消和超时用 context:把 ctx 一路传下去,在 select 里等 ctx.Done(),详见第 21 章

package main
import (
"context"
"fmt"
"time"
)
func work(ctx context.Context) error {
select {
case <-time.After(time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
fmt.Println(work(ctx)) // context deadline exceeded
}

互斥锁:sync.Mutex 的零值就能用,拿锁后紧跟 defer Unlock,详见第 17 章

package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func main() {
var c Counter
var wg sync.WaitGroup
for range 1000 {
wg.Go(c.Inc)
}
wg.Wait()
fmt.Println(c.n) // 1000
}

简单的计数用原子类型 atomic.Int64,详见第 17 章

package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var hits atomic.Int64
var wg sync.WaitGroup
for range 1000 {
wg.Go(func() { hits.Add(1) })
}
wg.Wait()
fmt.Println(hits.Load()) // 1000
}

限制并发数:带缓冲的通道当信号量

package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
sem := make(chan struct{}, 2) // 最多两个同时运行
var total atomic.Int64
var wg sync.WaitGroup
for i := 1; i <= 10; i++ {
wg.Go(func() {
sem <- struct{}{}
defer func() { <-sem }()
time.Sleep(10 * time.Millisecond)
total.Add(int64(i))
})
}
wg.Wait()
fmt.Println(total.Load()) // 55
}

只执行一次的初始化:sync.OnceValue,第一次调用时执行,之后返回同一个结果

package main
import (
"fmt"
"sync"
)
func main() {
load := sync.OnceValue(func() int {
fmt.Println("加载配置")
return 8080
})
fmt.Println(load(), load()) // 先打印一次"加载配置",再打印 8080 8080
}

运行外部程序并读它的输出:os/exec,详见第 18 章

package main
import (
"fmt"
"os/exec"
"strings"
)
func main() {
out, err := exec.Command("go", "version").Output()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(strings.HasPrefix(string(out), "go version go1.")) // true
}

整个读写文本文件:os.WriteFile、os.ReadFile,读出来是 []byte

package main
import (
"fmt"
"os"
)
func main() {
if err := os.WriteFile("note.txt", []byte("第一行\n第二行\n"), 0o644); err != nil {
fmt.Println(err)
return
}
data, err := os.ReadFile("note.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Print(string(data))
// 第一行
// 第二行
}

逐行读大文件:bufio.Scanner,不把整个文件读进内存

package main
import (
"bufio"
"fmt"
"os"
)
func main() {
os.WriteFile("log.txt", []byte("a\nb\n"), 0o644)
f, err := os.Open("log.txt")
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
fmt.Println(sc.Text()) // 依次打印 a、b
}
if err := sc.Err(); err != nil {
fmt.Println(err)
}
}

追加写入:os.OpenFile 加 O_APPEND

package main
import (
"fmt"
"os"
)
func main() {
f, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
fmt.Fprintln(f, "新的一行")
}

路径:filepath.Join 用当前系统的分隔符拼接;拆出文件名、扩展名、父目录

package main
import (
"fmt"
"path/filepath"
"strings"
)
func main() {
p := filepath.Join("data", "2026", "report.csv")
fmt.Println(filepath.Base(p)) // report.csv
fmt.Println(filepath.Ext(p)) // .csv
fmt.Println(strings.TrimSuffix(filepath.Base(p), filepath.Ext(p))) // report
fmt.Println(filepath.Base(filepath.Dir(p))) // 2026
}

建多级目录、判断存在、删除整个目录

package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
os.MkdirAll("out/logs", 0o755) // 多级目录一次建好,已存在也不报错
os.WriteFile("out/logs/x.txt", []byte("x"), 0o644)
_, err := os.Stat("out/logs/x.txt")
fmt.Println(err == nil) // true
os.RemoveAll("out")
_, err = os.Stat("out")
fmt.Println(errors.Is(err, fs.ErrNotExist)) // true
}

列出目录下的直接子项:os.ReadDir,结果按文件名排好序

package main
import (
"fmt"
"os"
)
func main() {
entries, err := os.ReadDir(".")
if err != nil {
fmt.Println(err)
return
}
for _, e := range entries {
fmt.Println(e.Name(), e.IsDir())
}
}

递归遍历目录:filepath.WalkDir

package main
import (
"fmt"
"io/fs"
"path/filepath"
)
func main() {
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && filepath.Ext(path) == ".txt" {
fmt.Println(path)
}
return nil
})
}

结构体和 JSON 互转:encoding/json,字段要导出,用结构标签指定键名,详见第 22 章;omitzero 在零值时省略字段(1.24 起)

package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Tags []string `json:"tags"`
Age int `json:"age,omitzero"`
}
func main() {
data, err := json.Marshal(User{Name: "小明", Tags: []string{"a", "b"}})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(data)) // {"name":"小明","tags":["a","b"]}
var back User
if err := json.Unmarshal(data, &back); err != nil {
fmt.Println(err)
return
}
fmt.Println(back.Name, len(back.Tags)) // 小明 2
}

缩进输出用 json.MarshalIndent;中文默认原样输出,map 的键按字母排序

package main
import (
"encoding/json"
"fmt"
)
func main() {
data, _ := json.MarshalIndent(map[string]any{"name": "小明", "age": 18}, "", " ")
fmt.Println(string(data))
// {
// "age": 18,
// "name": "小明"
// }
}

结构不固定的 JSON 解到 map[string]any:数字一律成了 float64,数组是 []any

package main
import (
"encoding/json"
"fmt"
)
func main() {
var v map[string]any
json.Unmarshal([]byte(`{"n": 1, "items": [1, 2], "x": null}`), &v)
fmt.Printf("%T\n", v["n"]) // float64
fmt.Println(v["items"].([]any)[1]) // 2
fmt.Println(v["x"] == nil) // true
}

读写 JSON 文件:json.NewEncoder、json.NewDecoder 直接对着文件读写

package main
import (
"encoding/json"
"fmt"
"os"
)
type Config struct {
Port int `json:"port"`
}
func main() {
out, err := os.Create("config.json")
if err != nil {
fmt.Println(err)
return
}
json.NewEncoder(out).Encode(Config{Port: 8080})
out.Close()
in, err := os.Open("config.json")
if err != nil {
fmt.Println(err)
return
}
defer in.Close()
var c Config
json.NewDecoder(in).Decode(&c)
fmt.Println(c.Port) // 8080
}

临时目录:os.MkdirTemp 建,用完 os.RemoveAll

package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
dir, err := os.MkdirTemp("", "demo-")
if err != nil {
fmt.Println(err)
return
}
defer os.RemoveAll(dir)
os.WriteFile(filepath.Join(dir, "t.txt"), []byte("临时"), 0o644)
fmt.Println(filepath.Base(dir)[:5]) // demo-
}

只在某个目录里操作:os.Root(1.24 起),路径跑到目录外面就报错

package main
import (
"fmt"
"os"
)
func main() {
root, err := os.OpenRoot(".")
if err != nil {
fmt.Println(err)
return
}
defer root.Close()
_, err = root.Open("../secret.txt")
fmt.Println(err) // openat ../secret.txt: path escapes from parent
}

测试文件以 _test.go 结尾,和被测代码在同一个包;测试函数写成 TestXxx(t *testing.T);没有断言函数,用 if 加 t.Errorf

calc.go
package calc
func Add(a, b int) int { return a + b }
// calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
if got := Add(1, 2); got != 3 {
t.Errorf("Add(1, 2) = %d,想要 3", got)
}
}

运行模块里所有包的测试;-v 打印每个测试的名字和结果

终端窗口
go test ./...
go test -v ./...

只跑名字匹配正则的测试

终端窗口
go test -run 'TestAdd$' ./...

表格驱动:用例放进一个切片,t.Run 给每组起名,单独报告结果

square_test.go
package calc
import "testing"
func TestSquare(t *testing.T) {
tests := []struct {
name string
n, want int
}{
{"正数", 2, 4},
{"零", 0, 0},
{"负数", -3, 9},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.n * tt.n; got != tt.want {
t.Errorf("得到 %d,想要 %d", got, tt.want)
}
})
}
}

t.Errorf 记下失败后接着跑,t.Fatalf 立即结束这个测试;前提条件出错用 Fatalf

parse_test.go
package calc
import (
"strconv"
"testing"
)
func TestParse(t *testing.T) {
n, err := strconv.Atoi("42")
if err != nil {
t.Fatalf("解析失败:%v", err)
}
if n != 42 {
t.Errorf("得到 %d,想要 42", n)
}
}

测试用的临时目录:t.TempDir() 在测试结束后自动删除;t.Cleanup 登记其他清理工作

file_test.go
package calc
import (
"os"
"path/filepath"
"testing"
)
func TestWrite(t *testing.T) {
path := filepath.Join(t.TempDir(), "a.txt")
if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { t.Log("清理完毕") })
}

基准测试:BenchmarkXxx 里写 for b.Loop()(1.24 起),用 go test -bench . 运行

bench_test.go
package calc
import "testing"
func BenchmarkAdd(b *testing.B) {
for b.Loop() {
Add(1, 2)
}
}

示例函数:// Output: 注释写预期输出,go test 会核对,go doc 生成的文档里也会显示

example_test.go
package calc
import "fmt"
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output: 3
}

覆盖率

终端窗口
go test -cover ./...

查数据竞争:竞态检测器 -race,测试时打开;它需要启用 cgo,除 macOS 外还要装 C 编译器(没有时报 -race requires cgo),详见第 17 章

终端窗口
go test -race ./...

模糊测试:FuzzXxx 给出种子输入,-fuzz 让引擎不断生成新输入

fuzz_test.go
package calc
import (
"strconv"
"testing"
)
func FuzzItoa(f *testing.F) {
f.Add(42)
f.Fuzz(func(t *testing.T, n int) {
got, err := strconv.Atoi(strconv.Itoa(n))
if err != nil || got != n {
t.Errorf("往返失败:%d", n)
}
})
}
// 运行:go test -fuzz FuzzItoa -fuzztime 10s

格式化:gofmt 随 Go 附带,风格没有可配置项;go fmt 对指定的包运行 gofmt -l -w

终端窗口
go fmt ./...

只检查不改:列出格式不对的文件,适合放进 CI,有输出就算失败

终端窗口
gofmt -l .

整理 import 并格式化:goimports,Go 团队维护,需要另外安装

终端窗口
go install golang.org/x/tools/cmd/goimports@latest
goimports -w .

go vet:随 Go 附带的静态检查,查 Printf 参数不符、复制了锁这类问题;go test 会自动运行其中一部分

终端窗口
go vet ./...

把旧写法改成新写法:go fix(1.26 起改为现代化工具),-diff 只显示要改什么

终端窗口
go fix -diff ./...
go fix ./...

第三方 lint:staticcheck,用 tool 指令记进模块(1.24 起),用 go tool 运行

终端窗口
go get -tool honnef.co/go/tools/cmd/staticcheck@latest
go tool staticcheck ./...

CI 里检查 go.mod、go.sum 是否整理过:go mod tidy -diff,有差异就以非零退出码结束

终端窗口
go mod tidy -diff

想把整数转成字符串,写了 string(n) → 整数被当成码点,65 变成 “A”(go vet 会报这种转换)→ 用 strconv.Itoa

package main
import (
"fmt"
"strconv"
)
func main() {
n := 65
fmt.Println(string(rune(n))) // A
fmt.Println(strconv.Itoa(n)) // 65
}

从别的切片切出一段再 append → 容量还有空余时直接写进原来的底层数组,改掉了别人的元素 → 要独立修改先 slices.Clone,详见第 5 章

package main
import (
"fmt"
"slices"
)
func main() {
base := []int{1, 2, 3}
head := base[:2]
head = append(head, 99)
fmt.Println(base) // [1 2 99]
safe := slices.Clone(base[:2])
safe = append(safe, 7)
fmt.Println(base, safe) // [1 2 99] [1 2 7]
}

用了新版本的特性,模块 go.mod 的 go 行却比它低 → 编译错误 → 用 go get go@版本 提高 go 行;1.22 起每轮新建的 for 循环变量同样只对 go 行不低于 1.22 的模块生效,详见第 7 章

.\main.go:5:28: new(42) requires go1.26 or later (-lang was set to go1.25; check go.mod)

多个 goroutine 同时写一个 map → 运行时多半会报 fatal error: concurrent map writes,进程退出,recover 接不住;这种检测不保证每次都能发现 → 加锁,并用 go test -race 查,详见第 12 章

package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
counts := map[string]int{}
var wg sync.WaitGroup
for i := range 100 {
wg.Go(func() {
mu.Lock()
defer mu.Unlock()
counts[[]string{"偶", "奇"}[i%2]]++
})
}
wg.Wait()
fmt.Println(counts) // map[偶:50 奇:50]
}

用下标取中文字符串的“第一个字” → 取到的是一个字节 → 用 []rune 或 range,详见第 14 章

package main
import "fmt"
func main() {
s := "中文"
fmt.Println(len(s), s[0]) // 6 228
fmt.Println(string([]rune(s)[0])) // 中
}

在循环里 defer 关闭文件 → 要等整个函数返回才执行,文件一直开着 → 把循环体放进一个函数里

package main
import "fmt"
func main() {
for i := range 2 {
func() {
fmt.Println("打开", i)
defer fmt.Println("关闭", i)
}()
}
// 依次打印 打开 0、关闭 0、打开 1、关闭 1
}

在同一个 goroutine 里往无缓冲通道发送 → 没人接收,所有 goroutine 都卡住,运行时报 fatal error: all goroutines are asleep - deadlock! → 接收放进另一个 goroutine,或者用带缓冲的通道,详见第 20 章

package main
import "fmt"
func main() {
ch := make(chan int, 1) // 写成 make(chan int) 会在下一行死锁
ch <- 1
fmt.Println(<-ch) // 1
}

写迭代器时不看 yield 的返回值 → 消费者 break 后还接着调用,运行时 panic:range function continued iteration after function for loop body returned false → if !yield(v) { return },详见第 13 章

package main
import (
"fmt"
"iter"
)
func Nums() iter.Seq[int] {
return func(yield func(int) bool) {
for i := range 3 {
if !yield(i) {
return
}
}
}
}
func main() {
for n := range Nums() {
if n == 1 {
break
}
fmt.Println(n) // 0
}
}

用常量算小数比较 → 常量按精确值计算,0.1+0.2 == 0.3 为 true,换成变量就是 false → 别拿常量表达式的结果推断运行时的浮点行为

package main
import "fmt"
func main() {
fmt.Println(0.1+0.2 == 0.3) // true
a, b := 0.1, 0.2
fmt.Println(a+b == 0.3) // false
}

时间格式写成 YYYY-MM-DD → 原样输出 → 用参考时间 2006-01-02 15:04:05 的写法,或用 time.DateOnly 这类常量

package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2026, 10, 8, 9, 5, 0, 0, time.UTC)
fmt.Println(t.Format("YYYY-MM-DD")) // YYYY-MM-DD
fmt.Println(t.Format("2006-01-02 15:04")) // 2026-10-08 09:05
fmt.Println(t.Format(time.DateOnly)) // 2026-10-08
}