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 的整体对照
| 维度 | Go | Java | Python |
|---|---|---|---|
| 类型系统 | 静态强类型 | 静态强类型 | 动态弱类型(渐进式) |
| 运行方式 | 编译为原生二进制 | JVM 字节码 | 解释执行 |
| 并发模型 | goroutine + channel | Thread + 锁 / Loom | 协程 + GIL |
| 对象模型 | 结构体 + 方法(无继承) | class 继承体系 | class / 多继承 |
| 错误处理 | 显式 error 返回 | try/catch 异常 | try/except 异常 |
| 依赖管理 | go mod | Maven / Gradle | pip / 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)。
go version # go version go1.22.5 windows/amd64
go env GOPROXY # 查看代理,国内可设为 https://goproxy.cn,directgo version # go version go1.22.5 windows/amd64
go env GOPROXY # 查看代理,国内可设为 https://goproxy.cn,direct建议将 GOPROXY 与 GOFLAGS 写入环境便于拉取依赖:
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOFLAGS=-mod=modgo env -w GOPROXY=https://goproxy.cn,direct
go env -w GOFLAGS=-mod=mod2.2 GOROOT 与 GOPATH
| 变量 | 含义 | 说明 |
|---|---|---|
| GOROOT | Go 安装目录 | 存放标准库与编译器,一般无需手动设置 |
| GOPATH | 工作区根目录 | 默认用户目录下 go 文件夹,内含 src/pkg/bin;Module 时代仅 bin 常用 |
| GOBIN | go 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 初始化与常用操作
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 文件结构
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包编译为可执行程序,其余为库包。
// 文件 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 声明语法
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 uintptr | int 在 64 位平台为 64 位 |
| 浮点 | float32 float64 | 无 double 关键字,float64 即 Java 的 double |
| 复数 | complex64 complex128 | Python 同款原生复数 |
| 字符串 | string | UTF-8,不可变字节序列 |
| 字节/字符 | byte(=uint8)rune(=int32,Unicode 码点) |
注意 Go 没有隐式类型转换,int64(a) 必须显式写,这是与 Python 差异最大、与 Java 类似的严格点。
4.3 常量与 iota
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 模拟:
type Status int
const (
Pending Status = iota + 1
Running
Done
)type Status int
const (
Pending Status = iota + 1
Running
Done
)4.4 类型转换与别名
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:条件前可执行语句
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 变体承担:
// 经典三段式
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:默认不穿透
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 与标签
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 函数声明与多返回值
// 类型放在变量名后,返回值也放后面
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 命名返回值与可变参数
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 函数是一等公民与闭包
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:延迟执行
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(),包级变量初始化完成后自动调用,用于注册、初始化连接池等:
var db *sql.DB
func init() {
// 读取配置、初始化全局资源
}var db *sql.DB
func init() {
// 读取配置、初始化全局资源
}初始化顺序为:依赖的包先 init → 当前包变量 → 当前包 init,且依赖按深度优先。应避免在 init 中做重活(如连数据库),否则测试难 mock。
七、复合类型:数组、切片与 Map
7.1 数组:值类型
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 切片:动态数组的本质
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 的根源:
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 扩容原理
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:哈希表
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) 返回字节数而非字符数:
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 类型互转
i, err := strconv.Atoi("42") // 字符串转 int
s := strconv.Itoa(42) // int 转字符串
f, _ := strconv.ParseFloat("3.14", 64) // 转 float
b, _ := strconv.ParseBool("true") // 转 booli, err := strconv.Atoi("42") // 字符串转 int
s := strconv.Itoa(42) // int 转字符串
f, _ := strconv.ParseFloat("3.14", 64) // 转 float
b, _ := strconv.ParseBool("true") // 转 bool8.4 字符串拼接性能对比
| 方式 | 示例 | 适用 |
|---|---|---|
+ | s := a + b | 少量拼接 |
fmt.Sprintf | fmt.Sprintf("%s-%d", a, 1) | 需格式化 |
strings.Builder | b.WriteString(x) | 循环大量拼接(推荐) |
[]byte 预分配 | make([]byte, 0, n) | 极致性能 |
循环内勿用 +:string 不可变,每次拼接都产生新分配,O(n^2) 退化。循环拼接请用 Builder,其内部是自动扩容的 []byte。
九、结构体基础
9.1 定义与初始化
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 组合而非继承
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
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 struct | Java class | Python class | 说明 |
|---|---|---|---|
| 字段 | 成员变量 | 类属性 | 首字母控制可见性 |
| 方法(receiver) | 实例方法 | 方法 | 方法定义在结构体外 |
| 内嵌 struct | 继承 / 组合 | 多继承 | 只有组合语义 |
| 工厂函数 | 构造器 / 重载 | __init__ | Go 用 NewXxx 替代 |
| 基础泛型(1.18+) | 泛型继承体系 | Protocol | 表达能力弱于 Java |
十、常见坑与编码习惯
10.1 变量遮蔽
:= 在内层作用域重新声明同名变量会遮蔽外层:
x := 1
if true {
x := 2 // 这里声明了新的 x,外层不受影响
fmt.Println(x) // 2
}
fmt.Println(x) // 1x := 1
if true {
x := 2 // 这里声明了新的 x,外层不受影响
fmt.Println(x) // 2
}
fmt.Println(x) // 1可用 go vet 的 shadow 检查器辅助发现。
10.2 短变量声明的"至少一个新变量"
a, b := 1, 2
a, c := 3, 4 // 合法:c 是新的,a 复用外层
_ = b
// 同一作用域内 b, c := 5, 6 若 b、c 均已声明则编译报错:no new variablesa, b := 1, 2
a, c := 3, 4 // 合法:c 是新的,a 复用外层
_ = b
// 同一作用域内 b, c := 5, 6 若 b、c 均已声明则编译报错:no new variables10.3 nil slice 与 empty slice
var s []int // nil slice,s == nil 为 true
e := []int{} // empty slice,非 nil 但长度为 0
n := make([]int, 0) // 同 empty slicevar 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 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 项目实践 中落地真实服务。
Cao Note