基础语法
# 第 2 章 基础语法与 Hello World
现代 Go(1.22)的第一行代码——以
go mod起手,不学 GOPATH 老模式。 关键词:package、import、go run、go build、go mod init、命名规范、25 个关键字
# 目录介绍
- 2.1 本章学习目标
- 2.2 安装 Go 1.22 与环境验证
- 2.3 第一个 Go 模块:现代 Hello World
- 2.4 包与导入
- 2.5 标识符、关键字、操作符
- 2.6 变量与常量
- 2.7 注释与文档(godoc)
- 2.8 综合示例:能跑的猜数字游戏
- 2.9 本章底层原理(简介)
- 2.10 Go 新手陷阱 Top 5
- 2.11 思考题
- 2.12 推荐阅读
# 2.1 本章学习目标
- ✅ 在自己电脑装好 Go 1.22 并跑通
go run与go build - ✅ 理解
go mod init/go mod tidy的基本用法 - ✅ 能背出 Go 的 25 个关键字
- ✅ 能解释为什么"导出"用首字母大小写、为什么没有
public/private - ✅ 能默写
iota枚举模式 - ✅ 知道
var/:=/const在何处该用何处不该用
# 2.2 安装 Go 1.22 与环境验证
# 2.2.1 三大平台安装速览
| 平台 | 推荐方式 | 验证 |
|---|---|---|
| macOS | brew install go(或官网 pkg) | go version |
| Linux | 官网 tar.gz 解到 /usr/local/go,PATH 加 /usr/local/go/bin | 同上 |
| Windows | 官网 msi | 同上 |
无论哪种方式,最终都应该看到:
$ go version
go version go1.22.0 darwin/arm64
2
⚠️ 基线提醒:本书所有代码以 Go 1.22 为基线。如果你装的是 1.21 或更早,部分新特性(如循环变量每轮新建、
for i := range N)会跑不通。
# 2.2.2 GOROOT / GOPATH / GOPROXY 三件套
| 环境变量 | 含义 | 是否需要手动设置 |
|---|---|---|
GOROOT | Go 的安装目录(含 SDK 源码、标准库) | ❌ 不要设。包管理器会自动处理 |
GOPATH | 旧时代"工作区",modules 时代仅存放 go install 安装的二进制(默认 ~/go) | ❌ 一般不要动 |
GOPROXY | 模块下载代理 | ✅ 国内强烈建议设置 |
GOSUMDB | 模块哈希验证服务 | 视环境而定 |
国内开发者推荐配置(一次设置,永久生效):
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOSUMDB=sum.golang.google.cn
2
go env -w 会把这些设置写入 ~/.config/go/env(或 Windows 下的对应位置),不污染 shell 环境。
💡 GOPATH 已经"死了"吗? 不完全。
$GOPATH/bin仍是go install默认产物目录,建议把它加入PATH:export PATH=$PATH:$(go env GOPATH)/bin。这样go install xxx@latest装的工具能直接调用。
# 2.3 第一个 Go 模块:现代 Hello World
我们不走 "在 GOPATH 下建目录" 这条已死的路,直接 go mod init。
# 2.3.1 go mod init 起手
任意目录(不必在 GOPATH 下):
mkdir hello && cd hello
go mod init example.com/hello
2
执行后会生成一个 go.mod 文件:
module example.com/hello
go 1.22
2
3
三件事说明:
module example.com/hello:本模块的"名字",也就是别人 import 你时写的路径。如果你打算开源到 GitHub,用github.com/yourname/hello起名最合适。go 1.22:本模块要求的最低 Go 版本(也作为语言行为的开关,比如循环变量语义)。- 还没有
require:因为我们还没引用任何第三方库。
# 2.3.2 写 main.go 并运行
在 hello 目录新建 main.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
2
3
4
5
6
7
逐行解释:
package main— 本文件属于哪个包。包名为main是特殊的:表示这是一个可执行程序,不是库。import "fmt"— 引入标准库fmt(format)。如果引入了不用,编译器直接报错——这是 Go 的"零容忍"风格。func main()— 程序入口;签名固定为func main(),无参数无返回值。fmt.Println(...)— 调用fmt包中的导出函数Println(首字母大写 = 导出)。
运行:
$ go run .
Hello, 世界
2
注意命令是
go run .(当前目录)而不是go run main.go。前者会把所有.go文件一起编译,后者在多文件项目里会找不到符号。养成go run .的肌肉记忆很重要。
# 2.3.3 go run vs go build
| 命令 | 行为 | 产出 |
|---|---|---|
go run . | 编译并立即运行,临时二进制随即删除 | 无 |
go build | 编译,产物落到当前目录 | ./hello(与目录同名) |
go build -o myapp | 同上但指定产物名 | ./myapp |
go install | 编译并把产物放到 $GOPATH/bin | ~/go/bin/hello |
$ go build
$ ls
go.mod hello* main.go
$ ./hello
Hello, 世界
2
3
4
5
6
- 注意产出
hello是静态链接的二进制——没有 JVM、没有.dll、没有node_modules,scp 到任意一台同架构同操作系统的机器都能跑。这是 Go 在容器时代的"杀手锏"。 - 如果要跨平台编译,加环境变量即可:
GOOS=linux GOARCH=amd64 go build -o hello-linux
GOOS=darwin GOARCH=arm64 go build -o hello-mac
GOOS=windows GOARCH=amd64 go build -o hello.exe
2
3
# 2.4 包与导入
# 2.4.1 package main vs 库包
package main 其他任意 package(如 utils、auth)
──────────── ──────────────────────────────
位置 顶层 任意子目录
是否能跑 ✅ 直接 go run ❌ 只能被 import
是否要 main() ✅ 必须 ❌ 不要写
能否被 import ❌ 不能 ✅ 可以
2
3
4
5
6
一个常见的多包项目布局:
myapp/
├── go.mod
├── main.go ← package main
├── auth/
│ └── auth.go ← package auth
└── store/
└── store.go ← package store
2
3
4
5
6
7
main.go 里 import "example.com/myapp/auth",对应硬盘上的 auth/ 子目录。目录名与包名最好一致——这是约定,不是强制。
# 2.4.2 import 的四种写法
// 1. 单条导入
import "fmt"
// 2. 分组导入(推荐)
import (
"fmt"
"strings"
"github.com/google/uuid" // 第三方与标准库之间留空行
)
// 3. 别名导入:解决重名 / 起短名
import (
crand "crypto/rand"
mrand "math/rand"
)
// 4. 副作用导入:只为执行 init(),不直接用包内符号
import _ "github.com/lib/pq"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
何时用别名:两个不同路径但同名包共存(最常见的就是 crypto/rand 与 math/rand)。
何时用 _:需要触发包的 init() 副作用——比如数据库驱动注册(database/sql 通过 init() 注册 driver)。
# 2.4.3 大写导出 / 小写未导出
Go 没有 public / private / protected 关键字,靠首字母大小写:
| 标识符 | 可见性 |
|---|---|
Foo / BarBaz | 导出(包外可见) |
foo / barBaz | 未导出(仅本包内可见) |
package mathx
func Square(x int) int { // ✅ 包外可调用
return x * x
}
func double(x int) int { // ❌ 包外不可见
return x * 2
}
2
3
4
5
6
7
8
9
设计哲学:约定即语法。这让你 git grep '^func [A-Z]' 就能列出所有公共 API,比在 IDE 里点开类查 public 修饰符快得多。
# 2.5 标识符、关键字、操作符
# 2.5.1 25 个关键字一览
Go 总共只有 25 个关键字——少到可以在脑子里全记下来。背下这张表,你就读得懂任何 Go 代码:
| 分类 | 关键字 |
|---|---|
| 声明(5) | var、const、type、func、package |
| 控制流(11) | if、else、for、switch、case、default、break、continue、fallthrough、goto、return |
| 并发(3) | go、chan、select |
| 复合类型相关(3) | struct、interface、map |
| 其他(3) | import、defer、range |
仔细数一下——确实只有 25 个。对比 C++ 的 ~100 个、Java 的 ~50 个,Go 的"少即是多"哲学一目了然。
# 2.5.2 命名规范(驼峰、缩写大写)
Go 官方风格指南 + Uber Style Guide 的共识:
| 元素 | 风格 | 例子 |
|---|---|---|
| 局部变量 | 短小驼峰 | i、buf、userID |
| 函数 / 方法 | 驼峰,导出大写 | Marshal / parseHeader |
| 类型 | 驼峰,导出大写 | Server、httpClient |
| 常量 | 驼峰(不用 UPPER_SNAKE) | MaxRetries、defaultPort |
| 包名 | 全小写、单词、不加下划线 | auth、httputil |
| 文件名 | 小写下划线 | user_repo.go、auth_test.go |
| 接口 | 单方法接口加 -er 后缀 | Reader、Writer、Stringer |
"缩写全大写"规则——缩写词整体大写或整体小写,不要驼峰:
// ✅ 正确
var userID int // ID 全大写
var httpClient *Client // http 全小写
func ParseURL(s string) {} // URL 全大写
// ❌ 错误
var userId int // 缩写 Id 不规范
var HttpClient *Client // 缩写 Http 不规范
func ParseUrl(s string) {} // 同上
2
3
4
5
6
7
8
9
golangci-lint 默认开启的 revive / stylecheck 会自动检测这些。
# 2.5.3 预声明标识符(int / true / nil ...)
Go 还有一批预声明标识符——它们不是关键字,但被 universe scope 占用,重定义不会编译错但是反模式:
// 类型
bool byte rune string error
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex64 complex128
any comparable // Go 1.18+
// 常量
true false iota nil
// 函数
make new len cap append copy delete close
panic recover print println
min max clear // Go 1.21+ 新增内置
// ❌ 反例:不要重新定义这些名字
var nil = 1 // 编译过,但任何看到这行的 reviewer 都会 reject
func len() {} // 同上
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.6 变量与常量
# 2.6.1 var / := / const 三件套
Go 提供三种声明方式,用对场景:
// 1. var:显式声明,可只声明不初始化(自动取零值)
var x int // x = 0
var name string // name = ""
var p *Person // p = nil
var s = "hello" // 类型由右侧推导
// 2. := 短变量声明:仅在函数内可用,必须初始化
func main() {
x := 42 // 等价于 var x int = 42
name := "Bob" // 等价于 var name string = "Bob"
}
// 3. const:编译期常量,必须能在编译期求值
const Pi = 3.14159
const MaxRetries int = 5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
何时用哪个?三条规则:
- 包级别(函数外)只能用
var或const,不能用:=。 - 函数内首选
:=——更短、有右值时类型自动推导。 - 零值就够用时(如
var sum int当累加器)才用var,不用写sum := 0。
// ✅ 推荐
var sum int // 利用零值,比 sum := 0 简洁
for _, v := range nums {
sum += v
}
// ❌ 反模式
var x int = 0 // 冗余,写 var x int 即可
sum := 0 // 函数内能用 := 但显式写 0 没必要,var 更简洁
2
3
4
5
6
7
8
9
# 2.6.2 多变量声明与并行赋值
// 多个变量一起声明(不同类型)
var (
name string
age int
married bool
)
// 同类型批量
var x, y, z int
// 多重赋值(最常用:交换、多返回值接收)
a, b := 1, 2
a, b = b, a // 一行交换,没有 tmp 变量
v, ok := m["key"] // map 双返回值
data, err := os.ReadFile("a.txt") // error 模式
2
3
4
5
6
7
8
9
10
11
12
13
14
15
多重赋值是右侧整体先求值后再赋给左侧,所以 a, b = b, a 不需要中间变量。这一点是 Go 的语义保证,与 Python 一致。
# 2.6.3 iota 枚举模式
Go 没有 enum 关键字,用 const + iota 模拟。iota 是const 块内自增的整型计数器,每个 const 块从 0 开始:
const (
Red = iota // 0
Green // 1(继承上一行表达式 = iota)
Blue // 2
)
// 经典用法:位掩码
const (
FlagRead = 1 << iota // 1 << 0 = 1
FlagWrite // 1 << 1 = 2
FlagExecute // 1 << 2 = 4
)
// 跳过某个值
const (
_ = iota // 0 不要
KB = 1 << (10 * iota) // 1 << 10
MB // 1 << 20
GB // 1 << 30
TB // 1 << 40
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
💡 给枚举类型起个独立类型名(type-safe enum):
type Color int
const (
Red Color = iota
Green
Blue
)
func (c Color) String() string {
return [...]string{"Red", "Green", "Blue"}[c]
}
func main() {
var c Color = Green
fmt.Println(c) // Green(自动调用 String 方法)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这种"自定义类型 + 字符串方法"的组合接近 Java/Rust 的 enum 体验。
# 2.7 注释与文档(godoc)
Go 把"注释"和"文档"统一起来——所有以包/函数/类型/常量为目标的注释自动是它的文档:
// Package mathx provides extra math utilities not in the standard library.
package mathx
// Square returns x * x.
//
// It panics if x*x overflows int64.
func Square(x int64) int64 {
return x * x
}
2
3
4
5
6
7
8
9
约定:
- 包注释紧贴
package行,第一句应当以包名开头:Package mathx provides ... - 导出符号必须有注释,第一句以符号名开头:
Square returns ... - 注释支持 Markdown-lite(Go 1.19+ 引入了
[link]风格)
查看:
go doc . # 当前包文档
go doc fmt.Println # 标准库符号
go doc -all strings # 整个包详细
2
3
或本地起 web:
go install golang.org/x/tools/cmd/godoc@latest
godoc -http=:6060
# 浏览器访问 http://localhost:6060
2
3
线上版:https://pkg.go.dev/ (opens new window) 是 Go 官方文档站。
# 2.8 综合示例:能跑的猜数字游戏
把本章知识全部用上,写一个 100 行的猜数字游戏:
// guessing/main.go
package main
import (
"bufio"
"fmt"
"math/rand/v2" // Go 1.22+ 推荐 v2 版
"os"
"strconv"
"strings"
)
const (
MinNum = 1
MaxNum = 100
MaxTries = 7
)
func main() {
target := rand.IntN(MaxNum-MinNum+1) + MinNum
fmt.Printf("我已经想好了一个 [%d, %d] 的数字,你有 %d 次机会猜中。\n",
MinNum, MaxNum, MaxTries)
reader := bufio.NewReader(os.Stdin)
for tries := 1; tries <= MaxTries; tries++ {
fmt.Printf("第 %d 次猜测,请输入: ", tries)
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取输入出错:", err)
return
}
line = strings.TrimSpace(line)
guess, err := strconv.Atoi(line)
if err != nil {
fmt.Println("⚠️ 请输入一个整数!")
tries-- // 输入错误不消耗次数
continue
}
switch {
case guess < target:
fmt.Println("🔼 偏小")
case guess > target:
fmt.Println("🔽 偏大")
default:
fmt.Printf("🎉 恭喜!你用 %d 次猜中了 %d。\n", tries, target)
return
}
}
fmt.Printf("😵 机会用尽,正确答案是 %d。\n", target)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
跑起来:
$ go mod init example.com/guessing
$ go run .
我已经想好了一个 [1, 100] 的数字,你有 7 次机会猜中。
第 1 次猜测,请输入: 50
🔽 偏大
第 2 次猜测,请输入: 25
🔼 偏小
...
2
3
4
5
6
7
8
这个例子用上了:
| 章节知识点 | 在哪一行体现 |
|---|---|
package main + func main() | 文件开头 |
分组 import | import (...) |
const + iota 风格 | const (...) 块 |
短变量声明 := | target := ... |
| 多返回值 + error | line, err := reader.ReadString(...) |
for 三段式 | for tries := 1; ... |
switch-true 模式 | switch { case ... } |
标准库 bufio / strconv / strings | 多处 |
# 2.9 本章底层原理(简介)
关于 go mod init / go build 背后到底做了什么,留到卷三第 16 章「编译链接与演进」详讲。这里只需知道:
go run .
│
├──► parse 解析 .go 源码 → AST
├──► typecheck 类型检查
├──► IR / SSA 中间代码
├──► 机器码生成
├──► link 链接为二进制(自动包含 runtime + GC)
└──► 运行临时二进制 → 退出 → 删除二进制
2
3
4
5
6
7
8
整个流程在 几百毫秒内完成——这就是 Go "编译快"的直接体验。
# 2.10 Go 新手陷阱 Top 5
# ❌ 陷阱 1:import 写了但没用
import (
"fmt"
"strings" // 引入但代码里没用到
)
func main() {
fmt.Println("hi")
}
2
3
4
5
6
7
8
编译错误:imported and not used: "strings"。
解决:删掉,或临时用 _ "strings" 副作用导入(仅当真的需要副作用时)。
这是 Go 故意为之的"零容忍"——避免依赖图臃肿。
# ❌ 陷阱 2:var x int = 0 冗余写法
// ❌ 冗余
var x int = 0
var s string = ""
var p *T = nil
// ✅ 简洁
var x int // 零值就是 0
var s string // 零值就是 ""
var p *T // 零值就是 nil
2
3
4
5
6
7
8
9
golangci-lint 的 gosimple 会标红。
# ❌ 陷阱 3::= 在已有变量上误用
x := 1
{
x := 2 // ❌ 这是新建一个 x(block-scoped),外层 x 还是 1
fmt.Println(x) // 2
}
fmt.Println(x) // 1(被遮蔽 shadow 了)
2
3
4
5
6
多变量短声明的"半新半旧"陷阱:
err := doSomething()
if cond {
val, err := doOther() // ⚠️ 这里 err 是新变量!外层 err 不会被赋值
_ = val
_ = err
}
// 此处 err 仍是上面 doSomething 的结果
2
3
4
5
6
7
修复:把内层 := 改成 =,或把变量提前声明。
# ❌ 陷阱 4:go run main.go 而不是 go run .
# 项目有多个 .go 文件时
go run main.go # ❌ 找不到其他文件中的符号
go run . # ✅ 编译当前目录所有 .go
2
3
记牢:永远 go run . / go build .,让目录而非单文件做编译单元。
# ❌ 陷阱 5:把 go mod tidy 当成"普通命令"乱跑
go mod tidy 会:
- 添加
go.mod中缺失的、代码里 import 的依赖 - 删除
go.mod中存在但代码里没 import 的依赖
如果你在大型项目里跑了一半就停,或者在 build tag 之外的代码里没 import 某个包,tidy 会把它从 go.mod 删掉。重要项目改 go.mod 前,先 commit,再跑 go mod tidy,diff 看清楚再提交。
# 2.11 思考题
- 为什么 Go 把
import没用变成编译错误,而不是警告?这种"零容忍"的设计代价是什么? package main与package mylib的二进制产物有什么区别?哪个能被go build直接生成可执行文件?- 如果一个常量
const N = 1 << 100你声明了但从来不用,编译能通过吗?为什么 Go 对未使用的常量没有报错? - 写一段代码,使两个不同包路径的包都名为
rand,并能在同一个文件里使用。 iota在嵌套const块里会怎么计数?请写一段代码验证你的猜测。- 把 2.8 节的猜数字游戏改造,让它读取命令行参数指定区间(
go run . 1 1000)。提示:用os.Args+strconv.Atoi。