大家好,我是极客老墨。

Go 没有类,也没有继承。它用结构体封装数据,用接口定义行为。这种"组合优于继承"的设计,让代码更灵活、更解耦。

这篇就聊聊 Go 的结构体和接口,看看它们是怎么配合工作的。

结构体基础

结构体是一组字段的集合,用来封装相关的数据。

定义和初始化

 1// 定义结构体
 2type Person struct {
 3    Name string
 4    Age  int
 5}
 6
 7// 初始化方式 1:按字段顺序
 8p1 := Person{"Alice", 30}
 9
10// 初始化方式 2:指定字段名(推荐)
11p2 := Person{
12    Name: "Bob",
13    Age:  25,
14}
15
16// 初始化方式 3:部分字段(其余为零值)
17p3 := Person{Name: "Charlie"} // Age 为 0
18
19// 初始化方式 4:使用 new(返回指针)
20p4 := new(Person)
21p4.Name = "David"

要点

  • 字段名首字母大写表示导出(public),小写表示私有(package 内可见)
  • 推荐使用字段名初始化,代码更清晰
  • 未初始化的字段会被赋予零值

访问和修改字段

 1p := Person{Name: "Alice", Age: 30}
 2
 3// 访问字段
 4fmt.Println(p.Name) // Alice
 5
 6// 修改字段
 7p.Age = 31
 8
 9// 指针访问(Go 会自动解引用)
10ptr := &p
11ptr.Name = "Alicia" // 等价于 (*ptr).Name = "Alicia"

匿名字段

结构体可以包含匿名字段(只有类型,没有字段名)。

1type Person struct {
2    string // 匿名字段
3    int    // 匿名字段
4}
5
6p := Person{"Alice", 30}
7fmt.Println(p.string) // Alice
8fmt.Println(p.int)    // 30

注意:匿名字段的类型名就是字段名,实际开发中很少用。

结构体嵌入

Go 通过嵌入实现类似"继承"的效果,但本质是组合。

 1// 基础结构体
 2type Animal struct {
 3    Name string
 4    Age  int
 5}
 6
 7// 嵌入 Animal
 8type Dog struct {
 9    Animal // 匿名嵌入
10    Breed  string
11}
12
13// 使用
14d := Dog{
15    Animal: Animal{Name: "Buddy", Age: 3},
16    Breed:  "Golden Retriever",
17}
18
19// 直接访问嵌入字段
20fmt.Println(d.Name)  // Buddy(提升字段)
21fmt.Println(d.Age)   // 3
22fmt.Println(d.Breed) // Golden Retriever

要点

  • 嵌入的字段会"提升"到外层结构体,可以直接访问
  • 如果有字段名冲突,外层优先
  • 也可以通过 d.Animal.Name 显式访问

嵌入的方法提升

嵌入的结构体的方法也会提升。

 1type Animal struct {
 2    Name string
 3}
 4
 5// Animal 的方法
 6func (a Animal) Speak() string {
 7    return "Some sound"
 8}
 9
10type Dog struct {
11    Animal
12}
13
14// Dog 的方法
15func (d Dog) Bark() string {
16    return "Woof!"
17}
18
19// 使用
20d := Dog{Animal: Animal{Name: "Buddy"}}
21fmt.Println(d.Speak()) // Some sound(继承自 Animal)
22fmt.Println(d.Bark())  // Woof!(Dog 自己的方法)

方法重写

外层结构体可以重写嵌入结构体的方法。

 1type Animal struct {
 2    Name string
 3}
 4
 5func (a Animal) Speak() string {
 6    return "Some sound"
 7}
 8
 9type Dog struct {
10    Animal
11}
12
13// 重写 Speak 方法
14func (d Dog) Speak() string {
15    return "Woof!"
16}
17
18d := Dog{Animal: Animal{Name: "Buddy"}}
19fmt.Println(d.Speak())        // Woof!(Dog 的方法)
20fmt.Println(d.Animal.Speak()) // Some sound(Animal 的方法)

