4、Go面向对象:方法、接口、空接口、类型断言、错误处理

说到Go的面向对象,我得先坦白一件事。

我刚从Java转Go那会儿,第一反应是:「这玩意儿也能叫面向对象?」没有class,没有继承,连个构造函数都没有。但用了一段时间后,我反而觉得Go这套设计更实在。它不跟你玩那些花里胡哨的继承树,而是用组合和接口来解决问题。

说白了,Go的面向对象就是「轻量级」的。它只保留了面向对象最核心的两个东西:封装多态。怎么实现的?靠方法(Methods)和接口(Interface)。

4.1 方法:给类型绑定行为

在Go里,方法就是带接收者(Receiver)的函数。接收者可以是结构体,也可以是任何自定义类型。

type User struct {
    Name string
    Age  int
}

// 值接收者
func (u User) SayHello() {
    fmt.Printf("你好,我是%s\n", u.Name)
}

// 指针接收者
func (u *User) Birthday() {
    u.Age++
}

这里有个细节我踩过坑:值接收者和指针接收者怎么选?

我的经验是:如果你要修改接收者的状态,或者接收者是个大结构体(拷贝代价高),就用指针接收者。否则用值接收者就行。

我的习惯:同一个类型的方法,接收者类型要保持一致。要么全用值,要么全用指针。混着用容易让人困惑。

4.2 接口:Go的「协议」

接口是Go实现多态的关键。但Go的接口和Java不一样——它是隐式实现的。

什么意思?你不需要写implements关键字。只要一个类型实现了接口里的所有方法,它就自动实现了这个接口。

type Speaker interface {
    Speak() string
}

type Dog struct{}
func (d Dog) Speak() string { return "汪汪" }

type Cat struct{}
func (c Cat) Speak() string { return "喵喵" }

// 任何实现了Speaker的类型都能传进来
func MakeSound(s Speaker) {
    fmt.Println(s.Speak())
}

我在项目中经常用接口来做依赖注入。比如数据库操作,先定义一个UserRepository接口,然后分别实现MySQL版和Redis版。切换起来特别方便。

核心思想:「只要它像鸭子一样走路,像鸭子一样叫,那它就是鸭子。」——这就是Go的鸭子类型。

4.3 空接口:万能的容器

空接口interface{}没有定义任何方法,所以任何类型都实现了空接口

var anything interface{}
anything = 42
anything = "hello"
anything = []int{1, 2, 3}

听起来很强大对吧?但我要提醒你:空接口是一把双刃剑

我曾经在一个项目里看到有人到处用interface{},结果代码里全是类型断言,可读性极差。后来重构时,能用具体类型的地方全换成了具体类型,代码清爽多了。

避坑指南:空接口会丢失类型信息,编译器无法帮你检查类型错误。能用具体类型就别用空接口。只有在你确实需要处理「任意类型」时(比如JSON解析、日志参数),才考虑用它。

4.4 类型断言:从接口中取出具体值

当你有一个接口类型的变量时,怎么拿到它背后的具体值?用类型断言。

var i interface{} = "hello"

// 安全断言
s, ok := i.(string)
if ok {
    fmt.Println(s) // 输出: hello
}

// 不安全的断言(会panic)
s2 := i.(string) // 如果i不是string类型,这里就崩了

我建议永远使用带ok的断言形式。除非你100%确定类型是对的,否则别偷懒。

还有一种场景是类型选择(Type Switch):

func checkType(v interface{}) {
    switch v.(type) {
    case string:
        fmt.Println("字符串")
    case int:
        fmt.Println("整数")
    default:
        fmt.Println("未知类型")
    }
}

这个语法在解析JSON或处理不确定类型的数据时特别好用。

4.5 错误处理:error、panic与recover

Go的错误处理一直是个热门话题。它不像Java那样有try-catch,而是把错误当作值来传递。

4.5.1 error接口

Go内置的error就是一个接口:

type error interface {
    Error() string
}

你只需要实现Error()方法,就能自定义错误。

type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("错误%d: %s", e.Code, e.Message)
}

func DoSomething() error {
    return &MyError{Code: 404, Message: "资源未找到"}
}

我习惯在函数返回值里总是把error放在最后一个。这是Go社区的约定俗成,别打破它。

4.5.2 panic:真的需要它吗?

panic是Go里的「紧急停止」。一旦触发,函数会立即停止执行,然后逐层向上返回,直到程序崩溃。

func divide(a, b int) int {
    if b == 0 {
        panic("除数不能为0")
    }
    return a / b
}

但说实话,我在生产代码里很少用panic。因为panic意味着程序挂了。只有遇到「不应该发生的错误」时,比如数组越界、空指针解引用,才考虑用panic。

我的原则:可预见的错误用error处理,不可预见的错误才用panic。别把panic当成异常来用。

4.5.3 recover:最后的防线

recover可以捕获panic,让程序继续运行。但它只能在defer函数里生效。

func safeDivide(a, b int) (result int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到panic:", r)
            result = 0 // 设置默认返回值
        }
    }()
    return a / b
}

这个模式在HTTP服务里很常见——每个请求都用一个goroutine处理,如果某个goroutine panic了,recover能保证服务不崩溃。

经验之谈:recover只应该在goroutine的顶层使用。别到处乱放,否则会掩盖真正的bug。我曾经在一个项目里看到有人每个函数都加recover,结果线上出了bug根本查不到日志——全被recover吞掉了。

4.6 小结

Go的面向对象设计,说白了就是「够用就好」。

  • 方法让类型有了行为
  • 接口实现了多态,而且是隐式实现,灵活度很高
  • 空接口是万能容器,但别滥用
  • 类型断言帮你从接口里取出具体值
  • 错误处理用error传递可预见的错误,用panic/recover处理意外情况

你想想看,这些特性组合起来,是不是已经能解决大部分问题了?Go的设计哲学就是「少即是多」。它不给你一堆复杂的继承、重载、异常机制,而是让你用最简单的方式写出可靠的代码。

下一章我们会聊Go的并发模型——goroutine和channel。那才是Go真正让人眼前一亮的地方。