golang中的组合多态

一直都觉得自己没入门,学不会golang,不如去学java.进公司也有一年多了,从实习就一直呆在这家公司,2024.10-2025.4似乎刚好一年半. 现在看也许就是编程语言的意识没有切换过来吧. 稍微总结吐槽下自己.万幸,卒获有所闻.

其实golang是门被阉割的语言,OO思想不深的人和深谙OO的人最好切换这种思想.但像我这半吊子,写起来真的是很别扭.记录一下常用的golang语法,方便各位看官看源码的时候方便一些.

Point1: 组合思想

最重要的一点就是组合大于继承,甚至可以夸张到说一句暴论:只有组合没有继承. 直接影响了golang的代码编写风格.

为什么这么说?

是因为Golang的继承就是一种组合,在子类中添加一个父类对象,就算实现了所谓的父类.可是这就是单纯的组合,所以以后别人再和我说用go继承一下,我真的会暴起.

因为之前写的CPP和Java都是会有转型的概念,可以理解为父类指针指向子类对象(将子类对象赋给父类对象),golang没有,这么写就会报错,毕竟是为了解耦合,取消继承是大趋势,Rust好像也是组合优先,这很正常,符合软件开发原则组合优于继承.

go 复制代码
type ParentClass struct { 
// 父结构体的字段
} 
type ChildClass struct {
Parent // 嵌套父结构体 
// 子结构体的字段 
} 
func main() {
c := ChildClass{} // 创建一个 Child 对象 
// p := ParentClass(c) // 强制转型 父类指针指向子类对象 golnag不支持,报错 
}

这同样导致另外一个蛋疼的现象: 多态以隐式实现接口的形式出现.

Java的接口是用implements来实现的,Golang是只管定义,实现了方法就算实现接口了.也就是你可能莫名其妙地就实现了一个接口.这就导致用golang开发的时候,智能提示去找相关的接口实现会给你飙到一些风马牛不相及的地方.

这是一个简单的接口实现,网络协议族的接口NetworkProtocol 以TCP UDP通过实现该接口的三个函数来实现网络协议的接口.

golang 复制代码
package main

import "fmt"

// NetworkProtocolIF 网络协议接口,包含三个方法
type NetworkProtocolIF interface {
   Send(data string) error
   Receive() (string, error)
   GetProtocolName() string
}

// TCP 结构体代表 TCP 协议
type TCP struct {
   port int
}

func (t *TCP) Send(data string) error {
   fmt.Printf("TCP sending data '%s' on port %d\n", data, t.port)
   return nil // 模拟发送成功
}

func (t *TCP) Receive() (string, error) {
   receivedData := fmt.Sprintf("TCP received data on port %d", t.port)
   fmt.Println(receivedData)
   return receivedData, nil // 模拟接收成功
}

func (t *TCP) GetProtocolName() string {
   return "TCP"
}

// UDP 结构体代表 UDP 协议
type UDP struct {
   port int
}

func (u *UDP) Send(data string) error {
   fmt.Printf("UDP sending data '%s' on port %d\n", data, u.port)
   return nil // 模拟发送成功
}

func (u *UDP) Receive() (string, error) {
   receivedData := fmt.Sprintf("UDP received data on port %d", u.port)
   fmt.Println(receivedData)
   return receivedData, nil // 模拟接收成功
}

func (u *UDP) GetProtocolName() string {
   return "UDP"
}

func processProtocol(protocol NetworkProtocolIF) {
   fmt.Println("Processing protocol:", protocol.GetProtocolName())
   err := protocol.Send("Hello, network!")
   if err != nil {
   	fmt.Println("Error sending:", err)
   }
   _, err = protocol.Receive()
   if err != nil {
   	fmt.Println("Error receiving:", err)
   }
   fmt.Println()
}

func main() {
   processProtocol(&TCP{port: 8080})
   processProtocol(&UDP{port: 9000})
}

从上面代码和可以看出,processProtocol(protocol NetworkProtocolIF )方法的参数是NetworkProtocolIF接口,而调用该方法的两个地方传入的是TCPUDP对象指针,这正是朴素上的多态体现,实现了相同接口从而达到一个函数被多个不同类型所调用的现象.

关于结构体方法的接收者类型和使用注意事项:

  1. 接收者类型选择:
  • 结构体方法的接收者可以是值类型(普通对象)或指针类型
  • 建议保持一致:一个结构体的所有方法最好统一使用值接收者或指针接收者
  1. 方法调用灵活性(go对指针的特殊处理):
  • 无论接收者是值类型还是指针类型,都可以通过值或指针来调用方法
  • 关键区别:当接收者为指针类型 时,方法内部对结构体的修改会反映到调用者
  1. 接口实现规则:
  • 如果方法使用值接收者,实现接口时可以传值或指针
  • 如果方法使用指针接收者,实现接口时只能传指针

Part2 万能类型空接口

记得大学时学C/C++, 爱说的万能类型是nil,万物皆可nil,万物皆可new. Golang似乎也有万能类型就是空接口.