接口基础

接口定义了一组方法签名,任何实现了这些方法的类型都自动实现了该接口。

定义和实现接口

 1// 定义接口
 2type Speaker interface {
 3    Speak() string
 4}
 5
 6// Dog 实现 Speaker
 7type Dog struct {
 8    Name string
 9}
10
11func (d Dog) Speak() string {
12    return "Woof!"
13}
14
15// Cat 实现 Speaker
16type Cat struct {
17    Name string
18}
19
20func (c Cat) Speak() string {
21    return "Meow!"
22}

要点

  • Go 的接口是隐式实现的,不需要 implements 关键字
  • 只要实现了接口的所有方法,就自动实现了该接口
  • 这就是"鸭子类型":走起来像鸭子,叫起来像鸭子,那就是鸭子

接口作为参数

接口可以作为函数参数,实现多态。

 1// 接收任何 Speaker
 2func introduce(s Speaker) {
 3    fmt.Println(s.Speak())
 4}
 5
 6d := Dog{Name: "Buddy"}
 7c := Cat{Name: "Kitty"}
 8
 9introduce(d) // Woof!
10introduce(c) // Meow!

空接口

空接口 interface{} 不包含任何方法,因此任何类型都实现了空接口。

1// 可以接收任何类型
2func printAnything(v interface{}) {
3    fmt.Println(v)
4}
5
6printAnything(42)
7printAnything("hello")
8printAnything([]int{1, 2, 3})

注意:Go 1.18+ 推荐使用 any 代替 interface{},它们是等价的。

1func printAnything(v any) {
2    fmt.Println(v)
3}

类型断言

类型断言用于检查接口变量的实际类型。

基本用法

1var s Speaker = Dog{Name: "Buddy"}
2
3// 类型断言
4if dog, ok := s.(Dog); ok {
5    fmt.Printf("It's a dog named %s\n", dog.Name)
6} else {
7    fmt.Println("Not a dog")
8}

要点

  • value, ok := interfaceVar.(Type) 是安全的断言方式
  • oktrue 表示断言成功,false 表示失败
  • 如果不检查 ok 直接断言,失败时会 panic

类型选择

使用 switch 进行类型选择。

 1func describe(i interface{}) {
 2    switch v := i.(type) {
 3    case int:
 4        fmt.Printf("Integer: %d\n", v)
 5    case string:
 6        fmt.Printf("String: %s\n", v)
 7    case Dog:
 8        fmt.Printf("Dog: %s\n", v.Name)
 9    default:
10        fmt.Printf("Unknown type: %T\n", v)
11    }
12}
13
14describe(42)
15describe("hello")
16describe(Dog{Name: "Buddy"})

接口组合

接口可以嵌入其他接口,形成新的接口。

 1type Reader interface {
 2    Read(p []byte) (n int, err error)
 3}
 4
 5type Writer interface {
 6    Write(p []byte) (n int, err error)
 7}
 8
 9// 组合接口
10type ReadWriter interface {
11    Reader
12    Writer
13}

标准库示例io.ReadWriter 就是 io.Readerio.Writer 的组合。

接口的最佳实践

1. 接口应该小而精

 1// 好:单一职责
 2type Reader interface {
 3    Read(p []byte) (n int, err error)
 4}
 5
 6// 不好:接口太大
 7type FileHandler interface {
 8    Open() error
 9    Close() error
10    Read(p []byte) (n int, err error)
11    Write(p []byte) (n int, err error)
12    Seek(offset int64, whence int) (int64, error)
13}

2. 接受接口,返回具体类型

1// 好:参数是接口,返回值是具体类型
2func NewReader(r io.Reader) *MyReader {
3    return &MyReader{r: r}
4}
5
6// 不好:返回接口
7func NewReader(r io.Reader) io.Reader {
8    return &MyReader{r: r}
9}

3. 在使用方定义接口

