主题
第06章 面试题精选 —— 结构体与方法
★ 基础题
Q1:Go 有 class 吗?怎么实现面向对象?
答:Go 没有 class。面向对象通过:
- struct 承载数据
- method 承载行为
- interface 表达抽象
- 嵌入 实现复用
Go 不强求面向对象,反对继承——推崇组合。
Q2:值接收者和指针接收者的区别?
go
func (r Rectangle) Foo() { ... } // 值接收者,r 是副本
func (r *Rectangle) Bar() { ... } // 指针接收者,r 指向原对象| 值接收者 | 指针接收者 | |
|---|---|---|
| 改字段 | 不影响外部 | 影响外部 |
| 调用开销 | 拷贝整个 struct | 拷贝 8 字节指针 |
| 适用 | 小结构、只读 | 大结构、需修改、含 sync 字段 |
Q3:什么时候必须用指针接收者?
- 要修改字段
- 结构体含 sync.Mutex 等不可拷贝类型
- 结构体很大(拷贝代价高)
Q4:以下代码输出什么?
go
type Cat struct{ Age int }
func (c Cat) Birthday() { c.Age++ }
func main() {
cat := Cat{Age: 3}
cat.Birthday()
fmt.Println(cat.Age)
}答:3。
Birthday 用值接收者,c 是 cat 的副本,自增后丢弃。改成 func (c *Cat) 才会输出 4。
Q5:Go 中的"继承"是怎么实现的?
通过嵌入(embedding):
go
type Animal struct{ Name string }
func (a Animal) Eat() { ... }
type Dog struct {
Animal // 嵌入 Animal
Breed string
}
d := Dog{}
d.Eat() // ✅ 自动获得 Animal 的 Eat
d.Name // ✅ 自动获得 Animal 的 Name 字段★★ 进阶题
Q6:嵌入和继承有什么本质区别?
| 维度 | 继承 (Java/C++) | 嵌入 (Go) |
|---|---|---|
| 关系 | is-a(子类是父类) | has-a(持有匿名字段) |
| 内层访问 | super.foo() | obj.Inner.Foo() |
| 多态 | 子类引用可赋父类指针 | 需要通过 interface |
| 字段冲突 | 编译错误或显式覆盖 | 外层字段优先,编译不报错 |
Q7:以下代码输出什么?
go
type A struct{ X int }
type B struct{ A; X int }
b := B{}
b.X = 1
b.A.X = 2
fmt.Println(b.X, b.A.X)答:1 2
b.X访问外层字段b.A.X访问内层字段- 它们是两个独立的字段
Q8:方法集(method set)是什么?
每个类型都有一个方法集——能调用的方法列表。
| 类型 | 方法集 |
|---|---|
T | 所有 receiver 是 T 的方法 |
*T | 所有 receiver 是 T 或 *T 的方法 |
重要影响:interface 实现判断只看方法集!
go
type Stringer interface { String() string }
type S struct{}
func (s *S) String() string { return "S" }
var i Stringer
i = &S{} // ✅ *S 的方法集包含 String
i = S{} // ❌ S 的方法集不包含 String(因为定义在 *S 上)这是非常常见的"为什么我的接口没实现"问题的根源。
Q9:结构体能用 == 比较吗?
go
type P struct{ X, Y int }
p1, p2 := P{1, 2}, P{1, 2}
p1 == p2 // true条件:所有字段都是可比较类型。
不可比较类型:slice、map、func、含这些字段的 struct。
go
type Bad struct{ Items []int }
b1, b2 := Bad{}, Bad{}
b1 == b2 // ❌ 编译错误Q10:Tag 是什么?编译器看吗?
go
type User struct {
ID int `json:"id" db:"user_id"`
}Tag 是字段元数据字符串。编译器不解析它,只检查格式(必须是空格分隔的 key:"value" 对)。
实际使用:
go
field, _ := reflect.TypeOf(User{}).FieldByName("ID")
fmt.Println(field.Tag.Get("json")) // "id"encoding/json、gorm、validator 都是用反射读 tag。
★★★ 深度题
Q11:以下代码 panic 吗?为什么?
go
type T struct{}
func (t *T) Foo() { fmt.Println("foo") }
var p *T
p.Foo()答:不会 panic!输出 foo。
原因:方法调用本质是函数调用 Foo(p),传 nil 进去。只要方法体内不解引用 t,就不会出错。
但如果方法体写 t.X 就会 panic(nil 解引用)。
Q12:构造函数为什么要返回 *T 而不是 T?
go
func NewServer() *Server { ... } // 推荐
func NewServer() Server { ... } // 不推荐理由:
- 大对象避免拷贝
- 调用者拿到指针后才能调指针接收者方法
- 保持 API 一致性
Q13:怎么实现一个"不可变"结构体?
Go 没有 final 关键字,但可以:
- 小写字段名 + 仅暴露 Getter
go
type Money struct {
amount int64
currency string
}
func New(a int64, c string) Money { return Money{a, c} }
func (m Money) Amount() int64 { return m.amount }外部无法修改字段,方法只用值接收者。
Q14:为什么 sync.Mutex 不能用值接收者方法?
go
type Counter struct {
mu sync.Mutex
n int
}
func (c Counter) Inc() { // ❌ 错误!
c.mu.Lock()
c.n++
c.mu.Unlock()
}问题:值接收者会拷贝整个结构体,连同 Mutex。每次调用都锁的是新的 Mutex,并发安全失效!
正确写法:
go
func (c *Counter) Inc() { ... }Go vet 会警告这种 case。
Q15:什么情况下方法可以"链式调用"?
go
type Builder struct{ s string }
func (b *Builder) Add(x string) *Builder { b.s += x; return b }
func (b *Builder) Build() string { return b.s }
s := (&Builder{}).Add("Hello").Add(" ").Add("World").Build()要点:方法返回 *self,让调用者可以接着 . 下一个方法。
📌 速记口诀
"struct 装数据,method 加行为;不知用啥用指针;嵌入复用避继承;tag 元数据靠反射"
🎯 自测练习
- 实现一个
User结构体,含 ID/Name/Email,提供构造函数NewUser - 实现一个
Stack结构体,支持 Push/Pop/Peek - 解释
var p *T = nil; p.Method()什么时候会 panic - 写一段链式调用风格的 SQL Builder:
Select("*").From("users").Where("id=1")
进入第07章 🚀