# Go面向对象编程:结构体、接口、组合与多态的实现

Go国密SDK适配从入门到实战 - 第4节

4.1 Go的面向对象——它真的“面向对象”吗?

说实话,我第一次接触Go的时候,也有这个疑问。你说它是面向对象吧,它没有class、没有继承;你说它不是吧,它又有结构体、接口、方法。我当时在做一个加密模块的封装时,硬是没找到合适的姿势来表达“这是一个SM2加密对象”。

后来我才理解——Go的面向对象,走的是另一条路。它不讲血缘关系,只讲能力组合。说白了,Go是用结构体(struct)当类,用方法(method)当行为,用接口(interface)当契约,用组合(composition)替代继承,用接口实现实现多态。

核心观点:Go不是传统的面向对象语言,它通过结构体+接口+组合实现了更灵活的对象模型。你不需要关心“父类子类”那一套,只需要关心“这个对象能干什么”。

我个人习惯把Go的对象模型叫做“能力驱动模型”。你不用问一个对象的出身,你只需要判断它是否实现了某个接口。

4.2 结构体——Go的“类”

4.2.1 基本定义与使用

结构体在Go里就是一组字段的集合。我在做国密SDK的时候,每个算法套件都是一组结构体。比如SM2的密钥对,就可以这样定义:

type SM2KeyPair struct {
    PrivateKey []byte
    PublicKey  []byte
    KeyID      string
}

你想想看,这不就是一个轻量级的“类”吗?它没有构造函数怎么办?嗯,Go习惯用工厂函数:

func NewSM2KeyPair() *SM2KeyPair {
    return &SM2KeyPair{
        KeyID: "default-key-001",
    }
}

这里有个小细节我提一下——用值类型还是指针类型?我一般遵循一个原则:大结构体用指针,小结构体用值。密钥对这种数据量大的,必须用指针,否则每次传参都是整份拷贝,性能扛不住。

4.2.2 方法——给结构体绑定行为

方法就是带接收者的函数。这是Go给结构体“加技能”的方式。我在写SM2签名验证时,会这样组织:

func (k *SM2KeyPair) Sign(data []byte) ([]byte, error) {
    // 签名逻辑
    return nil, nil
}

func (k *SM2KeyPair) Verify(data, signature []byte) bool {
    // 验签逻辑
    return true
}

你看,结构体 SM2KeyPair 现在有了 Sign 和 Verify 两个行为。这种方法调用的写法,和传统面向对象一模一样:

keyPair := NewSM2KeyPair()
sig, _ := keyPair.Sign([]byte("hello"))
valid := keyPair.Verify([]byte("hello"), sig)

小技巧:Go中的方法接收者有值接收者和指针接收者两种。值接收者在方法内部修改不会影响原对象,指针接收者会。我在国密SDK里,所有需要修改状态的方法都用指针接收者,单纯查询的则看情况。

4.2.3 结构体的标签与特殊方法

结构体标签是Go的一个特色。我在做SM4的加解密时,会用到它来标记字段的序列化规则:

type SM4Ciphertext struct {
    IV         []byte `json:"iv"`
    Ciphertext []byte `json:"ciphertext"`
    Tag        []byte `json:"tag,omitempty"`
}

这样,用标准库的 json.Marshal 就能直接序列化,省去手动拼接的麻烦。

4.3 接口——约定优于继承

4.3.1 隐式实现:不用写 implements

这是我个人最喜欢的设计。在Java里,你得写 class Foo implements Bar。在Go里,只要一个类型实现了接口的所有方法,它就自动满足该接口。不需要声明,不需要注册。

type Signer interface {
    Sign(data []byte) ([]byte, error)
}

// SM2KeyPair 因为实现了 Sign 方法,自动成为 Signer 接口的实现者
// 不需要写 "SM2KeyPair implements Signer"

曾经有个同事问我:“这不就是鸭子类型吗?”我说对,Go的接口就是结构化类型系统,它只看你“像不像鸭子”,不看你是不是“鸭子家族”的。

4.3.2 空接口:万能容器

空接口 interface{} 表示任何类型都能装进去。我在做国密SDK的配置模块时,就用它来处理不同类型的配置值:

type Config struct {
    Provider interface{} // 可以是字符串、结构体、函数
    KeySize  int
}

但这里必须提醒你——空接口是把双刃剑。它能装万物,但也让你失去了编译时类型检查。我一般在边界代码(比如输入输出解析)才用,内部逻辑尽量用具体类型。

