🧭 编程学习站

Go 入门 + 进阶训练

公网版 · 手机/电脑自适应

纯新手先看这一框

Go 适合做什么? 高并发服务端(微服务、API 网关、消息中间件)、云计算基础设施(Docker/Kubernetes 都是 Go 写的)、DevOps 工具、CLI 命令行程序。云原生时代它是事实标准语言。

为什么学它? 语法极简、编译飞快、天生支持并发(goroutine)、部署只要一个静态二进制文件(不用装运行时)。团队上手成本低,很适合写「稳、快、好部署」的后端。

怎么跑起来? ① 本页示例点 ▶ 运行 直接看结果;② 装 Go(go.dev 下 1.22+),把代码存成 main.go,命令行 go run main.go;③ go build 编译出可执行文件。

新手最常踩的坑? 没有「类继承」和「三元运算符」、错误处理要手写 if err != nilfor 是唯一循环、切片 append 可能共享底层数组。下面每节都会标红。

1. Hello World 与运行先跑通建立信心

Go 是编译型语言,但 go run 把「编译+运行」一步完成,体验类似脚本。

package main            // 可执行程序必须叫 main

import "fmt"            // 导入格式化输出包

func main() {           // 程序入口
    fmt.Println("Hello, World!")
    fmt.Printf("2 + 3 = %d\n", 2+3)
}
老师不教每个 Go 文件开头都要声明 packagemain 包 + main() 函数才是可执行入口;其他包是「库」。未使用的导入和变量会编译报错(这是 Go 的强制整洁机制,新手一开始会烦,久了真香)。

运行方式

  • go run main.go:编译到临时目录并运行,不留下文件。
  • go build:生成同名可执行文件,直接 ./main 运行。
  • go fmt ./...:按官方风格自动格式化(别手写格式,交给工具)。

2. 变量与常量类型在后面,能推断就推断

var x int = 10          // 完整写法
var y = 20              // 类型推断
z := 30                 // 短声明(函数内专用,最常用)

const PI = 3.14159      // 常量
// PI = 3               // 报错:常量不能改

// iota:常量计数器,自动生成递增序列
const (
    A = iota            // 0
    B                   // 1
    C                   // 2
)
易错点:= 只能在函数内部用;包级别变量必须用 var。同作用域里 x := 1 后再 x := 2 会编译错——但 x, err := f() 只要有一个是新变量就允许(这是「短变量重声明」规则)。

3. 基本类型零值 + 无隐式转换

类型说明零值
int/int64整数(平台相关/固定64位)0
float64浮点(默认用 64)0
stringUTF-8 不可变字节序列""
booltrue/falsefalse
runeint32 别名,一个 Unicode 码点0
var a int = 5
var b int64 = 10
// c := a + b          // 编译错!int 和 int64 不能混算
c := int64(a) + b     // 必须显式转换
易错点Go 没有隐式类型转换。不同数值类型相加、赋值都要手动转。数字转字符串用 strconv.Itoa / strconv.FormatInt,别用 string(65)(那会当成 Unicode 码点变成 "A")。

4. 流程控制只有 for,没有 while/三元

// if 条件不加括号
if x > 10 {
    fmt.Println("大")
} else if x > 5 {
    fmt.Println("中")
} else {
    fmt.Println("小")
}

// for 是唯一的循环
for i := 0; i < 5; i++ {        // 经典三段式
    fmt.Println(i)
}
for x < 10 {                    // 当 while 用(条件前置)
    x++
}
for {                           // 死循环,配 break 用
    break
}

// switch 自动 break,不用写 case 后 break
switch day {
case "sat", "sun":
    fmt.Println("周末")
default:
    fmt.Println("工作日")
}
易错点Go 没有三元运算符 a?b:c 不存在,老老实实写 if/else。for-range 遍历时取到的 v 是元素的副本,改 v 不影响原切片;想改原值用下标 for i := range s { s[i] = ... }

5. 数组与切片切片才是日常,数组很少直接用

切片(slice)是 Go 最常用的序列类型,底层是个「指向数组的窗口」,含指针/长度/容量三部分。

s := []int{1, 2, 3}        // 切片字面量
s = append(s, 4)           // 追加 → [1 2 3 4]
fmt.Println(s[1:3])        // [2 3] 切片(共享底层数组)

