Skip to content

Go 基础 ​

Go 语言语法与工程基础,面向 Java / Python 后端工程师的零基础到可上手指南。


一、Go 语言定位与设计哲学 ​

1.1 Go 是什么 ​

Go(又称 Golang)是 Google 于 2009 年发布的开源静态类型编译语言,设计目标是解决 C++ / Java 在后端大规模系统中的编译慢、并发难写、依赖失控三大痛点。它有三个关键词:

  • 简单:语言规范极简,关键字仅 25 个,没有继承、没有泛型历史包袱(1.18 才引入泛型)、没有异常机制。
  • 高效:编译为原生机器码,启动毫秒级,单二进制分发,内存占用远低于 JVM 系语言。
  • 并发内建:goroutine 与 channel 是语言级特性,而非像 Java 那样依赖 Thread 类与锁库。

1.2 与 Java / Python 的整体对照 ​

维度GoJavaPython
类型系统静态强类型静态强类型动态弱类型(渐进式)
运行方式编译为原生二进制JVM 字节码解释执行
并发模型goroutine + channelThread + 锁 / Loom协程 + GIL
对象模型结构体 + 方法(无继承)class 继承体系class / 多继承
错误处理显式 error 返回try/catch 异常try/except 异常
依赖管理go modMaven / Gradlepip / poetry / uv
部署形态单文件二进制jar + JRE源码 + 解释器
典型场景云原生、网关、微服务企业级后端脚本、AI、Web

1.3 适用场景与反模式 ​

适合:高并发网络服务(网关、代理、IM)、云原生基础设施(Kubernetes、Docker 即 Go 写)、CLI 工具、中间件。

不适合:强业务 ORM 模板化开发(生态弱于 Java)、科学计算与 AI(生态弱于 Python)、复杂 GUI。


二、环境安装与工具链 ​

2.1 安装与验证 ​

到 go.dev/dl 下载对应平台安装包(Windows 用 msi 会自动配好 PATH)。

bash
go version            # go version go1.22.5 windows/amd64
go env GOPROXY        # 查看代理,国内可设为 https://goproxy.cn,direct
go version            # go version go1.22.5 windows/amd64
go env GOPROXY        # 查看代理,国内可设为 https://goproxy.cn,direct

建议将 GOPROXY 与 GOFLAGS 写入环境便于拉取依赖:

bash
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOFLAGS=-mod=mod
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOFLAGS=-mod=mod

2.2 GOROOT 与 GOPATH ​

变量含义说明
GOROOTGo 安装目录存放标准库与编译器,一般无需手动设置
GOPATH工作区根目录默认用户目录下 go 文件夹,内含 src/pkg/bin;Module 时代仅 bin 常用
GOBINgo install 安装目录常设 %GOPATH%\bin 并加入 PATH

2.3 高频工具命令 ​

命令作用
go run main.go编译并运行(不产出文件)
go build ./...编译整个模块
go vet ./...静态检查常见错误
gofmt -w . / go fmt ./...统一格式
go mod tidy整理依赖(增删 go.mod / go.sum)
go test ./...运行测试
go install x@latest安装命令行工具

go vet 能抓出 fmt.Printf("%d", s) 这类格式串与参数不匹配、printf 误写等问题,建议提交前必跑。


三、模块与包管理 go mod ​

3.1 从 GOPATH 到 Go Modules ​

Go 1.11 前所有代码必须放在 GOPATH/src 下且无版本概念(vendor 手动管理),被戏称为"包管理石器时代"。Go 1.11 引入 Modules,Go 1.16 默认开启。模块(module)是一个带 go.mod 的根目录,go.mod 同时承担了 Java pom.xml 与 package.json 的职责。

3.2 初始化与常用操作 ​

bash
mkdir hello && cd hello
go mod init github.com/me/hello   # 模块路径建议用仓库地址,便于他人引用

go get github.com/gin-gonic/gin@v1.10.0   # 安装指定版本
go get -u ./...                            # 全部依赖升到最新小版本
go mod tidy                                # 清理并补齐依赖
go mod download                            # 预下载依赖到本地缓存
mkdir hello && cd hello
go mod init github.com/me/hello   # 模块路径建议用仓库地址,便于他人引用

go get github.com/gin-gonic/gin@v1.10.0   # 安装指定版本
go get -u ./...                            # 全部依赖升到最新小版本
go mod tidy                                # 清理并补齐依赖
go mod download                            # 预下载依赖到本地缓存

