结构体与方法
问题
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() 方法。