// make 创建:长度3,容量5
buf := make([]int, 3, 5)
fmt.Println(len(buf), cap(buf))   // 3 5
经典巨坑append 不一定新建底层数组!当原切片容量足够时,append 会复用底层数组并覆盖后面的元素。如果两个切片共享底层数组,改一个会「串」到另一个。安全做法:用 copymake + 重新切片隔离。避坑口诀:append 之后别再依赖旧切片的内容。
老师不教切片的「零值」是 nil(len=cap=0),但 nil 切片也能 appendrange,不用像某些语言先判空。判断「是否为空」用 len(s) == 0,别用 s == nil(空切片可能非 nil)。

6. map键值表,键类型有限制

m := make(map[string]int)
m["apple"] = 3
m["banana"] = 5
fmt.Println(m["apple"])        // 3

// 取键值 + 判断是否存在("comma ok" 惯用法)
v, ok := m["cherry"]
if ok {
    fmt.Println(v)
} else {
    fmt.Println("没有")
}

delete(m, "apple")             // 删除键
易错点未初始化的 map 是 nil,直接赋值会 panic!必须用 make 或字面量初始化。map 的键必须是可比较类型(string/int/struct 等),切片、map、func 不能做键。并发读写 map 会直接 panic——多 goroutine 访问要加锁或用 sync.Map

7. 函数多返回值 + defer + 变参

// 多返回值(Go 的标配)
func divmod(a, b int) (int, int) {
    return a / b, a % b
}
q, r := divmod(7, 3)

// 命名返回值 + defer
func greet(name string) (msg string) {
    defer fmt.Println("执行结束")     // 函数 return 前最后执行
    msg = "你好, " + name
    return                              // 直接返回命名的 msg
}

// 变参
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
fmt.Println(sum(1, 2, 3))
易错点defer 的执行顺序是后进先出(栈);而且 defer 后面的函数参数在声明时就求值,不是执行时。想延迟读变量要包一层闭包:defer func(){ fmt.Println(i) }()

8. 指针比 C 安全:无指针运算、有 GC

x := 10
p := &x              // p 是 *int,指向 x
fmt.Println(*p)     // 10  解引用
*p = 20
fmt.Println(x)      // 20  x 被改了

// 函数内想改外部变量,传指针
func inc(n *int) { *n++ }
inc(&x)
老师不教Go 的指针不能做加减法(不能 p+1 偏移),也没有悬空指针野指针问题——有垃圾回收(GC)自动管理。返回局部变量地址是安全的:func f() *int { x:=1; return &x } 编译器会把 x 逃逸到堆上。这点和 C 完全不同,放心用。

9. 结构体与方法组合优于继承

type User struct {
    Name string
    Age  int
}

// 值接收者(不改原对象)
func (u User) SayHi() string {
    return "我是 " + u.Name
}
// 指针接收者(可改原对象,大对象也该用指针避免拷贝)
func (u *User) Grow() {
    u.Age++
}

u := User{"小明", 18}
fmt.Println(u.SayHi())
u.Grow()
fmt.Println(u.Age)      // 19

// 组合("继承"的替代)
type Admin struct {
    User            // 匿名嵌入,Admin 直接拥有 User 的字段和方法
    Level int
}
易错点方法接收者选值还是指针:要修改对象、或结构体很大(拷贝贵),用 *T;否则用 T。但同一类型的方法集要一致,别混着来导致接口实现判断混乱。Go 没有继承,用「结构体嵌入」实现复用,这是刻意的设计——避免继承层级爆炸。

10. 接口隐式实现,小接口为王

Go 接口是隐式实现的:类型只要实现了接口要求的方法,就自动算实现了该接口,不用 implements 声明。

type Speaker interface {
    Speak() string
}

type Dog struct{}
func (d Dog) Speak() string { return "汪汪" }

func announce(s Speaker) {       // 接收任意 Speaker
    fmt.Println(s.Speak())
}
announce(Dog{})                  // Dog 自动满足 Speaker

// 空接口 interface{} 可装任何类型(Go1.18+ 用 any)
var anything any = 42
anything = "hello"
易错点对接口变量用类型断言取具体类型:v, ok := i.(string),用 comma ok 避免断言失败 panic。interface{}(即 any)装了指针后判等是比地址不是比内容,别踩坑。标准库里 error 本身就是个接口。

11. 并发:goroutine + channelGo 的杀手锏

goroutine 是轻量协程(几 KB 栈,可开百万个);channel 是 goroutine 之间传值的管道。Go 名言:「不要通过共享内存来通信,要通过通信来共享内存。」

package main

