Go 基础语法¶
学习目标¶
学完本章后,学习者应该能够:
- 掌握变量、常量、基础类型、条件、循环和
switch。 - 理解函数、多返回值、指针、数组、slice、map、string。
- 使用 struct、方法和 package 组织基础代码。
- 能写出清晰的 Go 后端数据处理函数。
变量与零值¶
Go 中变量声明后如果没有显式赋值,会使用零值:
| 类型 | 零值 |
|---|---|
| int / float | 0 |
| bool | false |
| string | "" |
| pointer / slice / map / channel / function / interface | nil |
| struct | 每个字段都是对应类型零值 |
零值设计让很多类型可以直接使用,但并不是所有零值都可用。例如 nil map 不能写入。
var counts map[string]int
// counts["go"] = 1 // panic
counts = make(map[string]int)
counts["go"] = 1
条件、循环和 switch¶
Go 只有 for 一种循环关键字:
switch 默认不会自动贯穿到下一个 case:
switch status {
case http.StatusOK:
return "ok"
case http.StatusNotFound:
return "not found"
default:
return "unknown"
}
函数与多返回值¶
Go 常用多返回值表达结果和错误:
func FindUser(id int64) (User, error) {
if id <= 0 {
return User{}, fmt.Errorf("invalid user id: %d", id)
}
return User{ID: id}, nil
}
这让错误处理显式可见,也形成了 Go 社区的代码风格。
指针¶
指针保存变量地址。常见用途:
- 避免复制较大的结构体。
- 修改调用方传入的对象。
- 表达可选值。
- 为方法接收者提供修改能力。
不要为了“看起来高级”到处使用指针。小结构体、不可变值、简单参数直接传值更清晰。
slice、map 和 string¶
slice 是对底层数组的一段视图,包含指针、长度和容量。append 可能复用底层数组,也可能分配新数组。
map 是哈希表,适合按 key 快速查找,但遍历顺序不稳定。
string 是不可变字节序列,通常保存 UTF-8 文本。按字节遍历和按 rune 遍历语义不同。
struct、方法和 package¶
struct 用于表达业务数据:
type User struct {
ID int64
Name string
Email string
}
func (u User) DisplayName() string {
if u.Name != "" {
return u.Name
}
return u.Email
}
package 用于组织代码边界。后端项目中,package 不应该只是按技术分层乱放文件,更应该表达职责边界。
Go 后端实际应用例子¶
例子一:请求参数校验¶
type CreateAPIKeyRequest struct {
Name string
Scopes []string
}
func (r CreateAPIKeyRequest) Validate() error {
if strings.TrimSpace(r.Name) == "" {
return fmt.Errorf("name is required")
}
if len(r.Scopes) == 0 {
return fmt.Errorf("scopes is required")
}
return nil
}
把校验逻辑靠近数据结构,比在 handler 里散落多个 if 更容易维护。
例子二:map 做轻量索引¶
func IndexUsers(users []User) map[int64]User {
index := make(map[int64]User, len(users))
for _, user := range users {
index[user.ID] = user
}
return index
}
这是后端服务里非常常见的模式:批量查询后按 ID 建索引,避免后续重复遍历。
常见误区¶
- 误区一:Go 语法简单,所以不需要认真学。
Go 语法少,但 slice、map、interface、defer、并发和错误处理都有明确边界。
- 误区二:任何地方都用指针更高效。
指针可能带来逃逸、共享可变状态和 nil 风险。是否使用指针要看语义和成本。
- 误区三:map 遍历顺序稳定。
Go 明确不保证 map 遍历顺序。需要稳定输出时,应先取出 key 排序。
实战任务¶
实现一个用户注册请求的校验函数:
Email不能为空且包含@。Password长度不能小于 8。Age必须大于等于 18。- 返回明确错误信息。
参考答案
可以定义请求结构体,并给它实现 Validate 方法:
type RegisterRequest struct {
Email string
Password string
Age int
}
func (r RegisterRequest) Validate() error {
if !strings.Contains(strings.TrimSpace(r.Email), "@") {
return fmt.Errorf("email is invalid")
}
if len(r.Password) < 8 {
return fmt.Errorf("password must be at least 8 characters")
}
if r.Age < 18 {
return fmt.Errorf("age must be at least 18")
}
return nil
}
真实项目中可以进一步把错误码、字段名和用户可见文案分开,避免直接把内部错误暴露给客户端。
面试题¶
1. Go 的零值有什么意义?¶
参考答案
零值让变量在未显式初始化时也有确定状态,降低了使用成本。例如 int 是 0,bool 是 false,string 是空字符串,struct 的字段也会递归使用零值。
但不是所有零值都可以直接使用。nil map 不能写入,nil channel 会永久阻塞,nil pointer 解引用会 panic。
2. slice 和数组有什么区别?¶
参考答案
数组长度是类型的一部分,值语义明显;slice 是对底层数组的一段描述,包含指针、长度和容量。slice 更常用于日常开发,因为长度可变,append 方便。
需要注意 slice 可能共享底层数组,append 也可能触发扩容并分配新数组。
3. 为什么 map 不能并发读写?¶
参考答案
Go 内置 map 不是并发安全的。一个 goroutine 写 map,另一个 goroutine 同时读或写,可能触发运行时 panic,也可能造成数据竞争。
并发场景应使用 sync.Mutex、sync.RWMutex、sync.Map,或把状态收敛到单 goroutine 管理。