02. 函数、结构体与方法

3 阅读5分钟

02. 函数、结构体与方法

这一节会进入 Go 最核心的几个概念。只要你能理解函数、结构体、方法和指针接收者,项目代码基本就能读下去一大半。

本节目标

  • 掌握 Go 的函数定义、多返回值和指针基础
  • 理解结构体是怎么组织数据的
  • 理解方法和值接收者、指针接收者的区别
  • 能看懂项目里的 Handler、Repository、Service 写法

函数定义

Go 的函数声明长这样:

func greet(name string) string {
    return "Hello, " + name
}

多个参数的相同类型可以合并写:

func add(a, b int) int {
    return a + b
}

多返回值

Go 非常常见的一种风格,是函数返回“结果 + 错误”:

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

调用方式:

result, err := divide(10, 2)
if err != nil {
    return err
}
fmt.Println(result)

这就是后面你会经常看到的模式:

repo, err := r.repoRepo.FindOne(ctx, id)
if err != nil {
    return nil, err
}

结构体:Go 组织数据的主要方式

结构体类似其他语言里的对象或数据类,但它本身只负责组织字段,不附带继承体系。

type Repository struct {
    ID       int64
    Owner    string
    Name     string
    FullName string
}

带标签的结构体在 Web 项目中特别常见:

type RepoQueryParams struct {
    Page     int    `query:"page"`
    PageSize int    `query:"page_size"`
    Keywords string `query:"keywords"`
}

项目里常见的标签有:

  • json:"...":JSON 序列化字段名
  • query:"...":查询参数绑定
  • bun:"...":Bun ORM 的数据库字段映射

例如数据库模型里会同时出现多种标签:

type Repository struct {
    ID      int    `bun:"id,pk,autoincrement" json:"id"`
    Name    string `bun:"name,notnull" json:"name"`
    Owner   string `bun:"owner,notnull" json:"owner"`
}

指针:保存另一个值的地址

Go 里的指针没有 C 那么“危险感强”,但它非常常用。你会在项目里频繁看到 *bun.DB*RepoHandler*Repository 这种写法。

count := 10
ptr := &count

fmt.Println(*ptr) // 10

*ptr = 20
fmt.Println(count) // 20

这里有两个符号一定要记住:

  • &x:取变量 x 的地址
  • *p:取指针 p 指向的值

类型上也很好理解:

  • int 是一个整数值
  • *int 是“指向整数的指针”

为什么 Go 代码里到处都是指针

主要有三种原因:

  • 需要修改原对象
  • 避免复制大型结构体
  • nil 表示“没有值”

例如函数想直接修改结构体时:

type Repo struct {
    Name string
}

func rename(repo *Repo, name string) {
    repo.Name = name
}

func main() {
    repo := Repo{Name: "old-name"}
    rename(&repo, "new-name")
    fmt.Println(repo.Name) // new-name
}

如果这里传的是值而不是指针,函数里修改到的就只是副本。

方法:给结构体挂行为

Go 没有 class,但可以给结构体定义方法:

type Repo struct {
    Owner string
    Name  string
}

func (r Repo) FullName() string {
    return r.Owner + "/" + r.Name
}

这里的 (r Repo) 叫接收者。你可以把它理解成“这个函数属于 Repo 类型”。

值接收者 vs 指针接收者

值接收者

func (r Repo) FullName() string {
    return r.Owner + "/" + r.Name
}

适合:

  • 方法只是读取数据
  • 结构体很小
  • 不需要修改原对象

指针接收者

func (r *Repo) Rename(name string) {
    r.Name = name
}

适合:

  • 需要修改原对象
  • 避免结构体复制开销
  • 保持一组方法接收者风格一致

在工程代码里,大多数 RepositoryServiceHandler 都会用指针接收者。

例如:

type RepoRepository struct {
    db *bun.DB
}

func (r *RepoRepository) List(ctx context.Context) ([]Repository, error) {
    // ...
    return nil, nil
}

这里也能看出一个常见规律:

  • Repo 表示值本身
  • *Repo 表示操作原对象

所以“指针接收者”本质上就是“方法拿到的是原对象地址,而不是对象副本”。

构造函数风格

Go 没有真正的构造函数,但社区常用 NewXxx 作为约定:

type RepoHandler struct {
    repoRepo *RepoRepository
}

func NewRepoHandler(repoRepo *RepoRepository) *RepoHandler {
    return &RepoHandler{repoRepo: repoRepo}
}

你在本项目里会看到大量 NewRepoHandlerNewSyncServiceNewDatabaseService 这样的命名。

嵌入:一种轻量复用方式

Go 支持把一个结构体嵌入到另一个结构体中:

type PaginationRequest struct {
    Page     int `query:"page"`
    PageSize int `query:"page_size"`
}

type RepoQueryParams struct {
    PaginationRequest
    Keywords string `query:"keywords"`
}

这在本项目里就是真实存在的写法。它的好处是:

  • 避免重复写分页字段
  • 让通用字段可复用
  • 访问时可以直接 params.Page

一个贴近项目的阅读思路

当你看到下面这种代码时:

type RepoHandler struct {
    repoRepo *repository.RepoRepository
}

func (h *RepoHandler) List(c *fiber.Ctx) error {
    repos, err := h.repoRepo.List(c.Context())
    if err != nil {
        return response.InternalServerCtx(c, "查询仓库列表失败")
    }
    return response.SuccessCtx(c, repos)
}

可以按这个顺序理解:

  1. RepoHandler 是一个结构体,里面依赖了 RepoRepository
  2. List 是绑定在 RepoHandler 上的方法
  3. h.repoRepo.List(...) 是调用仓储层
  4. 返回值里包含 error,所以先判断错误
  5. 成功后再写 JSON 响应

常见易错点

  • & 是取地址,* 在类型里表示指针,在表达式里表示解引用
  • 指针接收者和值接收者混用过多,会让一组方法风格不统一
  • 不是所有东西都要用指针,像 intbool 这类小值通常直接传值更自然
  • 结构体标签写错时,JSON 绑定和 ORM 映射会悄悄失效
  • 以为 Go 的方法就是“类成员函数”,其实底层更接近“带接收者的函数”
  • 命名返回值能用,但不要为了炫技滥用

小结

这一节最关键的是建立这两个直觉:

  1. Go 用结构体组织数据,用方法组织行为,而指针决定你是在操作副本还是原对象。
  2. 工程代码里大量对象都是“结构体 + 指针接收者方法 + NewXxx 初始化”。