import (
    "fmt"
    "time"
)

func worker(id int, ch chan int) {
    for n := range ch {            // 从 channel 收,关了就退出
        fmt.Printf("worker %d 收到 %d\n", id, n)
    }
}

func main() {
    ch := make(chan int, 3)        // 缓冲大小 3
    go worker(1, ch)               // 启动一个 goroutine
    for i := 1; i <= 3; i++ {
        ch <- i                    // 往 channel 发
    }
    close(ch)
    time.Sleep(time.Millisecond)   // 等 worker 打印完
}
易错点goroutine 泄漏:往没人收的 channel 发数据会永久阻塞。向已关闭的 channel 发送会 panic。主函数退出不会等 goroutine——要用 sync.WaitGroupchannel 同步,否则 goroutine 可能没跑完程序就结束了。
老师不教无缓冲 channel 的发送和接收会互相阻塞配对(同步点);带缓冲的则缓冲满才阻塞。生产代码用 context.Context 来取消 goroutine 和设超时,而不是依赖 time.Sleep 这种凑合办法。

12. 错误处理error 是值,不是异常

import (
    "errors"
    "fmt"
)

func mayFail(n int) (int, error) {
    if n < 0 {
        return 0, errors.New("n 不能为负")
    }
    return n * 2, nil
}

func main() {
    r, err := mayFail(-1)
    if err != nil {              // Go 的标配:显式检查
        fmt.Println("出错:", err)
        return
    }
    fmt.Println(r)
}
易错点Go 没有 try/catch。错误是普通返回值,必须 if err != nil 检查——这是 Go 最被吐槽也最被喜爱的地方。把 err 静默忽略(_ = f())是常见 bug 来源。Go 1.13+ 用 errors.Is/errors.As 判断包装过的错误,用 fmt.Errorf("...: %w", err) 包装上层错误。
老师不教panic 是给「真不可能发生」的致命错误用的(类似异常),正常业务流程别用。用 defer func(){ if r := recover(); r != nil { ... } }() 在 goroutine 顶层兜住 panic,防止一个 goroutine 崩了拖垮整个进程(尤其是 HTTP handler 里)。

13. 包管理与可见性大写才导出

// go.mod 是项目清单
// module example.com/hello
// go 1.22

package mathutil

func Add(a, b int) int { return a + b }   // 大写:对外可见(导出)
func secret() int { return 42 }           // 小写:包内私有
建议可见性规则极简:首字母大写 = 导出(public),小写 = 包内私有(private)。没有 public/private 关键字。用 go mod init 名字 初始化模块,go get 拉依赖,全部记录在 go.mod / go.sum

14. 标准库与 context开箱即用的云原生工具链

Go 标准库异常强大,很多场景不用第三方包:

用途
net/http写 Web 服务/客户端(见实战节)
encoding/jsonJSON 序列化(结构体加 json tag)
strings/strconv字符串处理/类型转换
time时间、时区、定时器
context取消、超时、传值(贯穿并发)
syncWaitGroup/Mutex/Once/Map
易错点context.Context 的取消信号要沿着调用链一直传下去,别传 nil(用 context.Background() 作根)。JSON 字段想导出,结构体字段必须大写并加 `json:"name"` tag,否则序列化时丢字段。

15. 测试与基准内置 testing,无需框架

// add_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
    if Add(2, 3) != 5 {
        t.Fatal("Add 算错了")
    }
}

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {   // b.N 由测试框架决定跑多少次
        Add(2, 3)
    }
}
建议测试文件命名 xxx_test.go,函数 TestXxx/BenchmarkXxx。跑:go test ./...、基准 go test -bench=. -benchmem。配合 -race 标志能检测数据竞争(并发 bug 神器)。

16. 实战:并发 Web 服务拼起前面所有知识点

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
        defer cancel()
        select {
        case <-time.After(100 * time.Millisecond):
            fmt.Fprintln(w, "pong")
        case <-ctx.Done():
            http.Error(w, "超时", http.StatusGatewayTimeout)
        }
    })
    fmt.Println("监听 :8080")
    http.ListenAndServe(":8080", mux)
}
老师不教这就是一个真实的并发 HTTP 服务:每个请求由独立 goroutine 处理,天然支持高并发;用 context 做请求级超时;编译后是单个二进制,./server 直接跑,没有任何运行时依赖——这正是 Go 在云原生里横着走的原因。

🧪 动手实验室代码已写好,点 ▶ 直接看结果(手机/电脑通用)

