不教基础概念。下面每一节只回答三个问题:Go 里怎么写、和 C/C++/PHP/Java 差在哪、你最容易踩的坑是什么。所有代码在 1.27.2 下均可直接运行。
打开 PowerShell,执行(整个模块都在这个目录里练):
mkdir D:\go10 ; cd D:\go10 go mod init go10 # 生成 go.mod(内容见 §02) go version # 应输出 go version go1.27.x windows/amd64
| 概念 | Go 1.27 写法 | 你熟悉语言里的对应物 | 差异要点 / 坑 |
|---|---|---|---|
| 声明变量 | var n int = 5 或短变量 n := 5 | C: int n=5;;PHP: $n=5; | 类型写在变量名后面;短变量 := 只能在函数内用,包级变量必须用 var。 |
| 零值 | var s string 即 "";var p *int 即 nil | C/C++:野指针;Java:成员字段默认 null | Go 没有未初始化垃圾:数值 0、字符串 ""、指针/slice/map/channel/func/接口 = nil。这是特性不是缺陷。 |
| 常量 | const Pi = 3.14(无类型常量优先) | C: #define/const | 无 #define;枚举用 iota。 |
| 基本类型 | int int8 int16 int32 int64 uint float32 float64 bool byte rune string | C/C++ 几乎同名;Java 无 unsigned | int 宽度随平台(64 位机上是 64 位);跨平台二进制里别用 int 存长度,用 int64。byte=uint8,rune=int32(一个 Unicode 码点)。 |
| 类型转换 | f := float64(n) | C: (float)n | Go没有隐式数值转换——int + int64 必须显式转,否则编译错误(见 §10 报错 3)。 |
| 自定义类型 | type UserID int64 | C: typedef | 命名类型与底层类型之间仍需显式转换,但获得类型安全。 |
// D:\go10\01_var.go —— go run .\01_var.go package main import "fmt" func main() { var a int = 42 b := 3.14 // 短声明,类型自动推断为 float64 var s string // 零值 "",不是 null/野值 const Pi = 3.14159 fmt.Println(a, b, s == "", Pi) // 输出: 42 3.14 true 3.14159 // 没有隐式转换:int 与 float64 相加必须显式转 c := float64(a) + b fmt.Println(c) // 45.14 }
| 结构 | Go 写法 | 差异要点 |
|---|---|---|
| if / else | if x > 0 { ... } else { ... } | 条件不写括号;大括号强制(不能写成单行 if)。支持 if v := compute(); v > 0 {}——v 作用域只在 if/else 内。 |
| 循环 | 只有一个关键字:for | for i:=0; i<n; i++ {};当 while 用 for cond {};无限循环 for {}。没有 while、没有 do-while。 |
| switch | switch x { case 1: ...; case 2,3: ...; default: } | 默认自动 break(不像 C 穿透);想穿透用 fallthrough;case 可以是表达式(相当于 else-if 链);switch 后可不带表达式。 |
| goto / break label | 有 goto;break/continue 可带标签 | 标签用于嵌套循环跳出,比 C 干净。 |
| 三目运算符 | 没有 | 用 if/else 函数封装。这是故意的:可读性优先。 |
// D:\go10\02_func.go package main import ( "errors" "fmt" ) // 多返回值:(结果, 错误) 是全行业惯例 func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("b must not be zero") // 错误是普通值 } return a / b, nil } // 命名返回值 + defer(见 1.7) func parseInt(s string) (n int, err error) { defer func() { fmt.Println("defer: return", n, err) }() if s == "" { err = errors.New("empty input") // 命名返回值被 defer 看见 return } n = 7 return } func main() { v, err := divide(10, 0) if err != nil { // 错误必须显式处理,编译器盯着你 fmt.Println("error:", err) // error: b must not be zero } else { fmt.Println(v) } // "_" 丢弃返回值;但 error 永远不要随便丢 _, e := parseInt("") fmt.Println("err =", e) // err = empty input }
| 对比点 | Go | 你熟悉的语言 |
|---|---|---|
| 错误传递 | 多返回值 (T, error),调用方显式判 nil | Java/PHP:异常 throw/catch;C:errno/返回码 |
| 函数是值 | f := func(x int) int { return x*2 } | JS/PHP 闭包;C 函数指针 |
| 可变参数 | func sum(nums ...int),内部是 slice | C variadic;PHP ... |
| 没有 | 没有默认参数、没有重载、没有可选参数 | Java/PHP 的重载在 Go 里用"不同函数名"或 options struct 替代 |
// D:\go10\03_struct.go package main import "fmt" type User struct { Name string Age int } // 值接收者:操作副本(类似 Java 传值) func (u User) Greet() string { return "hi, " + u.Name } // 指针接收者:改原值。要修改状态就用指针接收者,全 struct 保持一致 func (u *User) Birthday() { u.Age++ } // 构造函数:Go 没有构造器惯例——写 NewXxx 函数 func NewUser(name string, age int) *User { return &User{Name: name, Age: age} } func main() { u := NewUser("Ada", 30) u.Birthday() fmt.Println(u.Greet(), u.Age) // hi, Ada 31 // 结构体字面量 p := struct{ X, Y int }{3, 4} fmt.Println(p) }
铁律:一个类型的所有方法,要么全用值接收者、要么全用指针接收者,不要混——混了会在接口实现上出问题(§03)。
// D:\go10\04_iface.go package main import "fmt" type Speaker interface { // 定义接口:一组方法签名 Speak() string } type Dog struct{ Name string } type Cat struct{ Name string } // Dog 实现了 Speak() —— 它就自动是 Speaker,什么都不用声明 func (d Dog) Speak() string { return d.Name + ": woof" } func (c Cat) Speak() string { return c.Name + ": meow" } func introduce(s Speaker) { fmt.Println(s.Speak()) } func main() { introduce(Dog{"Rex"}) // Rex: woof introduce(Cat{"Tom"}) // Tom: meow }
// D:\go10\05_defer.go package main import "fmt" func main() { fmt.Println("start") defer fmt.Println("defer 1") // 压栈:LIFO 顺序执行 defer fmt.Println("defer 2") fmt.Println("end") // 输出顺序: start → end → defer 2 → defer 1 }
f, _ := os.Open(p); defer f.Close()——哪怕中途 return 或 panic,Close 一定执行。defer fmt.Println(time.Now()) 记录的是 defer 注册时刻的时间,不是执行时刻。要延迟求值就包一层闭包。// D:\go10\06_ptr.go package main import "fmt" func inc(n int) { n++ } // 传值:外面不变 func incp(n *int) { *n++ } // 传指针:外面变 func main() { x := 10 inc(x) fmt.Println(x) // 10(副本+1) incp(&x) fmt.Println(x) // 11 p := &x *p = 99 fmt.Println(x) // 99 }
与 C 的差异:① 没有指针算术(p++ 编译不过)——这是 Go 安全模型的根基;② new(T) 返回 *T,但更常用 &T{...};③ GC 自动回收,不用 free;④ slice/map/channel/func 本身是引用语义的"描述符",传参时别再包一层指针。
// D:\go10\07_coll.go package main import ( "fmt" "strings" ) func main() { // string:UTF-8 只读字节序列。rune 遍历拿码点 s := "你好Go" for _, r := range s { fmt.Printf("%c ", r) } // 你 好 G o fmt.Println(len(s)) // 8:字节数不是字符数! // slice:动态数组 = (指针, len, cap)。append 是最常见的扩容场景 names := []string{"Ada", "Bob"} names = append(names, "Cid") fmt.Println(strings.Join(names, ",")) // Ada,Bob,Cid // make 预分配容量,避免反复扩容(性能常识) buf := make([]int, 0, 100) // map:哈希表,make 后才能写 m := make(map[string]int) m["apple"] = 3 v, ok := m["apple"] // comma-ok 判断存在,别用 ok==false 当错误 fmt.Println(v, ok) // 3 true }
| 坑 | 说明与对策 |
|---|---|
| slice 共享底层数组 | sub := s[2:5] 与 s 共享数组,改 sub 会改 s。需要独立副本就 copy() 或 append([]T(nil), s...)。 |
| append 越界静默 | append 超 cap 会分配新数组——把 slice 当返回值传出时,调用方必须接收新值 s = append(s, x)。 |
| len("中文")≠字数 | len 返回字节数;按字符用 range 或 unicode/utf8 包。 |
| nil map 写会 panic | var m map[string]int 读返回零值(不 panic),写直接 panic。先 make。 |
| map 并发写 | map 不是并发安全的,多 goroutine 写 → fatal error: concurrent map writes(见 §10 报错 4)。加锁或用 channel。 |
(T, error),用 errors.New/fmt.Errorf("...: %w", err) 创建,用 errors.Is/errors.As 判断类型。错误必须一路 wrap 到最上层打日志,禁止吞掉(_ = f.Close() 是例外惯例)。recover() 只在被 defer 的函数里能接住 panic。库函数不要 panic 给调用方;main 包顶层可以兜底一次。// error wrap 与判定(Go 1.13+ ,1.27 照用) err := fmt.Errorf("save user: %w", os.ErrNotExist) errors.Is(err, os.ErrNotExist) // true:沿 wrap 链找
Go 1.27 强制 module 模式(GOPATH 模式已死)。你只需要理解两个文件、五条命令、一个代理。
# D:\go10\go.mod(go mod init go10 自动生成,本模块零第三方依赖) module go10 go 1.27 # 将来引入 Gin 后会变成: # require github.com/gin-gonic/gin v1.12.0
| 文件 | 作用 | 类比 |
|---|---|---|
| go.mod | 模块声明:模块路径、Go 版本、直接依赖清单 | PHP composer.json / JS package.json |
| go.sum | 每个依赖版本的哈希校验,唯一事实来源,必须提交 git | composer.lock / pnpm-lock.yaml |
按版本纪律(08 §7):go.sum 入库即冻结,AI 建议升级默认拒绝。本模块所有示例只用标准库,零依赖——这是故意的:先把内核练熟,第三方框架(Gin v1.12.x / GORM v1.31.x)在 11 模块才登场。
cd D:\go10 # 1) 初始化模块(每个新项目只做一次) go mod init go10 # 2) 直接跑(单文件或包) go run .\01_var.go go run . # 3) 编译出 exe(输出到当前目录 go10.exe) go build -o go10.exe . # 4) 跑测试(见 §06) go test ./... go test -race ./... # 5) 整理依赖:补齐缺失、移除无用(引了新库后必跑) go mod tidy
# 只执行一次(写到用户级 go env 配置里) go env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,direct go env -w GOSUMDB=sum.golang.google.cn # 验证 go env GOPROXY
goproxy.cn 是七牛维护的国内模块代理,,direct 表示私有仓库直连。配完之后 go mod tidy 才不会卡在 proxy.golang.org(§10 报错 8)。
D:\go10\ ├── go.mod # module go10 ├── main.go # package main └── mathutil\ └── add.go # package mathutil // mathutil/add.go package mathutil func Add(a, b int) int { return a + b } // main.go 里 import:路径 = module 路径 + 目录相对路径 import "go10/mathutil" fmt.Println(mathutil.Add(1, 2)) // 3
package mathutil),与目录名一致是惯例。package xxx_test 外部测试包。本模块练习法:上面 01–07 的示例都放在 D:\go10\ 根目录时,由于它们都是 package main,不要在同一目录放多个 main.go——要么单文件 go run .\01_var.go,要么把每个示例放进自己的子目录。
接口在 §1.6 已经见过。这里补三块硬骨头:类型断言、空接口、Go 1.18+ 泛型(1.27 已是泛型第三代,写法稳定)。
// D:\go10\08_assert.go package main import "fmt" func describe(x any) { // any = interface{} 的别名(Go 1.18+) switch v := x.(type) { // 类型断言 + 类型 switch case int: fmt.Println("int:", v*2) case string: fmt.Println("string:", v) default: fmt.Printf("unknown: %T\n", x) // %T 打印动态类型 } } func main() { var i any = "hello" s, ok := i.(string) // comma-ok:断言失败 ok=false,不 panic fmt.Println(s, ok) // hello true n, ok := i.(int) // 失败:n=0, ok=false fmt.Println(n, ok) // 0 false describe(42) // int: 84 describe("go") // string: go }
x.(T) 失败会 panic——对外接口一律用 comma-ok 双值形式。any 是空接口的现代别名(Go 1.18+),新代码写 any。%T 打印动态类型,调试接口问题的第一工具。// D:\go10\09_generic.go package main import "fmt" // 泛型函数:[K comparable, V any] 是类型参数 + 约束 // comparable = 支持 == 比较(map 的 key 必须满足) func MapKeys[K comparable, V any](m map[K]V) []K { keys := make([]K, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } // 约束用 interface 声明:~int 表示"底层类型是 int 的所有类型" type Number interface { ~int | ~int64 | ~float64 } func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total } func main() { m := map[string]int{"a": 1, "b": 2} fmt.Println(MapKeys(m)) // [a b](顺序不定) fmt.Println(Sum([]int{1, 2, 3})) // 6 fmt.Println(Sum([]float64{1.5, 2.5})) // 4 }
| 概念 | 记法 |
|---|---|
| 类型参数列表 | 函数名后 [T any],结构体也可以 type Stack[T any] struct {...} |
| 内置约束 | any(任意类型)、comparable(可 == 比较) |
| 自定义约束 | type Number interface { ~int | ~float64 };~ 表示底层类型匹配 |
| 何时用 | 不要为了泛型而泛型:只有"同一份逻辑要跑在 3 种以上类型上"时才引入;否则用接口或直接写具体类型。 |
你的 Java 直觉:Go 泛型 ≈ Java 泛型的类型参数部分,但没有类型擦除的运行时包袱;Go 的约束是"接口里放方法签名",比 Java 的 extends 更灵活。
这是 Go 的立身之本,也是你从 C/C++ 过来最需要重建直觉的部分。一句总纲:不要用共享内存来通信,要用通信来共享内存。
// D:\go10\10_goroutine.go package main import ( "fmt" "time" ) func say(msg string) { for i := 0; i < 3; i++ { fmt.Println(msg, i) time.Sleep(100 * time.Millisecond) } } func main() { go say("worker") // go 关键字 = 异步启动,立刻返回 say("main") // 主线程同时跑 // main 一返回,所有 goroutine 直接被杀——所以这里靠 say 阻塞住 }
// D:\go10\11_channel.go package main import "fmt" func main() { // 无缓冲 channel:发送与接收必须"碰面"才继续 ch := make(chan string) go func() { ch <- "hello from goroutine" // 发送 close(ch) }() msg := <-ch // 接收 fmt.Println(msg) // hello from goroutine // range 读取直到 close numCh := make(chan int, 3) // 缓冲 channel:容量 3 numCh <- 1; numCh <- 2; numCh <- 3 close(numCh) for n := range numCh { fmt.Print(n, " ") // 1 2 3 } }
| 操作 | 行为 | 坑 |
|---|---|---|
| 发送到已 close 的 channel | panic | 只有发送方负责 close;接收方不要 close。 |
| 接收已 close 的 channel | 立即返回零值 + ok=false | 用 v, ok := <-ch 判断是否已关。 |
| 向 nil channel 收发 | 永久阻塞 | select 里常用这个技巧做"停用某个分支"。 |
| 无缓冲 channel | 同步会合 | 发送方会阻塞到有人接收。 |
// D:\go10\12_select.go package main import ( "fmt" "time" ) func main() { tick := time.Tick(100 * time.Millisecond) boom := time.After(350 * time.Millisecond) for { select { case <-tick: fmt.Print("tick ") case <-boom: fmt.Println("\nBOOM") return } } // 输出: tick tick tick BOOM }
// D:\go10\13_sync.go package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup var mu sync.Mutex counter := 0 for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() // 保护 counter:不加锁就 data race counter++ mu.Unlock() }() } wg.Wait() // 等 100 个 goroutine 全部结束 fmt.Println(counter) // 100 }
Add(n) 必须在 go 之前调用(在 goroutine 内 Add 会 race);Done() 用 defer 挂;Wait() 阻塞到归零。atomic 包用于单个计数器的无锁加减,比 Mutex 更轻。// D:\go10\14_context.go —— 三个核心 API 一眼看懂 package main import ( "context" "fmt" "time" ) func worker(ctx context.Context, name string) { select { case <-time.After(2 * time.Second): fmt.Println(name, "done") case <-ctx.Done(): // 监听取消信号 fmt.Println(name, "canceled:", ctx.Err()) } } func main() { // 1) WithCancel:手动取消 ctx1, cancel1 := context.WithCancel(context.Background()) go worker(ctx1, "c1") cancel1() // 立刻取消 // 2) WithTimeout:到点自动取消 ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel2() // 哪怕没超时也必须调 cancel,释放资源 go worker(ctx2, "c2") // 3) WithValue:请求级数据(只放请求域数据,不要当参数袋) ctx3 := context.WithValue(context.Background(), "trace-id", "abc-123") fmt.Println("trace-id =", ctx3.Value("trace-id")) time.Sleep(500 * time.Millisecond) }
ctx context.Context;② WithCancel/WithTimeout 返回的 cancel 函数必须调用(哪怕 defer),否则泄漏 goroutine;③ 不要把 context 存进结构体,请求一来就往下传。这是本模块的"毕业作品",也是 §09 验收标准的默写对象。下面是完整可运行的 stdlib 版本,零第三方依赖。逐文件复制。
D:\grace\ ├── go.mod └── main.go
module grace go 1.27
// D:\grace\main.go
package main
import (
"context"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
// ① 根 context:Ctrl+C 或 kill 信号到达时自动取消
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// ② 业务 handler:每个请求都带自己的 ctx
mux := http.NewServeMux()
mux.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) {
reqCtx := r.Context() // 客户端断开 → reqCtx 自动取消
log.Println("work start")
select {
case <-time.After(2 * time.Second):
w.Write([]byte("work done\n"))
log.Println("work finished")
case <-reqCtx.Done():
log.Println("work canceled:", reqCtx.Err())
}
})
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok\n"))
})
srv := &http.Server{Addr: ":8080", Handler: mux}
// ③ 异步启动 HTTP 服务(ListenAndServe 是阻塞的)
go func() {
log.Println("listening on http://localhost:8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server crashed: %v", err)
}
}()
// ④ 主 goroutine 挂起,等信号到达
<-ctx.Done()
log.Println("shutdown signal received, draining ...")
// ⑤ 宽限 5 秒:等在飞请求处理完,再退出
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("graceful shutdown error: %v", err)
}
log.Println("server exited gracefully")
}
# 窗口 A:启动服务 cd D:\grace go run . # 窗口 B:发一个慢请求(2 秒才返回) curl.exe http://localhost:8080/work # 窗口 B 立刻再开:健康检查 curl.exe http://localhost:8080/healthz
然后:在窗口 B 的 /work 还没返回时,到窗口 A 按 Ctrl+C。你应该看到:
shutdown signal received, draining ... 打印;server exited gracefully,进程干净退出(而不是硬杀留下半截响应)。为什么这个例子值得默写:它同时覆盖了 ① goroutine 启动服务 ② signal 接 ctx ③ handler 用 r.Context() 响应客户端断开 ④ WithTimeout 做退出宽限 ⑤ Shutdown 优雅收尾——五个高频知识点一次串通。这就是 11 工作台实战里 main.go 的雏形。
Go 自带 testing 包,不需要任何第三方框架。掌握三件事:表驱动写法、HTTP 测试、race 检测器。
// D:\grace\math_test.go(与 main.go 同目录,package main) package main import "testing" func add(a, b int) int { return a + b } func TestAdd(t *testing.T) { cases := []struct { name string a, b int want int }{ {"1+1", 1, 1, 2}, {"负数", -1, -2, -3}, {"零", 0, 0, 0}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { // 子测试:单独跑某一条 if got := add(c.a, c.b); got != c.want { t.Errorf("add(%d,%d) = %d, want %d", c.a, c.b, got, c.want) } }) } }
go test -v ./... # -v 打印每个子测试 go test -run TestAdd -v # 只跑匹配的测试
// D:\grace\handler_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthz(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rec := httptest.NewRecorder()
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok\n"))
})
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("code = %d", rec.Code)
}
if body := rec.Body.String(); body != "ok\n" {
t.Errorf("body = %q", body)
}
}
# 在你的并发代码上必跑一次 go test -race ./... # 如果有竞争,你会看到类似: # WARNING: DATA RACE # Read at 0x00c0001a2xxx by goroutine 7: # previous write ...
-race 是 Go 赠送的免费正餐:你的 C/C++ 经验里要靠 ThreadSanitizer 才能做的事,这里 go test -race 一条命令。提交任何并发代码前必须过 -race。
你的 C/C++ 直觉会想上 perf/VS Profiler——Go 标准库自带 net/http/pprof,挂一行 import 就能采 CPU/堆/goroutine 数据。
import ( _ "net/http/pprof" // 匿名导入:自动注册 /debug/pprof 路由 ) func main() { // 另起一个端口专给 pprof(不对外暴露) go func() { log.Println("pprof on :6060") _ = http.ListenAndServe("localhost:6060", nil) }() // ... 原服务代码不变 }
# 1) 看一次堆快照(哪块内存多) go tool pprof http://localhost:6060/debug/pprof/heap # 进入 pprof 交互界面后: (pprof) top # 占用最高的 10 个函数 (pprof) list main # 看 main 包里哪一行烧内存 (pprof) web # 生成调用图(需装 graphviz) (pprof) exit # 2) 采 30 秒 CPU(哪段代码烧 CPU) go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 # 3) 看 goroutine 泄露 go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2
入门套路:先 heap 后 CPU——内存问题看 heap,速度问题看 profile。阶段四(机制深潜)会再带你读 runtime 源码,本阶段只要做到"会挂、会采、会 top"就够。
与 08 总纲 §3 的每日模板对齐:每天 8 个 30 分钟块。理论只读 1 块,其余 7 块在敲代码。
| 天 | 对应小节 | 块 1(理论 30min) | 块 2–7(编码 3.5h) | 块 8(验收 30min) |
|---|---|---|---|---|
| D1 | §1.1–1.3 | 读变量/类型/控制流对照 | 敲 01_var.go、写 3 个小程序(摄氏度换算、打印乘法表、判断闰年) | 能默写 if/for/switch 与 C 的差异 5 条 |
| D2 | §1.4–1.5、1.7 | 读多返回值/defer | 敲 02_func.go、03_struct.go、05_defer.go;给 divide 加一个"除零错误用 errors.Is 判定"的分支 | 能解释"为什么 Go 没有重载"和 defer 的 LIFO |
| D3 | §1.6、1.8–1.10 | 读接口/指针/错误哲学 | 敲 04_iface.go、06_ptr.go、07_coll.go;把 Dog/Cat 改成实现一个新接口 Stringer | 能说出 slice 与数组的区别、map 为什么不能并发写 |
| D4 | §2 包与模块 | 读 go.mod/go.sum/五条命令 | 建 D:\grace 结构;写 mathutil/add.go 并在 main 里 import;配 GOPROXY=goproxy.cn | go run . 能跑出 add 结果;go env GOPROXY 输出正确 |
| D5 | §3 接口与泛型 | 读类型断言/泛型约束 | 敲 08_assert.go、09_generic.go;自己写一个泛型 Contains[T comparable]([]T, T) bool | 写得出 MapKeys 且能解释 comparable 约束 |
| D6 | §4.1–4.4 并发 | 读 goroutine/channel/select/sync | 敲 10–13 四个示例;把 13 的 Mutex 去掉跑一次,用 go test -race 抓出竞争 | 能解释 WaitGroup.Add 为什么要在 go 之前调 |
| D7 | §4.5 + §05 | 读 context 三兄弟 | 敲 14_context.go;把 §05 的 graceful server 完整敲一遍并跑通验收动作 | Ctrl+C 后看到 "server exited gracefully" |
| 天 | 对应小节 | 任务(4h) | 当日验收 |
|---|---|---|---|
| D8 | §4 加深 | 把 graceful server 的 /work handler 改成"起一个 goroutine 跑任务,任务 ctx 取消就退出" | 客户端断开 curl 时,服务日志打印 work canceled |
| D9 | §6.1 表驱动 | 给 mathutil/add.go 和自己写的 Contains 补表驱动测试 | go test -v ./... 全绿,子测试逐个可见 |
| D10 | §6.2–6.3 | 给 /healthz 和 /work 写 httptest;跑 go test -race ./... | race 报告干净(无 DATA RACE 字样) |
| D11 | §7 pprof | 挂 pprof 到 :6060;在 /work 里故意制造一个内存热点(append 不预分配),用 heap top 找到它 | 能在 pprof top 里指出热点函数并修复 |
| D12 | 综合项目 | 写一个 CLI:并发worker池(5 个 worker 从 channel 取任务,context 取消时全部退出) | 按 Ctrl+C 后所有 worker 3 秒内退出,无 goroutine 泄露 |
| D13 | §9 默写 | 关掉教程,白纸默写 graceful server;对照 §05 找漏;重写漏的部分 | 默写版一次 go run . 跑通 + Ctrl+C 优雅退出 |
| D14 | 沉淀 | 把 graceful server、表驱动测试、pprof 三行、worker 池 4 段代码写进四件套的 snippets/;把"context 军规""WaitGroup 规则"写进 patterns/ | 资产库新增 ≥4 个片段 + 2 个模式;准备进入 11 工作台实战 |
| 阶段 | 天数 | 主题 | 里程碑 |
|---|---|---|---|
| 第 1 周 | D1–D3 | 语法对照 + 错误哲学 | 所有示例敲通,能讲差异 |
| D4–D5 | 模块 + 接口泛型 | go mod 工作流肌肉记忆 | |
| D6–D7 | 并发 + 优雅停机 v1 | graceful server 跑通 | |
| 第 2 周 | D8–D11 | 测试 + race + pprof | go test ./... 全绿 + race 干净 |
| D12–D13 | worker 池 + 默写 | 白纸默写验收通过 | |
| D14 | 四件套沉淀 | 进入 11 工作台实战 |
08 总纲给阶段一的验收只有一句话:"能默写带 context 取消与优雅停机的并发服务"。下面是它的展开版——12 项全过才算本模块毕业。
| # | 自测项(白纸/关教程完成) | 通过标准 |
|---|---|---|
| 1 | 默写 graceful server:signal.NotifyContext 接 SIGINT/SIGTERM | Ctrl+C 后打印 shutdown 日志,进程 5 秒内退出码 0 |
| 2 | server.Shutdown 带 WithTimeout 宽限 | 在飞 /work 请求能在日志里看到 canceled 或 finished |
| 3 | handler 里用 r.Context() 响应客户端断开 | curl 中途关掉,服务日志打印 work canceled |
| 4 | go run / go build / go test / go mod tidy 五条命令不查文档 | 闭卷写得出完整命令 |
| 5 | 解释 GOPROXY=goproxy.cn,direct 每一段含义 | 讲得出"国内代理 + 私有直连" |
| 6 | 写一个表驱动测试(≥3 个子测试) | go test -v 子测试名逐个打印 |
| 7 | 写一个 httptest 测 /healthz | 不开真服务器也能断言 200 + body |
| 8 | 跑 go test -race 且无 DATA RACE | 输出含 "0 races detected" |
| 9 | 挂 pprof 并采集一次 heap top | 能进入交互界面并 top 出结果 |
| 10 | 解释 WaitGroup.Add 为什么必须在 go 之前 | 讲得出"否则 wg.Wait 可能等不到"的 race 原理 |
| 11 | 说出 error 与 panic 的使用边界 | 讲得出"可预期失败用 error,bug 用 panic,库函数不 panic 给调用方" |
| 12 | 把本模块 4 段代码沉淀进四件套 snippets | assets/snippets/ 下新增文件存在 |
补考规则:任何一项不过,回到 §8 对应那一天重练,不要带着漏洞进 11 工作台——11 模块的 main.go 就是 §05 的扩展版,这里默写不过去,11 会卡死。
闭关期间 90% 的卡点都在这张表里。卡住了先查这张,不要跳出教程。
| 报错原文(节选) | 原因 | 解决 |
|---|---|---|
go: go.mod file not found in current directory or any parent directory. ... |
当前目录不在 Go module 里 | 先 cd 到项目根(go.mod 所在目录);或 go mod init 模块名 新建。 |
undefined: xxx 或 xxx declared and not used |
变量/函数没定义,或定义了没用(编译器硬规则) | 检查拼写与大小写(小写=包私有);没用的变量要么用掉要么改成 _ = xxx;import 了没用的包同理。 |
cannot use x (variable of type int) as float64 value in argument to ... |
Go 无隐式数值转换 | 显式转换:float64(x)。这不是 bug,是设计。 |
fatal error: concurrent map writes |
两个 goroutine 同时写同一个 map | ① 读写都包 sync.Mutex;② 改用 channel 传递;③ 用 sync.Map(读多写少场景)。 |
fatal error: all goroutines are asleep - deadlock! |
主 goroutine 在等一个永远不会到来的 channel 发送 | 检查:是不是忘了 go func() 起发送方?是不是该 close(channel) 却没 close?main 直接 <-ch 而没人发。 |
missing go.sum entry for module providing package ...; to add: go mod tidy |
go.sum 与 go.mod 不同步 | 直接照提示跑 go mod tidy。clone 别人的仓库后第一次 build 前必跑。 |
dial tcp localhost:8080: connect: connection refused(或 context deadline exceeded) |
服务没起来 / 端口错 / 客户端 ctx 超时 | ① 确认 go run . 已打印 listening;② 端口与服务端一致;③ 若是 context 超时,看是不是 WithTimeout 给短了。 |
get https://proxy.golang.org/...: i/o timeout 或 unrecognized import path |
没配国内代理,直连 proxy.golang.org 被墙 | go env -w GOPROXY=https://goproxy.cn,direct(见 §2.3),然后重试 tidy。 |
panic: runtime error: invalid memory address or nil pointer dereference |
对 nil 指针/接口解引用 | 调用链上找哪个返回了 nil(多半是 error 非 nil 却没判,继续用了返回值)。养成 if err != nil 先行。 |
import ... is a program, not an importable package |
试图 import 一个 package main 的目录 | 可复用代码必须放在 package xxx(非 main)的目录;main 包只放可执行入口。 |
expression in go func literal should not be func literal 类 vet 警告 / GOFLAGS 相关报错 |
vet 静态检查不通过,或 GOFLAGS 环境变量残留旧值 | 按提示改代码;go env GOFLAGS 看残留,go env -u GOFLAGS 清除。 |
signal: killed / Windows 下 exit status 0xffffffff |
进程被外部强杀(OOM 或手动 close 端口) | 不是代码错。若是 graceful server 调试,确认你按的是 Ctrl+C 而不是关窗口——关窗口就收不到 SIGINT。 |
还卡住?把完整报错原文 + 复现命令 + 相关代码段,按 08 §6 的"打包证据给 AI"流程处理——不要凭感觉瞎改。
本页所有结论的外部事实来源如下;教学结构(14 天计划、对照表写法、验收清单)为编者基于 07/08 的执行化设计。
| Go 1.27.2 版本与发布节奏(1.27.0=2026-08-19;1.27.2=2026-10-08) | https://go.dev/dl/ · https://go.dev/doc/devel/release |
| Go 语言规范(类型、控制流、方法、接口、defer) | https://go.dev/ref/spec |
| A Tour of Go(语法对照示例基准) | https://go.dev/tour/welcome/1 |
| Effective Go(错误处理、接口惯用法) | https://go.dev/doc/effective_go |
| Go Modules 参考(go.mod/go.sum、go mod tidy) | https://go.dev/ref/mod |
| goproxy.cn 官方说明(国内模块代理) | https://github.com/goproxy/goproxy.cn |
| Go 泛型教程(类型参数、约束、comparable) | https://go.dev/doc/tutorial/generics |
| context 包文档(WithCancel/WithTimeout/WithValue) | https://pkg.go.dev/context |
| sync 包文档(WaitGroup/Mutex) | https://pkg.go.dev/sync |
| http.Server.Shutdown 优雅停机官方示例 | https://pkg.go.dev/net/http#Server.Shutdown |
| testing 包与表驱动测试惯例 | https://pkg.go.dev/testing · https://go.dev/doc/tutorial/add-a-test |
| go test -race 数据竞争检测官方文档 | https://go.dev/doc/articles/race_detector |
| net/http/pprof 官方文档 | https://pkg.go.dev/net/http/pprof |
| Go 生态锁定版本(Gin v1.12.0 / GORM v1.31.2 / glebarez v1.11.0 等,本模块未直接引用,仅列基线) | https://pkg.go.dev/ 各模块页(详见 09 版本总表) |
(编者整理:本页 14 天计划、对照表、验收清单、报错表为基于 07 报告与 08 总纲的执行化设计,无外部独立来源。)