go.sum 记录依赖的校验和,用于供应链安全校验,应提交到仓库(Java 用户可类比 lockfile)。

3.3 go.mod 文件结构 ​

go
module github.com/me/hello

go 1.22

require (
    github.com/gin-gonic/gin v1.10.0
    github.com/spf13/viper v1.19.0 // indirect // 非直接依赖由 tidy 自动标注
)
module github.com/me/hello

go 1.22

require (
    github.com/gin-gonic/gin v1.10.0
    github.com/spf13/viper v1.19.0 // indirect // 非直接依赖由 tidy 自动标注
)

小知识:require 里的 // indirect 表示非直接依赖(Java 的传递依赖),go mod tidy 会自动维护该标记。

3.4 包(package)与可见性 ​

  • 一个目录下所有 .go 文件属于同一个包,包名一般等于目录名。
  • 包内标识符首字母大写 = 导出(public),小写 = 包内私有,没有 Java 的 protected/private 多级关键字。
  • 约定俗成:main 包编译为可执行程序,其余为库包。
go
// 文件 service/user.go,包名 service
package service

// User 首字母大写,外部可见(导出类型建议写注释,golint 会检查)
type User struct {
    Name string // 导出字段
    age  int    // 私有字段,仅包内可访问
}

func NewUser(name string) *User { return &User{Name: name} } // 工厂函数模式

func (u *User) private() {} // 包内方法
// 文件 service/user.go,包名 service
package service

// User 首字母大写,外部可见(导出类型建议写注释,golint 会检查)
type User struct {
    Name string // 导出字段
    age  int    // 私有字段,仅包内可访问
}

func NewUser(name string) *User { return &User{Name: name} } // 工厂函数模式

func (u *User) private() {} // 包内方法

Java 开发者注意:Go 没有 public class 文件约束,一个文件可含多个类型,也没有"一个 public 类型一个文件"的强制。


四、变量、常量与类型 ​

4.1 声明语法 ​

go
package main

import "fmt"

func main() {
    var a int = 1                 // 完整声明
    var b = 2                     // 类型推导
    c := 3                        // 短变量声明(只能在函数内),最常用
    var d, e = "x", 4             // 多变量
    var (                          // 批量声明
        f float64 = 1.5
        g string
    )
    fmt.Println(a, b, c, d, e, f, g)
}
package main

import "fmt"

func main() {
    var a int = 1                 // 完整声明
    var b = 2                     // 类型推导
    c := 3                        // 短变量声明(只能在函数内),最常用
    var d, e = "x", 4             // 多变量
    var (                          // 批量声明
        f float64 = 1.5
        g string
    )
    fmt.Println(a, b, c, d, e, f, g)
}

:= 相当于 var + 推导,但要求左侧至少有一个新变量且作用域内未声明。

4.2 基本类型一览 ​

类别类型说明
布尔bool仅 true / false
整型int int8 int16 int32 int64 uint uintptrint 在 64 位平台为 64 位
浮点float32 float64无 double 关键字,float64 即 Java 的 double
复数complex64 complex128Python 同款原生复数
字符串stringUTF-8,不可变字节序列
字节/字符byte(=uint8)rune(=int32,Unicode 码点)

注意 Go 没有隐式类型转换,int64(a) 必须显式写,这是与 Python 差异最大、与 Java 类似的严格点。

4.3 常量与 iota ​

go
const Pi = 3.14
const (
    A = iota // 0,iota 为常量计数器
    B        // 1
    C        // 2
)

// 经典用法:定义一周或状态枚举
const (
    StatusPending = iota + 1 // 1
    StatusRunning            // 2
    StatusDone               // 3
)
const Pi = 3.14
const (
    A = iota // 0,iota 为常量计数器
    B        // 1
    C        // 2
)

// 经典用法:定义一周或状态枚举
const (
    StatusPending = iota + 1 // 1
    StatusRunning            // 2
    StatusDone               // 3
)

iota 只在 const 块内有效,每次换行递增,可配合运算;Java 的枚举比它表达能力更强,Go 里多用类型别名 + iota 模拟:

go
type Status int

const (
    Pending Status = iota + 1
    Running
    Done
)
type Status int

const (
    Pending Status = iota + 1
    Running
    Done
)

4.4 类型转换与别名 ​