不要在实现方定义接口,而是在使用方定义需要的接口。

 1// 使用方定义自己需要的接口
 2type MyService struct{}
 3
 4type DataStore interface {
 5    Save(data string) error
 6}
 7
 8func (s *MyService) Process(store DataStore) {
 9    store.Save("data")
10}

完整示例

把前面的知识点串起来,看个完整的例子:

 1package main
 2
 3import "fmt"
 4
 5// 定义接口
 6type Shape interface {
 7    Area() float64
 8    Perimeter() float64
 9}
10
11// 矩形
12type Rectangle struct {
13    Width  float64
14    Height float64
15}
16
17func (r Rectangle) Area() float64 {
18    return r.Width * r.Height
19}
20
21func (r Rectangle) Perimeter() float64 {
22    return 2 * (r.Width + r.Height)
23}
24
25// 圆形
26type Circle struct {
27    Radius float64
28}
29
30func (c Circle) Area() float64 {
31    return 3.14 * c.Radius * c.Radius
32}
33
34func (c Circle) Perimeter() float64 {
35    return 2 * 3.14 * c.Radius
36}
37
38// 打印形状信息
39func printShapeInfo(s Shape) {
40    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
41
42    // 类型断言
43    switch v := s.(type) {
44    case Rectangle:
45        fmt.Printf("Rectangle: %.2f x %.2f\n", v.Width, v.Height)
46    case Circle:
47        fmt.Printf("Circle: radius %.2f\n", v.Radius)
48    }
49}
50
51func main() {
52    r := Rectangle{Width: 10, Height: 5}
53    c := Circle{Radius: 7}
54
55    printShapeInfo(r)
56    printShapeInfo(c)
57}

这个例子展示了:

  • 接口的定义和实现
  • 多个类型实现同一个接口
  • 接口作为函数参数实现多态
  • 类型断言和类型选择

老墨踩过的坑

坑 1:值接收者 vs 指针接收者

刚开始写 Go 时,老墨经常搞混值接收者和指针接收者。

 1type Counter struct {
 2    count int
 3}
 4
 5// 值接收者:无法修改原对象
 6func (c Counter) Increment() {
 7    c.count++  // 只修改了副本
 8}
 9
10// 指针接收者:可以修改原对象
11func (c *Counter) IncrementPtr() {
12    c.count++  // 修改了原对象
13}
14
15func main() {
16    c := Counter{count: 0}
17    c.Increment()
18    fmt.Println(c.count)  // 0(没变!)
19    
20    c.IncrementPtr()
21    fmt.Println(c.count)  // 1(变了)
22}

教训

  • 需要修改对象状态 → 用指针接收者
  • 对象很大(避免拷贝)→ 用指针接收者
  • 只读操作且对象小 → 用值接收者
  • 同一类型的方法,接收者类型要保持一致

坑 2:接口的 nil 判断

这个坑老墨踩了好几次:

 1type MyError struct {
 2    msg string
 3}
 4
 5func (e *MyError) Error() string {
 6    return e.msg
 7}
 8
 9func doSomething() error {
10    var err *MyError  // nil 指针
11    return err        // 返回的是接口类型
12}
13
14func main() {
15    err := doSomething()
16    if err != nil {  // true!虽然 err 的值是 nil
17        fmt.Println("Error:", err)  // panic: nil pointer dereference
18    }
19}

原因:接口包含类型和值两部分,只有两者都为 nil,接口才为 nil。

正确做法

1func doSomething() error {
2    var err *MyError
3    if err != nil {
4        return err
5    }
6    return nil  // 返回 nil 接口,而不是 nil 指针
7}

坑 3:嵌入字段的方法集

 1type Animal struct {
 2    Name string
 3}
 4
 5func (a *Animal) SetName(name string) {
 6    a.Name = name
 7}
 8
 9type Dog struct {
10    Animal  // 值嵌入
11}
12
13func main() {
14    d := Dog{Animal: Animal{Name: "Buddy"}}
15    d.SetName("Max")  // 编译通过,但...
16    fmt.Println(d.Name)  // 还是 "Buddy"!
17}