万能类型其实就是空接口(不含任何方法的接口),可以把任何对象赋值给空接口对象(实例化空接口),类似 Java 中的Object.

golang 复制代码
// any 和 interface{} 等价,都可以表示空接口
// `interface{}` 是 Go 语言内置的空接口类型
// `any` 是 Go 1.18 引入的预定义类型别名:`type any = interface{}` 属于语法糖
type any = interface{}

var a1 interface{}  // 定义一个空接口变量 a1
var a2 any          // 定义一个空接口变量 a2
  1. 空接口 (interface{}/any)

    • 使用 eface 表示
    • 不包含方法集信息
    • 空接口有额外的内存开销(两个指针大小)
  • 涉及动态类型检查,比静态类型慢
  1. 非空接口
    • 使用 iface 结构表示 (定义在同一个文件中)
    • 包含方法表信息

空接口的底层实现:

golang 复制代码
// src/runtime/runtime2.go
type eface struct {
	_type *_type          // 类型指针
	data  unsafe.Pointer  // 数据指针
}

接口可以通过断言判断其动态类型,说明接口中保存了赋值变量的类型,即:_type *_type 接口保存的是变量值,则修改后不影响,接口保存的是指针,则会影响num值

interface{}注意点

golang 复制代码
var dataSlice []int = foo()
var interfaceSlice []interface{} = dataSlice


cannot use dataSlice (type []int) as type []interface { } in assignment

go.dev/wiki/Interf...

[]interface{}那么问题是,"当我可以将任何类型分配给 时,为什么不能将任何切片分配给interface{}?"

  • 万能类型是interface{},不是[]interface{}[]int可以赋值给interface{},但不能赋值给[]interface{}

  • 从内存的角度,假设长度为N[]interface{},那么所占空间就是N*2,因为一个interface{}中含有两个指针;而对于长度为N[]int,那么所占空间就是N*sizeof(MyType)

interface{}与...语法糖的坑

上述问题,单看还算好理解,一旦和别的掺杂在一起就比较恶心了. ...语法用在函数定义等价于规整为切片,在使用上等价切片打散拆分为单个子元素

golang 复制代码
package main

import "fmt"

func f(a ...int) {
    fmt.Printf("参数类型: %T, 值: %v\n", a, a)
}

func g(b ...interface{}) {
    fmt.Printf("参数类型: %T, 值: %v\n", b, b)
}

func main() {
    // 情况1: 直接传递多个int参数
    f(1, 2, 3, 4)  // 参数类型: []int, 值: [1 2 3 4]
    
    // 情况2: 展开int切片
    f([]int{1, 2, 3, 4}...)  // 参数类型: []int, 值: [1 2 3 4]
    
    // 情况3: 尝试展开int8切片 - 会编译错误
    // f([]int8{1, 2, 3, 4}...)  // 错误: cannot use []int8{...} as []int
    
    // 情况4: 使用interface{}可变参数
    g(1, "hello", true)  // 参数类型: []interface {}, 值: [1 hello true]
    
    // 情况5: 展开任意类型切片到interface{}
    ints := []int{1, 2, 3, 4}
    g(ints...)  // 参数类型: []interface {}, 值: [1 2 3 4]
    
    // 情况6: 展开interface{}切片
    mix := []interface{}{1, "world", 3.14}
    g(mix...)  // 参数类型: []interface {}, 值: [1 world 3.14]
    
    // 情况7: 展开字符串切片
    strs := []string{"a", "b", "c"}
    g(strs...)  // 参数类型: []interface {}, 值: [a b c]
    
    // 情况8: []int{1, 2, 3, 4}...
    f([]int{1, 2, 3, 4}...) // 数会转换成[][]interface{}{{1, 2, 3, 4}}
    
    // 情况9: []int{1, 2, 3, 4}...
    f([]int{1, 2, 3, 4}...) 报错
     因为被拆分为 int, int ,而不是interfce{},interface{}
}

参考

segmentfault.com/a/119000002...

two.github.io/2019/09/02/...

相关推荐
Asthenia04128 分钟前
用RocketMQ和MyBatis实现下单-减库存-扣钱的事务一致性
后端
Pasregret21 分钟前
04-深入解析 Spring 事务管理原理及源码
java·数据库·后端·spring·oracle
Micro麦可乐22 分钟前
最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用
java·spring boot·后端·spring·intellij-idea·spring security
returnShitBoy30 分钟前
Go语言中的defer关键字有什么作用?
开发语言·后端·golang
Asthenia041243 分钟前
面试场景题:基于Redisson、RocketMQ和MyBatis的定时短信发送实现
后端
Asthenia04121 小时前
链路追踪视角:MyBatis-Plus 如何基于 MyBatis 封装 BaseMapper
后端
Ai 编码助手1 小时前
基于 Swoole 的高性能 RPC 解决方案
后端·rpc·swoole
翻滚吧键盘1 小时前
spring打包,打包错误
java·后端·spring
夕颜1112 小时前
记录一下关于 Cursor 设置的问题
后端
凉白开3382 小时前
Scala基础知识
开发语言·后端·scala