附录 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 versiongo 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 --exactLinux 上用官方压缩包:解压到 /usr/local/go,再把它的 bin 加进 PATH;macOS 用官网的 .pkg 安装包
curl -LO https://go.dev/dl/go1.27.0.linux-amd64.tar.gzsudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.27.0.linux-amd64.tar.gzexport PATH=$PATH:/usr/local/go/bin多版本不用另装管理工具:go 命令自带工具链切换,GOTOOLCHAIN 指定版本时自动下载那个版本来运行
GOTOOLCHAIN=go1.26.0 go version项目里固定版本:go.mod 的 go 行是最低要求,也决定能用哪些语言特性;本机工具链比它旧时,go 命令自动下载够新的版本
只用本机装的版本,不自动下载
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 bpackage 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 extrapackage 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)}项目初始化与依赖管理
Section titled “项目初始化与依赖管理”新建模块:在项目目录里生成 go.mod;模块路径一般写代码仓库的地址,详见第 11 章
mkdir demo && cd demogo mod init example.com/demogo.mod:模块路径、Go 版本、依赖(本机 Go 1.27 生成,加了一个依赖之后)
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 -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 allgo list -m -u all没有 package.json 那样的 scripts,用 go 的子命令代替;项目要用的命令行工具用 tool 指令记进 go.mod(1.24 起),再用 go tool 运行
go get -tool golang.org/x/tools/cmd/stringergo 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 = 5const 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}基本类型与转换
Section titled “基本类型与转换”字符串转整数: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 章
package pricing
const taxRatePercent = 13 // 小写:只在 pricing 包里可见
func WithTax(price int) int { // 大写:导出 return price + price*taxRatePercent/100}
// main.gopackage 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同一个包的多个文件直接互相使用,不用导入
package main
func double(n int) int { return n * 2 }
// main.gopackage 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.goshop 以外的模块导入它,构建报错
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.txtvar hello string
func main() { fmt.Println(hello) // 你好}查看包和函数的文档
go doc strings.Cut迭代与生成器
Section titled “迭代与生成器”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}文件与 JSON
Section titled “文件与 JSON”整个读写文本文件: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
package calc
func Add(a, b int) int { return a + b }
// calc_test.gopackage 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 给每组起名,单独报告结果
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
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 登记其他清理工作
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 . 运行
package calc
import "testing"
func BenchmarkAdd(b *testing.B) { for b.Loop() { Add(1, 2) }}示例函数:// Output: 注释写预期输出,go test 会核对,go doc 生成的文档里也会显示
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 让引擎不断生成新输入
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格式化与 lint
Section titled “格式化与 lint”格式化:gofmt 随 Go 附带,风格没有可配置项;go fmt 对指定的包运行 gofmt -l -w
go fmt ./...只检查不改:列出格式不对的文件,适合放进 CI,有输出就算失败
gofmt -l .整理 import 并格式化:goimports,Go 团队维护,需要另外安装
go install golang.org/x/tools/cmd/goimports@latestgoimports -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@latestgo 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}