跳到主要内容

结构体与方法

问题

Go 没有 class,如何实现面向对象?值接收者和指针接收者有什么区别?

答案

结构体定义

type User struct {
Name string
Age int
Email string
}

// 创建实例的几种方式
u1 := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
u2 := User{"Bob", 25, "bob@example.com"} // 按字段顺序(不推荐)
u3 := new(User) // *User,零值
u4 := &User{Name: "Charlie"} // *User,最常用

方法

方法通过接收者(receiver) 绑定到类型上:

// 值接收者
func (u User) FullName() string {
return u.Name
}

// 指针接收者
func (u *User) SetAge(age int) {
u.Age = age
}

值接收者 vs 指针接收者

这是 Go 面试的超高频考点

维度值接收者 (u User)指针接收者 (u *User)
方法内操作对象副本原始值
能否修改接收者
值类型调用✅(编译器自动 &u
指针类型调用✅(编译器自动 *p
接口实现User*User 都实现只有 *User 实现
内存开销拷贝整个结构体拷贝指针(8字节)
type Writer interface {
Write(data []byte) error
}

type File struct{}

// 指针接收者实现接口
func (f *File) Write(data []byte) error {
return nil
}

var w Writer
w = &File{} // ✅ *File 实现了 Writer
// w = File{} // ❌ 编译错误!File 没实现 Writer,只有 *File 实现了
接口实现的不对称性

如果方法用值接收者定义,那么值类型和指针类型都实现了接口。 如果方法用指针接收者定义,只有指针类型实现了接口

原因:Go 总是可以从值获取指针(&v),但不一定能从指针恢复出可寻址的值(接口中存的值可能不可寻址)。

使用建议

// ✅ 使用指针接收者的场景
func (u *User) SetName(name string) { u.Name = name } // 需要修改
func (c *BigCache) Get(key string) {} // 大结构体

// ✅ 使用值接收者的场景
func (p Point) Distance(q Point) float64 { ... } // 小结构体,不修改
func (c Color) String() string { ... } // 不可变类型

// ⚠️ 同一类型的所有方法应统一使用值或指针接收者
// 混用会导致接口实现不一致的困惑
一致性原则

如果一个类型的任何一个方法使用了指针接收者,那么所有方法都应该使用指针接收者,保持一致性。

结构体嵌入(组合)

Go 用组合(Composition) 代替继承。通过结构体嵌入实现代码复用:

type Animal struct {
Name string
}

func (a Animal) Speak() string {
return a.Name + " makes a sound"
}

// Dog "继承" Animal(实际是组合)
type Dog struct {
Animal // 匿名嵌入(字段名默认为 Animal)
Breed string
}

d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden",
}

// 直接访问嵌入类型的字段和方法(提升)
fmt.Println(d.Name) // "Buddy"(等价于 d.Animal.Name)
fmt.Println(d.Speak()) // "Buddy makes a sound"(提升的方法)

方法提升规则

// 外层方法优先(覆盖嵌入类型的方法)
func (d Dog) Speak() string {
return d.Name + " barks"
}

d.Speak() // "Buddy barks"(调用 Dog 的方法)
d.Animal.Speak() // "Buddy makes a sound"(显式调用嵌入类型的方法)

结构体比较

type Point struct {
X, Y int
}

p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true(所有字段都可比较)

// 包含不可比较字段的结构体不能用 ==
type Data struct {
Values []int // slice 不可比较
}
// d1 == d2 // 编译错误!
// 用 reflect.DeepEqual(d1, d2) 代替

结构体标签(Tag)

type User struct {
Name string `json:"name" validate:"required"`
Age int `json:"age,omitempty" validate:"min=0,max=150"`
Email string `json:"email" db:"email_addr"`
}

// 通过反射读取标签
t := reflect.TypeOf(User{})
field, _ := t.FieldByName("Name")
fmt.Println(field.Tag.Get("json")) // "name"
fmt.Println(field.Tag.Get("validate")) // "required"

常见面试问题

Q1: Go 如何实现面向对象的封装、继承、多态?

答案

OOP 特性Go 实现方式
封装首字母大小写控制可见性(大写=导出/public,小写=未导出/private)
继承(复用)结构体嵌入(Composition),Go 没有真正的继承
多态接口(Interface),隐式实现
// 封装
type user struct { // 未导出类型
name string // 未导出字段
Age int // 导出字段
}

// 继承(组合)
type Admin struct {
user // 嵌入 user,获得其所有字段和方法
Level int
}

// 多态
type Speaker interface {
Speak() string
}
// 任何实现了 Speak() 的类型都满足 Speaker 接口

Q2: 空结构体 struct{} 有什么用?

答案

空结构体不占用任何内存unsafe.Sizeof(struct{}{}) = 0),常用于:

// 1. 实现 Set(map 的 value 用空结构体)
set := make(map[string]struct{})
set["key"] = struct{}{}
if _, exists := set["key"]; exists { }

// 2. Channel 只做信号通知(不传数据)
done := make(chan struct{})
go func() {
// 完成工作
close(done) // 通知
}()
<-done // 等待

// 3. 实现只有方法没有数据的类型
type Validator struct{}
func (v Validator) Validate(s string) bool { ... }

空结构体节省内存的原理:Go 运行时为所有零大小类型返回同一个地址(runtime.zerobase)。

Q3: 结构体嵌入和字段有什么区别?

答案

// 匿名嵌入(字段名 = 类型名)
type Dog struct {
Animal // 嵌入:方法和字段会"提升"
}
d.Name // OK(提升)
d.Speak() // OK(提升)

// 命名字段
type Dog2 struct {
animal Animal // 普通字段:不会提升
}
d2.animal.Name // 必须显式访问
d2.animal.Speak() // 必须显式访问

嵌入不是继承——嵌入类型的方法被调用时,接收者仍然是嵌入类型本身,不是外层类型。这在多态场景下有区别:

type Base struct{}
func (b Base) Type() string { return "Base" }
func (b Base) Describe() string { return b.Type() + " struct" }

type Child struct{ Base }
func (c Child) Type() string { return "Child" }

c := Child{}
c.Describe() // "Base struct"(不是 "Child struct"!)
// Base.Describe 中调用的 b.Type() 是 Base 的方法,不是 Child 的

Q4: 如何初始化包含匿名字段的结构体?

答案

type Animal struct {
Name string
}

type Dog struct {
Animal
Breed string
}

// 必须显式初始化嵌入字段
d := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden",
}

// 不能这样
// d := Dog{Name: "Buddy", Breed: "Golden"} // 编译错误

Q5: 结构体可以比较吗?

答案

只有当结构体的所有字段都是可比较类型时才可以用 == 比较。包含 slice、map、func 字段的结构体不可比较。

不可比较的结构体可以用 reflect.DeepEqual() 比较,但有性能开销。也可以自定义 Equal() 方法。

相关链接