go
var i int = 65
f := float64(i)          // 显式转换
r := rune(f)             // int -> rune
s := string(r)           // rune -> string,得到 "A"
b := []byte("abc")       // string <-> []byte 互转
var i int = 65
f := float64(i)          // 显式转换
r := rune(f)             // int -> rune
s := string(r)           // rune -> string,得到 "A"
b := []byte("abc")       // string <-> []byte 互转

type 关键字有两种用途:定义新类型(type Celsius float64)与定义别名(type MyInt = int,带等号)。新类型与原类型不兼容,需显式转换。


五、流程控制 ​

5.1 if:条件前可执行语句 ​

go
if err := load(); err != nil { // if 的初始化语句作用域仅在 if-else 内
    fmt.Println("load failed:", err)
} else {
    fmt.Println("load ok")
}
if err := load(); err != nil { // if 的初始化语句作用域仅在 if-else 内
    fmt.Println("load failed:", err)
} else {
    fmt.Println("load ok")
}

5.2 for:唯一循环关键字 ​

Go 没有 while / do-while,全部由 for 变体承担:

go
// 经典三段式
for i := 0; i < 10; i++ { fmt.Println(i) }

// while 写法
n := 0
for n < 5 { n++ }

// 无限循环
for { break }

// range 遍历(数组/切片/map/字符串/channel)
nums := []int{1, 2, 3}
for idx, v := range nums { _, _ = idx, v } // 不用的变量用 _ 占位
// 经典三段式
for i := 0; i < 10; i++ { fmt.Println(i) }

// while 写法
n := 0
for n < 5 { n++ }

// 无限循环
for { break }

// range 遍历(数组/切片/map/字符串/channel)
nums := []int{1, 2, 3}
for idx, v := range nums { _, _ = idx, v } // 不用的变量用 _ 占位

Go 1.22 起 for range 可直接遍历整数 for i := range 10;且循环变量每次迭代是新变量(修复了经典闭包捕获 Bug,见 10.4)。

5.3 switch:默认不穿透 ​

go
func grade(score int) string {
    switch {
    case score >= 90:
        return "A"
    case score >= 60:
        return "B"
    default:
        return "C"
    }
}
func grade(score int) string {
    switch {
    case score >= 90:
        return "A"
    case score >= 60:
        return "B"
    default:
        return "C"
    }
}
  • 每个 case 末尾不需要 break,默认匹配即跳出(与 Java 相反,Java 的 fall-through 是常态)。
  • 需要穿透时显式写 fallthrough。
  • Go 1.17+ 的 switch x := f(); x { ... } 支持初始化语句。
  • case 可并列:case 1, 2, 3:。

5.4 break / continue / goto 与标签 ​

go
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i*j == 2 {
            break outer // 跳出外层循环,Java 中需 labeled break 等价
        }
    }
}
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i*j == 2 {
            break outer // 跳出外层循环,Java 中需 labeled break 等价
        }
    }
}

六、函数 ​

6.1 函数声明与多返回值 ​

go
// 类型放在变量名后,返回值也放后面
func add(a, b int) int { // a, b 同类型可合并声明
    return a + b
}

// 多返回值:Go 无异常,约定最后一个返回值承载 error
func div(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide by zero")
    }
    return a / b, nil
}
// 类型放在变量名后,返回值也放后面
func add(a, b int) int { // a, b 同类型可合并声明
    return a + b
}

// 多返回值:Go 无异常,约定最后一个返回值承载 error
func div(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide by zero")
    }
    return a / b, nil
}

"类型后置"常让 Java 开发者不习惯,但换来的是函数签名可读性与多返回值这种 Python 风格体验。

6.2 命名返回值与可变参数 ​

go
func minMax(nums ...int) (min, max int) { // 返回值已命名,自动初始化为零值
    min, max = nums[0], nums[0]
    for _, v := range nums[1:] {
        if v < min { min = v }
        if v > max { max = v }
    }
    return // 裸返回:直接返回命名变量当前值
}

// 调用:minMax(3, 1, 2);切片展开:minMax(s...)
func minMax(nums ...int) (min, max int) { // 返回值已命名,自动初始化为零值
    min, max = nums[0], nums[0]
    for _, v := range nums[1:] {
        if v < min { min = v }
        if v > max { max = v }
    }
    return // 裸返回:直接返回命名变量当前值
}

