Skip to content

第06章 面试题精选 —— 结构体与方法


★ 基础题

Q1:Go 有 class 吗?怎么实现面向对象?

:Go 没有 class。面向对象通过:

  1. struct 承载数据
  2. method 承载行为
  3. interface 表达抽象
  4. 嵌入 实现复用

Go 不强求面向对象,反对继承——推崇组合。


Q2:值接收者和指针接收者的区别?

go
func (r Rectangle) Foo() { ... }     // 值接收者,r 是副本
func (r *Rectangle) Bar() { ... }    // 指针接收者,r 指向原对象
值接收者指针接收者
改字段不影响外部影响外部
调用开销拷贝整个 struct拷贝 8 字节指针
适用小结构、只读大结构、需修改、含 sync 字段

Q3:什么时候必须用指针接收者?

  1. 要修改字段
  2. 结构体含 sync.Mutex 等不可拷贝类型
  3. 结构体很大(拷贝代价高)

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 用值接收者,ccat 的副本,自增后丢弃。改成 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

条件:所有字段都是可比较类型

不可比较类型:slicemapfunc、含这些字段的 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/jsongormvalidator 都是用反射读 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 { ... }    // 不推荐

理由

  1. 大对象避免拷贝
  2. 调用者拿到指针后才能调指针接收者方法
  3. 保持 API 一致性

Q13:怎么实现一个"不可变"结构体?

Go 没有 final 关键字,但可以:

  1. 小写字段名 + 仅暴露 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 元数据靠反射"


🎯 自测练习

  1. 实现一个 User 结构体,含 ID/Name/Email,提供构造函数 NewUser
  2. 实现一个 Stack 结构体,支持 Push/Pop/Peek
  3. 解释 var p *T = nil; p.Method() 什么时候会 panic
  4. 写一段链式调用风格的 SQL Builder:Select("*").From("users").Where("id=1")

进入第07章 🚀