下面每个程序都已预填好。点 ▶ 运行 看输出;想改就在框里改再运行;右上角可切语言、↺ 重置。

① 基础:打印与循环

② 切片与 append

③ 函数:多返回值

④ 结构体与方法

⑤ 并发:goroutine + channel

💡 连老师都不会教你的干货(工业级技巧)学校不教,公司天天用

  1. 无隐式转换:intint64 不能混算,必须显式转;数字转字符串用 strconv,别用 string(65)(那是码点 'A')。
  2. 切片 append 的共享坑:容量够时 append 复用底层数组,可能「串」改其他切片。需要隔离就用 copy 或重新 make
  3. range 取的是副本:遍历时 v 是元素拷贝,改 v 不动原切片;改原值用下标 for i := range s
  4. defer 是栈 + 即时求值:多个 defer 后进先出;参数在 defer 声明时就定下,想延迟读变量要包闭包。
  5. 零值可用:未初始化的变量自动是零值(0/""/false/nil),nil 切片/map 也能 range 和 append;但 map 必须先 make 才能写。
  6. goroutine 泄漏:往没人收的 channel 发数据会永久阻塞;用 context 传递取消信号,主函数退出不等 goroutine。
  7. 通信优于共享内存:多 goroutine 协作优先用 channel,而非加锁共享变量;加锁也优先 sync 而非自己造轮子。
  8. 接口隐式实现:不用 implements,只要方法集匹配就自动满足;优先定义小而专的接口(io.Reader 这种),别搞大而全的接口。
  9. error 是值:异常流程用多返回值 error + if err != nil,别滥用 panic;用 %w 包装、errors.Is/As 判断。
  10. 调度器 GMP:Go 用 G(goroutine)/M(线程)/P(处理器)的 M:N 调度,默认并行度 = CPU 核数(GOMAXPROCS),理解它能解释很多性能现象。
  11. map 并发会 panic:多 goroutine 读写同一 map 直接崩,用 sync.Mapsync.RWMutex 保护。
  12. 方法接收者选择:要改对象/结构体大 → 指针接收者;只读且小 → 值接收者;同一类型别混用。
  13. 可见性靠大小写:首字母大写导出、小写私有,没有 private 关键字;包名要短而准。
  14. 工具链即规范:gofmt 统一格式、go vet 抓可疑代码、-race 查竞争、golangci-lint 做团队 lint,CI 里全跑。
  15. string 拼接用 strings.Builder:循环里拼大量字符串用 strings.Builderbytes.Buffer,比 += 少分配很多次。
  16. context 别传 nil:根用 context.Background(),沿调用链传递取消与超时;请求处理里用 r.Context()
  17. 部署零依赖:CGO_ENABLED=0 go build 出纯静态二进制,丢到任何同架构 Linux 直接跑,容器镜像可以小到几 MB(scratch 基础镜像)。

📝 自测题点「查看答案」核对,先自己想

1. 以下代码输出什么?

s := []int{1, 2, 3}
s2 := append(s, 4)
s[0] = 99
fmt.Println(s2[0])
查看答案

可能输出 99 也可能不是——取决于 append 时是否扩容。容量 3 的切片 append 后容量变 6(翻倍),此时会分配新底层数组,s2 不共享,输出 1;但若原容量足够复用,就会输出 99。结论:append 后不要再依赖旧切片内容。

2. Go 里如何表示「三目运算符」a?b:c

查看答案

没有。Go 刻意不提供三元运算符,必须写完整的 if/else。这是 Rob Pike 等设计者的取舍:让代码更直白、避免嵌套地狱。

3. 未初始化的 map 直接赋值会怎样?

查看答案

panic(运行时崩溃)。map 零值是 nil,必须先用 make(map[K]V) 或字面量初始化才能写。读取 nil map 不会崩(返回零值),但写入会崩。

4. 两个 goroutine 并发读写同一个 map,会发生什么?

查看答案

Go 运行时检测到并发读写 map 会直接 panic 并崩溃(不是数据错乱,是主动 abort)。正确做法:加 sync.RWMutex 或用 sync.Map

5. 为什么 Go 说「不要通过共享内存来通信」?

查看答案

因为加锁共享内存容易出竞争 bug 且难调试。Go 鼓励用 channel 在 goroutine 之间传值,把「共享」变成「传递所有权」,逻辑更清晰、更难写错。这是 CSP(Communicating Sequential Processes)模型。