原因Dog 值嵌入了 Animal,但 SetName 是指针接收者,实际调用的是 (&d.Animal).SetName(),修改的是临时指针。

正确做法

1type Dog struct {
2    *Animal  // 指针嵌入
3}
4
5func main() {
6    d := Dog{Animal: &Animal{Name: "Buddy"}}
7    d.SetName("Max")
8    fmt.Println(d.Name)  // "Max"
9}

坑 4:接口比较的陷阱

 1type Person struct {
 2    Name string
 3    Tags []string  // 切片不可比较
 4}
 5
 6func main() {
 7    var p1 interface{} = Person{Name: "Alice", Tags: []string{"a"}}
 8    var p2 interface{} = Person{Name: "Alice", Tags: []string{"a"}}
 9    
10    if p1 == p2 {  // panic: comparing uncomparable type
11        fmt.Println("Equal")
12    }
13}

教训:包含不可比较类型(切片、map、函数)的结构体不能直接比较。

实战建议

1. 接口设计原则

接口隔离原则(ISP)

 1// ❌ 不好:接口太大
 2type Database interface {
 3    Connect() error
 4    Close() error
 5    Query(sql string) ([]Row, error)
 6    Insert(table string, data map[string]interface{}) error
 7    Update(table string, data map[string]interface{}) error
 8    Delete(table string, id int) error
 9    BeginTransaction() error
10    Commit() error
11    Rollback() error
12}
13
14// ✅ 好:拆分成小接口
15type Connector interface {
16    Connect() error
17    Close() error
18}
19
20type Querier interface {
21    Query(sql string) ([]Row, error)
22}
23
24type Writer interface {
25    Insert(table string, data map[string]interface{}) error
26    Update(table string, data map[string]interface{}) error
27    Delete(table string, id int) error
28}

2. 使用接口实现依赖注入

 1// 定义接口
 2type UserRepository interface {
 3    GetByID(id int) (*User, error)
 4    Save(user *User) error
 5}
 6
 7// 服务依赖接口,不依赖具体实现
 8type UserService struct {
 9    repo UserRepository
10}
11
12func NewUserService(repo UserRepository) *UserService {
13    return &UserService{repo: repo}
14}
15
16// 可以轻松替换实现(测试时用 mock)
17type MockUserRepository struct{}
18
19func (m *MockUserRepository) GetByID(id int) (*User, error) {
20    return &User{ID: id, Name: "Mock User"}, nil
21}
22
23func (m *MockUserRepository) Save(user *User) error {
24    return nil
25}

3. 结构体标签(Tags)

结构体标签用于元数据,常用于 JSON 序列化、数据库映射等:

 1type User struct {
 2    ID        int       `json:"id" db:"user_id"`
 3    Name      string    `json:"name" db:"user_name"`
 4    Email     string    `json:"email" db:"email"`
 5    CreatedAt time.Time `json:"created_at" db:"created_at"`
 6    Password  string    `json:"-" db:"password"`  // json:"-" 表示不序列化
 7}
 8
 9// JSON 序列化
10user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
11data, _ := json.Marshal(user)
12fmt.Println(string(data))
13// {"id":1,"name":"Alice","email":"alice@example.com","created_at":"..."}

常用标签:

  • json:"field_name" - JSON 序列化
  • xml:"field_name" - XML 序列化
  • db:"column_name" - 数据库映射
  • validate:"required" - 数据验证
  • form:"field_name" - 表单绑定

4. 空结构体的妙用