避坑指南:我曾经在一个SM3哈希适配器里过度使用 interface{},导致运行时 panic 不断,排查了好几个小时才发现是类型断言失败。后来我加了严格的类型断言检查 + 默认分支处理,问题才彻底解决。

4.3.3 类型断言——安全地取出具体类型

func UseSigner(s Signer) {
    // 安全的类型断言
    if kp, ok := s.(*SM2KeyPair); ok {
        fmt.Println("这是SM2密钥对:", kp.KeyID)
    }
}

这个 ok 模式一定要用。我见不少新手直接写 s.(*SM2KeyPair),一旦类型不对,程序就崩了。

4.4 组合——Go的继承替代方案

传统面向对象用继承来复用代码。Go用组合——把一个结构体嵌入另一个结构体里,就能直接用它的字段和方法。

我在设计国密算法的统一接口时,就用到了组合:

type BaseCipher struct {
    Key []byte
}

func (b *BaseCipher) SetKey(key []byte) {
    b.Key = key
}

type SM4Cipher struct {
    BaseCipher              // 嵌入 BaseCipher
    BlockSize int
}

// 现在 SM4Cipher 可以直接调用 SetKey 方法

你看,SM4Cipher 不需要重写 SetKey,直接通过组合就拿过来了。这就叫“更有复用性”。

为什么Go选择组合而非继承?因为继承容易产生复杂的层级关系,组合则让代码更扁平、更容易测试。你想想看,一个对象“有一个”别的对象,比它“是”别的对象,耦合度低得多。

4.5 多态——接口的真正威力

多态的实质是“同一接口,不同实现”。在Go里,通过接口调用方法,实际执行的是具体类型的实现,这就是多态。

我在国密SDK里,定义了统一的 Hashing 接口,让 SM3、SHA256 都能接入:

type Hashing interface {
    Sum(data []byte) []byte
    BlockSize() int
}

// SM3Hash 实现了 Hashing
type SM3Hash struct{}
func (h *SM3Hash) Sum(data []byte) []byte { /* SM3 计算 */ }
func (h *SM3Hash) BlockSize() int { return 64 }

// SHA256Hash 也实现了 Hashing
type SHA256Hash struct{}
func (h *SHA256Hash) Sum(data []byte) []byte { /* SHA256 计算 */ }
func (h *SHA256Hash) BlockSize() int { return 64 }

然后我写一个通用的哈希对比函数,传入接口就行:

func CompareHash(h Hashing, a, b []byte) bool {
    return string(h.Sum(a)) == string(h.Sum(b))
}

// 调用时,传不同实现即可
sm3 := &SM3Hash{}
sha256 := &SHA256Hash{}
result1 := CompareHash(sm3, []byte("data1"), []byte("data2"))
result2 := CompareHash(sha256, []byte("data1"), []byte("data2"))

调用 CompareHash 时,你不用关心传的是SM3还是SHA256。这就是多态——接口统一,行为各异。

4.6 本章核心知识图

Go面向对象核心知识结构 结构体 (struct) 字段 + 方法 = 类的替代 接口 (interface) 隐式实现 + 契约 组合 (Composition) 嵌入 = 继承的替代 方法 (Method) 类型断言 空接口 多态 (Polymorphism) 同一接口,不同行为 关键理解: 结构体定义数据 + 方法;接口定义能力契约;组合复用代码;多态让同一接口可调用不同实现。 这四个概念共同构成Go的“面向对象”体系——没有类继承,但一样优雅强大。

4.7 实践中的几个原则

说了这么多,我总结几个在国密SDK开发中实际用到的原则:

原则 具体做法 我的经验
小接口大组合 接口只包含1-3个方法,复杂功能通过组合多个小接口实现 我在SM2签名模块中定义了 Signer 和 Verifier 两个小接口,再通过组合拼成完整的 CryptoService
接受者一致性 同一个类型的所有方法,要么都用值接收者,要么都用指针接收者 我曾经混用过,导致意想不到的复制行为,后来统一用指针接收者
组合优先于继承 能用嵌入组合解决问题,就不搞复杂的类型层级 国密SDK的算法实现都是组合结构,没有超过2层的嵌套,测试起来特别轻松
空接口少用 只在跨边界处理时使用,内部逻辑用具体类型 我规定SDK中空接口的数量不超过3个,任何新增都要评审

特别提醒:Go的面向对象虽然看起来“简陋”,但写惯了之后你会觉得它更清爽。没有复杂的继承链,没有重载的迷惑,一切靠接口和组合来解决。你在国密SDK里遇到的任何一个对象关系,用这四招都能搞定。