大家好,我是极客老墨。
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)是安全的断言方式ok为true表示断言成功,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.Reader 和 io.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 个关键点:
- 结构体封装数据:通过字段组织相关数据,支持嵌入实现组合
- 接口定义行为:一组方法签名,隐式实现,无需 implements 关键字
- 鸭子类型:只要实现了接口的所有方法,就自动实现了该接口
- 组合优于继承:通过嵌入和接口实现代码复用,比继承更灵活
- 类型断言:用于检查接口变量的实际类型,记得检查 ok 避免 panic
实战建议:
- 接口要小而精,单一职责
- 在使用方定义接口,不在实现方
- 接受接口,返回具体类型
- 优先使用组合,避免深层嵌套
- 指针接收者用于修改状态或大对象
- 注意接口的 nil 判断陷阱
Go 的接口设计简洁而强大,理解了接口,就理解了 Go 的设计哲学。
练习题
练习题 1:实现动物接口(⭐)
定义一个 Animal 接口,包含 Eat() 和 Sleep() 方法,然后实现 Dog 和 Cat 两个结构体。
要求:
- 每个方法打印不同的信息
- 编写一个函数接收
Animal接口参数 - 测试多态性
练习题 2:结构体嵌入(⭐⭐)
创建一个 Person 结构体,嵌入 Address 结构体(包含 City 和 Street 字段),演示字段提升。
要求:
- 实现
Person的String()方法 - 演示直接访问嵌入字段
- 演示通过嵌入类型访问字段
练习题 3:形状计算器(⭐⭐)
实现一个 Shape 接口(包含 Area() 和 Perimeter() 方法),创建 Rectangle、Circle、Triangle 三个结构体实现该接口。
要求:
- 实现准确的面积和周长计算
- 编写测试用例验证计算结果
- 实现一个
TotalArea(shapes []Shape)函数
练习题 4:类型选择(⭐⭐)
编写一个函数,接收 interface{} 参数,使用类型选择判断参数类型并打印相应信息。
要求:
- 支持 int、string、bool、[]int、map[string]int 类型
- 对于不支持的类型,打印类型名称
- 编写测试用例
练习题 5:接口组合(⭐⭐⭐)
定义 Reader 和 Writer 接口,然后组合成 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 个不同的插件
- 实现插件注册和查找机制
- 支持按名称执行插件
- 处理插件不存在的情况
思考题
- 为什么 Go 选择隐式接口实现? 相比 Java 的显式实现有什么优势?
- 什么时候应该使用值接收者,什么时候用指针接收者? 有没有通用的判断标准?
- 接口的 nil 判断为什么这么容易出错? Go 为什么这样设计?
- “接受接口,返回具体类型"这个原则的理由是什么? 有没有例外情况?
快来评论区秀出你的想法,大家一起讨论!
你在项目中是怎么使用接口的?遇到过哪些坑?欢迎评论区聊聊。
极客老墨,继续折腾! 💪
如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。
完整示例代码在 go-tutorial-code/08-struct-interface。