空结构体 struct{} 不占用内存,常用于:

 1// 1. 实现 Set(集合)
 2type Set map[string]struct{}
 3
 4func (s Set) Add(key string) {
 5    s[key] = struct{}{}
 6}
 7
 8func (s Set) Contains(key string) bool {
 9    _, ok := s[key]
10    return ok
11}
12
13// 2. 信号通道
14done := make(chan struct{})
15go func() {
16    // do something
17    done <- struct{}{}  // 发送信号
18}()
19<-done  // 等待信号
20
21// 3. 实现接口但不需要状态
22type NoOpWriter struct{}
23
24func (NoOpWriter) Write(p []byte) (n int, err error) {
25    return len(p), nil  // 什么都不做
26}

老墨总结

Go 结构体和接口的 5 个关键点:

  1. 结构体封装数据:通过字段组织相关数据,支持嵌入实现组合
  2. 接口定义行为:一组方法签名,隐式实现,无需 implements 关键字
  3. 鸭子类型:只要实现了接口的所有方法,就自动实现了该接口
  4. 组合优于继承:通过嵌入和接口实现代码复用,比继承更灵活
  5. 类型断言:用于检查接口变量的实际类型,记得检查 ok 避免 panic

实战建议

  • 接口要小而精,单一职责
  • 在使用方定义接口,不在实现方
  • 接受接口,返回具体类型
  • 优先使用组合,避免深层嵌套
  • 指针接收者用于修改状态或大对象
  • 注意接口的 nil 判断陷阱

Go 的接口设计简洁而强大,理解了接口,就理解了 Go 的设计哲学。

练习题

练习题 1:实现动物接口(⭐)

定义一个 Animal 接口,包含 Eat()Sleep() 方法,然后实现 DogCat 两个结构体。

要求:

  • 每个方法打印不同的信息
  • 编写一个函数接收 Animal 接口参数
  • 测试多态性

练习题 2:结构体嵌入(⭐⭐)

创建一个 Person 结构体,嵌入 Address 结构体(包含 City 和 Street 字段),演示字段提升。

要求:

  • 实现 PersonString() 方法
  • 演示直接访问嵌入字段
  • 演示通过嵌入类型访问字段

练习题 3:形状计算器(⭐⭐)

实现一个 Shape 接口(包含 Area()Perimeter() 方法),创建 RectangleCircleTriangle 三个结构体实现该接口。

要求:

  • 实现准确的面积和周长计算
  • 编写测试用例验证计算结果
  • 实现一个 TotalArea(shapes []Shape) 函数

练习题 4:类型选择(⭐⭐)

编写一个函数,接收 interface{} 参数,使用类型选择判断参数类型并打印相应信息。

要求:

  • 支持 int、string、bool、[]int、map[string]int 类型
  • 对于不支持的类型,打印类型名称
  • 编写测试用例

练习题 5:接口组合(⭐⭐⭐)

定义 ReaderWriter 接口,然后组合成 ReadWriter 接口,实现一个满足 ReadWriter 的结构体。

要求:

  • Reader 包含 Read(p []byte) (n int, err error)
  • Writer 包含 Write(p []byte) (n int, err error)
  • 实现一个 Buffer 结构体满足 ReadWriter
  • 编写测试用例

练习题 6:插件系统(⭐⭐⭐)

实现一个简单的插件系统:定义 Plugin 接口(包含 Name()Execute() 方法),创建多个插件并动态调用。

要求:

  • 实现至少 3 个不同的插件
  • 实现插件注册和查找机制
  • 支持按名称执行插件
  • 处理插件不存在的情况

思考题

  1. 为什么 Go 选择隐式接口实现? 相比 Java 的显式实现有什么优势?
  2. 什么时候应该使用值接收者,什么时候用指针接收者? 有没有通用的判断标准?
  3. 接口的 nil 判断为什么这么容易出错? Go 为什么这样设计?
  4. “接受接口,返回具体类型"这个原则的理由是什么? 有没有例外情况?

快来评论区秀出你的想法,大家一起讨论!


你在项目中是怎么使用接口的?遇到过哪些坑?欢迎评论区聊聊。

极客老墨,继续折腾! 💪

如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。

完整示例代码在 go-tutorial-code/08-struct-interface


相关阅读