// 调用:minMax(3, 1, 2);切片展开:minMax(s...)

裸返回(naked return)在短函数中可读,长函数中建议显式 return 避免歧义。

6.3 函数是一等公民与闭包 ​

go
type Handler func(ctx string) error // 函数类型

func apply(h Handler) error { return h("go") }

func counter() func() int { // 闭包:捕获外层变量
    n := 0
    return func() int { n++; return n }
}

func main() {
    c := counter()
    fmt.Println(c(), c(), c()) // 1 2 3
}
type Handler func(ctx string) error // 函数类型

func apply(h Handler) error { return h("go") }

func counter() func() int { // 闭包:捕获外层变量
    n := 0
    return func() int { n++; return n }
}

func main() {
    c := counter()
    fmt.Println(c(), c(), c()) // 1 2 3
}

函数类型可作参数、返回值、字段,等价于 Java 的函数式接口与 Python 的 lambda 应用位置。

6.4 defer:延迟执行 ​

go
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil { return err }
    defer f.Close() // 函数返回前执行,用于释放资源

    data := make([]byte, 100)
    f.Read(data)
    return nil
}
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil { return err }
    defer f.Close() // 函数返回前执行,用于释放资源

    data := make([]byte, 100)
    f.Read(data)
    return nil
}
  • defer 在**函数返回时按 LIFO(后进先出)**执行。
  • defer 参数在 defer 语句处求值,不是执行时。
  • 经典坑:defer f.Close() 的错误被吞掉;严谨写法是 defer 包裹闭包处理 error。
  • defer 与 panic:即使函数 panic,defer 仍会执行,可用于 recover(见进阶篇)。

6.5 init 函数与包初始化顺序 ​

每个包可有多个 init(),包级变量初始化完成后自动调用,用于注册、初始化连接池等:

go
var db *sql.DB

func init() {
    // 读取配置、初始化全局资源
}
var db *sql.DB

func init() {
    // 读取配置、初始化全局资源
}

初始化顺序为:依赖的包先 init → 当前包变量 → 当前包 init,且依赖按深度优先。应避免在 init 中做重活(如连数据库),否则测试难 mock。


七、复合类型:数组、切片与 Map ​

7.1 数组:值类型 ​

go
var a [3]int          // [0 0 0]
b := [3]int{1, 2, 3}
c := [...]int{1, 2}   // 编译器数长度
var a [3]int          // [0 0 0]
b := [3]int{1, 2, 3}
c := [...]int{1, 2}   // 编译器数长度

数组是值语义:赋值与传参会整体复制(Java 数组是引用)。因此业务代码几乎不用数组,只用切片。

7.2 切片:动态数组的本质 ​

go
s1 := []int{1, 2, 3}        // 字面量创建
s2 := make([]int, 0, 5)     // make(len, cap),预分配容量避免扩容
s3 := a[1:3]                // 从数组切片(左闭右开)
s1 := []int{1, 2, 3}        // 字面量创建
s2 := make([]int, 0, 5)     // make(len, cap),预分配容量避免扩容
s3 := a[1:3]                // 从数组切片(左闭右开)

切片底层是结构体 {ptr 指针, len 长度, cap 容量},多个切片可能共享同一底层数组,这是很多诡异 Bug 的根源:

go
s := []int{1, 2, 3, 4, 5}
t := s[1:3]       // t 引用 s 的底层数组
t[0] = 99         // 修改会影响 s[1]
fmt.Println(s)    // [1 99 3 4 5]
s := []int{1, 2, 3, 4, 5}
t := s[1:3]       // t 引用 s 的底层数组
t[0] = 99         // 修改会影响 s[1]
fmt.Println(s)    // [1 99 3 4 5]

7.3 append 扩容原理 ​

go
s := make([]int, 0, 3)
s = append(s, 1, 2, 3)
s := make([]int, 0, 3)
s = append(s, 1, 2, 3)
  • 当 len == cap 时 append 会分配新数组:容量小于 256 时翻倍,之后按约 1.25 倍增长(Go 1.18 起的增长公式),并把旧元素拷过去。
  • append 可能返回新底层数组,所以必须用返回值接收:s = append(s, x)。
  • 跨包共享时可用 copy(dst, src) 显式复制,或 slices.Clone(Go 1.21+)避免误改。

7.4 Map:哈希表 ​

