跳转至

Go 基础语法

学习目标

学完本章后,学习者应该能够:

  1. 掌握变量、常量、基础类型、条件、循环和 switch
  2. 理解函数、多返回值、指针、数组、slice、map、string。
  3. 使用 struct、方法和 package 组织基础代码。
  4. 能写出清晰的 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 一种循环关键字:

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

for _, user := range users {
    fmt.Println(user.Name)
}

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 社区的代码风格。

指针

指针保存变量地址。常见用途:

  • 避免复制较大的结构体。
  • 修改调用方传入的对象。
  • 表达可选值。
  • 为方法接收者提供修改能力。
func Rename(user *User, name string) {
    user.Name = name
}

不要为了“看起来高级”到处使用指针。小结构体、不可变值、简单参数直接传值更清晰。

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 排序。

实战任务

实现一个用户注册请求的校验函数:

  1. Email 不能为空且包含 @
  2. Password 长度不能小于 8。
  3. Age 必须大于等于 18。
  4. 返回明确错误信息。
参考答案

可以定义请求结构体,并给它实现 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.Mutexsync.RWMutexsync.Map,或把状态收敛到单 goroutine 管理。