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
}
适合:
- 需要修改原对象
- 避免结构体复制开销
- 保持一组方法接收者风格一致
在工程代码里,大多数 Repository、Service、Handler 都会用指针接收者。
例如:
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}
}
你在本项目里会看到大量 NewRepoHandler、NewSyncService、NewDatabaseService 这样的命名。
嵌入:一种轻量复用方式
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)
}
可以按这个顺序理解:
RepoHandler是一个结构体,里面依赖了RepoRepositoryList是绑定在RepoHandler上的方法h.repoRepo.List(...)是调用仓储层- 返回值里包含
error,所以先判断错误 - 成功后再写 JSON 响应
常见易错点
&是取地址,*在类型里表示指针,在表达式里表示解引用- 指针接收者和值接收者混用过多,会让一组方法风格不统一
- 不是所有东西都要用指针,像
int、bool这类小值通常直接传值更自然 - 结构体标签写错时,JSON 绑定和 ORM 映射会悄悄失效
- 以为 Go 的方法就是“类成员函数”,其实底层更接近“带接收者的函数”
- 命名返回值能用,但不要为了炫技滥用
小结
这一节最关键的是建立这两个直觉:
- Go 用结构体组织数据,用方法组织行为,而指针决定你是在操作副本还是原对象。
- 工程代码里大量对象都是“结构体 + 指针接收者方法 +
NewXxx初始化”。