go
m := make(map[string]int, 16)  // 预分配减少扩容
m["a"] = 1
v, ok := m["b"]        // ok=false 表示键不存在,区分"零值"与"不存在"
delete(m, "a")

for k, v := range m { _, _ = k, v } // 遍历顺序随机,勿依赖
m := make(map[string]int, 16)  // 预分配减少扩容
m["a"] = 1
v, ok := m["b"]        // ok=false 表示键不存在,区分"零值"与"不存在"
delete(m, "a")

for k, v := range m { _, _ = k, v } // 遍历顺序随机,勿依赖
  • map 是引用语义,可直接传给函数修改。
  • map 并发读写会直接 panic(fatal error: concurrent map writes),需要加锁或换 sync.Map(见并发篇)。
  • nil map 可读不可写:var m map[string]int; m["a"] = 1 会 panic,须先 make。
  • Go 1.21 新增 clear(m) 一键清空。

八、字符串处理 ​

8.1 string 的本质 ​

string 是只读的字节切片,底层不存字符而是 UTF-8 字节。len(s) 返回字节数而非字符数:

go
s := "你好go"
fmt.Println(len(s))          // 8:'你'3字节 + '好'3字节 + 'g','o'各1字节
fmt.Println(len([]rune(s)))  // 4:转 rune 切片后数出 4 个字符
s := "你好go"
fmt.Println(len(s))          // 8:'你'3字节 + '好'3字节 + 'g','o'各1字节
fmt.Println(len([]rune(s)))  // 4:转 rune 切片后数出 4 个字符

遍历中文字符串必须用 for i, r := range s(自动解码 UTF-8),直接用下标取值会得到残缺字节。

8.2 strings 包速查 ​

函数作用对应 Python
strings.Contains(s, sub)是否包含in
strings.HasPrefix / HasSuffix前后缀startswith / endswith
strings.Split / Join切分 / 拼接split / join
strings.TrimSpace / Trim去空白 / 去指定字符strip
strings.ReplaceAll全量替换replace
strings.ToLower / ToUpper大小写lower / upper
strings.Fields按空白分词split()
strings.Builder高效拼接无

8.3 strconv 类型互转 ​

go
i, err := strconv.Atoi("42")            // 字符串转 int
s := strconv.Itoa(42)                    // int 转字符串
f, _ := strconv.ParseFloat("3.14", 64)  // 转 float
b, _ := strconv.ParseBool("true")        // 转 bool
i, err := strconv.Atoi("42")            // 字符串转 int
s := strconv.Itoa(42)                    // int 转字符串
f, _ := strconv.ParseFloat("3.14", 64)  // 转 float
b, _ := strconv.ParseBool("true")        // 转 bool

8.4 字符串拼接性能对比 ​

方式示例适用
+s := a + b少量拼接
fmt.Sprintffmt.Sprintf("%s-%d", a, 1)需格式化
strings.Builderb.WriteString(x)循环大量拼接(推荐)
[]byte 预分配make([]byte, 0, n)极致性能

循环内勿用 +:string 不可变,每次拼接都产生新分配,O(n^2) 退化。循环拼接请用 Builder,其内部是自动扩容的 []byte。


九、结构体基础 ​

9.1 定义与初始化 ​

go
type Person struct {
    Name string
    Age  int
}

// 三种初始化方式
p1 := Person{Name: "zhang", Age: 18} // 键值对(推荐)
p2 := Person{"li", 20}               // 位置初始化,字段顺序变化易错
p3 := new(Person)                    // 返回 *Person,字段为零值
var p4 Person                        // 零值对象
type Person struct {
    Name string
    Age  int
}

// 三种初始化方式
p1 := Person{Name: "zhang", Age: 18} // 键值对(推荐)
p2 := Person{"li", 20}               // 位置初始化,字段顺序变化易错
p3 := new(Person)                    // 返回 *Person,字段为零值
var p4 Person                        // 零值对象

new(T) 返回指向零值的指针;Go 无构造函数,惯用的"构造函数"是返回 *T 的普通函数 NewPerson(...)。

9.2 组合而非继承 ​

go
type Animal struct{ Name string }

func (a *Animal) Speak() string { return "..." }

type Dog struct {
    Animal          // 内嵌(匿名嵌入),字段与方法被"提升"
    Breed string
}

