前言

Go 没有 class、没有继承,但通过结构体 + 方法 + 接口实现了更轻量的面向对象。核心哲学一句话:组合优于继承,隐式优于显式。

一、结构体

type Server struct {
	Host     string        // 大写开头 = 导出(公开)
	Port     int           // 小写 = 包内私有
	Tags     []string
	metadata map[string]string
}

// 字面量初始化(推荐: 带字段名, 可读性好且不怕字段调整)
s := Server{
	Host: "10.0.0.1",
	Port: 8080,
	Tags: []string{"prod", "web"},
}

// 零值可用(Go 设计哲学!)
var s2 Server               // Host="" Port=0 Tags=nil, 直接用不会崩
s2.Port = 9090

// 结构体是值类型:赋值即拷贝
s3 := s2
s3.Port = 1000
fmt.Println(s2.Port)        // 9090, 不受影响

构造函数惯例(Go 没有 constructor,用普通函数模拟):

func NewServer(host string, port int) *Server {
	return &Server{
		Host: host,
		Port: port,
	}
}

二、方法与接收者

// 值接收者
func (s Server) Addr() string {
	return fmt.Sprintf("%s:%d", s.Host, s.Port)
}

// 指针接收者:方法内能修改原对象
func (s *Server) SetPort(port int) {
	s.Port = port
}

怎么选? 记住两条规则:

  1. 需要修改接收者 → 必须指针接收者
  2. 结构体较大或包含 mutex 等字段 → 指针接收者(避免拷贝开销/拷贝导致锁失效)
  3. 同一结构体的方法保持统一:要有一个用指针,全用指针
s := NewServer("10.0.0.1", 80)
s.SetPort(8080)             // 语法糖: Go 自动 (&s).SetPort(8080)
fmt.Println(s.Addr())       // 10.0.0.1:8080

三、组合与嵌入(Go 的"继承")

type BaseLogger struct {
	Prefix string
}

func (l BaseLogger) Log(msg string) {
	fmt.Println(l.Prefix, msg)
}

// 嵌入(不是继承!): 直接把类型写进来
type App struct {
	BaseLogger            // 匿名嵌入
	Name    string
}

app := App{BaseLogger: BaseLogger{Prefix: "[app]"}, Name: "web"}
app.Log("started")       // 方法被"提升", 像自己的方法一样调用

嵌入 vs 继承的区别:没有父类子类的多态链条,只是能力复用。需要覆盖时显式重定义:

func (a App) Log(msg string) {          // 覆盖提升的方法
	a.BaseLogger.Log(a.Name + " " + msg) // 显式调用内层的
}

四、接口:隐式实现

Go 接口最独特之处:不需要 implements 声明,只要实现了接口要求的方法集,就自动满足该接口:

type Describer interface {
	Describe() string
}

func (s Server) Describe() string {      // Server 实现了 Describer
	return "server at " + s.Addr()
}

func Print(d Describer) {                // 面向接口编程
	fmt.Println(d.Describe())
}

Print(s)          // ✅ 编译器自动确认 Server 满足 Describer

接口是"能力清单",惯例是小而专:

// 标准库的例子
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface {              // 接口组合
	Reader
	Writer
}

接口值 = 动态类型 + 动态值:

var d Describer
fmt.Println(d == nil)        // true
d = s
fmt.Printf("%T\n", d)        // main.Server —— 动态类型

// 类型断言与 type switch
v, ok := d.(Server)          // 断言成具体类型
if ok { fmt.Println(v.Host) }

switch x := d.(type) {
case Server:
	fmt.Println("是 Server", x.Host)
default:
	fmt.Println("未知类型")
}

⚠️ 经典坑——nil 接口 ≠ 装着 nil 指针的接口:

var p *Server               // p == nil
var d Describer = p         // 接口里装了 (类型=*Server, 值=nil)
fmt.Println(d == nil)       // false!

返回 error 时如果 var e *MyErr; return e,调用方 if err != nil 会误判——返回明确的 nil。

五、两个必会接口

Stringer:自定义打印

func (s Server) String() string {
	return fmt.Sprintf("Server{%s:%d}", s.Host, s.Port)
}
fmt.Println(s)          // Server{10.0.0.1:8080} —— %v 自动调用

error:标准错误接口

type error interface { Error() string }

// 自定义错误类型(比 fmt.Errorf 多了类型信息, 可断言分类处理)
type ConfigError struct {
	File string
	Err  error
}

func (e *ConfigError) Error() string {
	return fmt.Sprintf("config %s: %v", e.File, e.Err)
}

六、实战:用接口解耦

type Notifier interface {
	Notify(title, body string) error
}

type DingtalkNotifier struct{ Webhook string }
func (d DingtalkNotifier) Notify(t, b string) error { /* ... */ return nil }

type EmailNotifier struct{ SMTP string }
func (e EmailNotifier) Notify(t, b string) error { /* ... */ return nil }

// 告警系统只依赖接口, 不关心具体渠道
type Alerter struct{ N Notifier }

func (a Alerter) Fire(title string) {
	_ = a.N.Notify(title, time.Now().String())
}

// 切换渠道只改这一行
alert := Alerter{N: DingtalkNotifier{Webhook: "https://..."}}
alert.Fire("CPU high")

这就是 Go 风格的多态:小接口 + 组合 + 依赖注入。

小结

概念 Go 的做法
类 struct + 方法
继承 嵌入组合(无多态链)
接口 隐式实现、小接口、面向能力
多态 接口变量持有动态类型
陷阱 nil 接口、值/指针接收者混用

本文是「Golang」系列第 4 篇。