← Backend / Go

02_接口错误与泛型

Go 接口的方法集、错误包装与判断、泛型使用边界和包组织。

Go 接口、错误与泛型

学习目标:理解 Go 如何通过小接口组合能力、显式处理错误,并在必要时使用泛型。

1. 接口按行为定义

接口(interface)描述方法集合。类型只要实现了这些方法就满足接口,无需显式声明 implements。

package main

import "fmt"

type Named interface { Name() string }

type User struct { Username string }

func (u User) Name() string { return u.Username }

func greet(n Named) string { return "你好," + n.Name() }

func main() { fmt.Println(greet(User{Username: "小明"})) }

接口通常由使用方按需求定义,保持小而清晰。接收者是值还是指针会影响方法集:若方法定义在 *T 上,通常只有 *T 满足含该方法的接口。

2. 错误是返回值

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("not found")

func find(id int) (string, error) {
    if id != 1 { return "", fmt.Errorf("find id %d: %w", id, ErrNotFound) }
    return "第一条", nil
}

func main() {
    _, err := find(2)
    if errors.Is(err, ErrNotFound) { fmt.Println("返回 404") }
}

%w 保留错误链,调用者可用 errors.Is 或 errors.As 判断;err.Error() 文本不适合作为稳定控制流程。错误应携带上下文,但不要重复包装到难以阅读,也不要把内部细节直接返回 HTTP 客户端。

易错点:`defer` 在函数返回前执行,适合 `file.Close()`;但要先确认获取资源成功,再注册关闭动作。

3. 泛型何时用

类型参数让同一算法处理多种类型,并在编译期检查。

func Contains[T comparable](items []T, wanted T) bool {
    for _, item := range items {
        if item == wanted { return true }
    }
    return false
}

只有当多种具体类型确实共享同一行为时才引入泛型;HTTP 业务层通常先写清晰的领域类型和接口。comparable 约束意味着该类型可用 == 比较,不等于它一定适合做业务上的“相等”判断。

4. 项目组织

cmd/api/          # 程序入口
internal/httpapi/ # 请求解析与响应
internal/service/ # 业务规则
internal/store/   # 数据访问

从最小结构开始,避免为一个小服务预先堆叠很多层。包名用短小的小写词;避免循环依赖,接口尽量放在需要它的包中。

自测

  1. 如何保留原始错误以供上层判断?用 fmt.Errorf("context: %w", err) 包装。
  2. 什么时候不需要接口?只有一个实现、没有替换需求且调用方不需要抽象时。

5. 方法集决定谁实现接口

如果 T 定义了值接收者方法,T 和 *T 通常都能调用该方法;如果方法只定义在 *T 上,只有 *T 满足要求该方法的接口。因为接口变量内部保存具体类型和值,把 T 放进去后不一定有可寻址的原值供指针接收者修改。

type Renamer interface { Rename(string) }
type Note struct { Title string }
func (n *Note) Rename(title string) { n.Title = title }

var item Renamer = &Note{Title: "旧标题"}
item.Rename("新标题")

var item Renamer = Note{} 会编译失败。设计接口时通常从调用方需要的行为出发,保持接口小;不要为了测试而给每个结构体提前造一层接口。

6. 错误链与业务边界

使用 fmt.Errorf("查找笔记 %d: %w", id, err) 包装底层错误,并在有足够上下文的边界处理它。errors.Is 匹配哨兵错误,errors.As 提取具体错误类型。向 HTTP 客户端返回稳定的错误代码,例如 note_not_found;日志可保留包装后的内部错误,但须去除密码与个人信息。

if errors.Is(err, ErrNotFound) {
    http.Error(w, "note not found", http.StatusNotFound)
    return
}
// 其他错误记录受控日志,并返回 500。

上段是处理分支片段,不是完整服务。不要比较 err.Error() 的文本;文本可能改变,也可能包含底层实现细节。

7. 泛型和接口的取舍

泛型适合“同一个算法用于多种静态类型”,例如去重或查找;接口适合“调用方只关心某种行为”,例如读写存储。any 是 interface{} 的别名,不意味着可以绕过类型检查后随意断言。类型断言 value, ok := x.(T) 应处理 ok == false;直接 x.(T) 在类型不匹配时会 panic。

练习:为 find 写三个测试,分别验证成功、errors.Is 能识别包装后的 ErrNotFound、其他故障不会被错误归类。