func main() {
    d := Dog{Animal: Animal{Name: "wangcai"}, Breed: "golden"}
    fmt.Println(d.Name)    // 提升字段,d.Name == d.Animal.Name
    fmt.Println(d.Speak()) // 提升方法
}
type Animal struct{ Name string }

func (a *Animal) Speak() string { return "..." }

type Dog struct {
    Animal          // 内嵌(匿名嵌入),字段与方法被"提升"
    Breed string
}

func main() {
    d := Dog{Animal: Animal{Name: "wangcai"}, Breed: "golden"}
    fmt.Println(d.Name)    // 提升字段,d.Name == d.Animal.Name
    fmt.Println(d.Speak()) // 提升方法
}

Go 用结构体内嵌实现复用,没有 extends / implements 关键字,也不存在方法重载(同名方法全局唯一),强制开发者走"组合优于继承"路线。

9.3 结构体 Tag 与 JSON ​

go
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email,omitempty"` // 空值不输出
}
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email,omitempty"` // 空值不输出
}

Tag 是字段后的反引号字符串,由反射机制解析,最常见的用途就是 json 序列化控制(详细对照见进阶篇 JSON 小节)。

9.4 与 Java / Python 类对照 ​

Go structJava classPython class说明
字段成员变量类属性首字母控制可见性
方法(receiver)实例方法方法方法定义在结构体外
内嵌 struct继承 / 组合多继承只有组合语义
工厂函数构造器 / 重载__init__Go 用 NewXxx 替代
基础泛型(1.18+)泛型继承体系Protocol表达能力弱于 Java

十、常见坑与编码习惯 ​

10.1 变量遮蔽 ​

:= 在内层作用域重新声明同名变量会遮蔽外层:

go
x := 1
if true {
    x := 2 // 这里声明了新的 x,外层不受影响
    fmt.Println(x) // 2
}
fmt.Println(x) // 1
x := 1
if true {
    x := 2 // 这里声明了新的 x,外层不受影响
    fmt.Println(x) // 2
}
fmt.Println(x) // 1

可用 go vet 的 shadow 检查器辅助发现。

10.2 短变量声明的"至少一个新变量" ​

go
a, b := 1, 2
a, c := 3, 4 // 合法:c 是新的,a 复用外层
_ = b
// 同一作用域内 b, c := 5, 6 若 b、c 均已声明则编译报错:no new variables
a, b := 1, 2
a, c := 3, 4 // 合法:c 是新的,a 复用外层
_ = b
// 同一作用域内 b, c := 5, 6 若 b、c 均已声明则编译报错:no new variables

10.3 nil slice 与 empty slice ​

go
var s []int        // nil slice,s == nil 为 true
e := []int{}       // empty slice,非 nil 但长度为 0
n := make([]int, 0) // 同 empty slice
var s []int        // nil slice,s == nil 为 true
e := []int{}       // empty slice,非 nil 但长度为 0
n := make([]int, 0) // 同 empty slice
  • nil slice 可 append(会自动分配),可 range,但 s[0] 越界。
  • JSON 序列化差异:nil slice 输出 null,empty slice 输出 [],对接前端时注意(实践篇会再提)。

10.4 循环变量捕获(Go 1.22 前) ​

Go 1.22 前 for 循环变量是复用的同一个变量,闭包延迟执行会全部取到最后值:

go
// Go 1.22 前的输出:5 5 5 5 5
for i := 0; i < 5; i++ {
    go func() { fmt.Println(i) }()
}
// Go 1.22 前的输出:5 5 5 5 5
for i := 0; i < 5; i++ {
    go func() { fmt.Println(i) }()
}

旧版本需用参数拷贝:go func(n int) { fmt.Println(n) }(i)。Go 1.22 起每次迭代是新变量,行为符合直觉,升级后无需 workaround。

10.5 格式化与命名习惯 ​

  • 官方强制 gofmt,无需争论风格;命名推荐驼峰(导出首字母大写)。
  • 错误处理走"快乐路径":先判断 error 提前 return,主逻辑下沉,减少嵌套。
  • 善用 _ 忽略不需要的返回值,但 error 永远不要用 _ 丢弃,除非明确该错误可忽略。

通过本篇已覆盖 Go 的基础语法骨架,下一步请进入 Go 进阶特性 掌握指针、接口、错误处理与泛型,再通过 Go 并发编程 理解 goroutine 模型,最后在 Go Gin 框架 与 Go Web 项目实践 中落地真实服务。