首页
学习
活动
专区
圈层
工具
发布

别再被误导了!Go 里的 any 和 interface{} 真的完全一样吗?

golang技术圈有个未解之谜:any和interface{}到底有啥区别?

虽然泛型已经发布四年了,我敢打赌,90% 的人都会说:“这不就是个别名吗?官方文档都写了type any = interface{},它们完全一样!”

先说结论:技术上完全一样,但语义上天差地别

官方定义很简单粗暴:

type any = interface{}

翻译成人话:any就是interface{}的别名,底层一模一样,编译器看到它们俩就是一个东西。

那 Go 团队吃饱了没事干,搞这么个any出来干嘛?

为了代码可读性!

语义层面的核心差异

interface{}:动态类型的老玩家

看到interface{},你的第一反应应该是:"这里要用类型断言/反射。"

典型场景:JSON 解析、反射调用、不确定类型的数据。

// 经典的动态类型用法

func processData(data interface{}) {

  if v, ok := data.(string); ok {

      fmt.Println("是个字符串:", v)

  } else if v, ok := data.(int); ok {

      fmt.Println("是个整数:", v)

  }

}

这种代码读起来就像拆盲盒:你永远不知道里面是啥,得一层层拆开看。

any:泛型的新宠儿

看到any,你的第一反应应该是:"这是泛型,类型在编译时就定下来了。"

典型场景:泛型函数、泛型数据结构。

// 泛型函数的用法

func Print[T any](val T) {

  fmt.Println(val)

}

这玩意儿读起来就像:我知道它类型是啥,但我懒得写出来,编译器你帮我推一下。

那个让人吐血的重复代码问题

在 Go 1.18 泛型出来之前,写后端代码最痛苦的是什么?

Ctrl+C/Ctrl+V 换个类型,又是一个函数。

看个经典场景:写求和函数。

// 求和 []int64

func SumInts(numbers []int64)int64 {

  var s int64

  for _, v := range numbers {

      s += v

  }

  return s

}

// 求和 []float64 —— 等等,这代码怎么似曾相识?

func SumFloats(numbers []float64)float64 {

  var s float64

  for _, v := range numbers {

      s += v

  }

  return s

}

这玩意儿简直是 DRY 原则的反面教材。

每次改个 bug,你得改两个地方;漏改一个,线上就炸。

泛型三板斧

Go 1.18 救了我们,带来了泛型。核心就仨概念:

1.类型参数:让函数/类型支持参数化类型

2.类型约束:限制类型参数的范围

3.类型推导:编译器自动推导类型,少写废话

重构求和函数:从屎山到优雅

直接上代码:

// 一个函数搞定 int64 和 float64

func SumNumbers[T int64 | float64](numbers []T) T {

  var s T

  for _, v := range numbers {

      s += v

  }

  return s

}

这里[T int64 | float64]是啥意思?

•T是类型变量(就像函数的参数)

•int64 | float64是类型约束(限制 T 只能是这两个)

调用的时候不需要指定类型,编译器自动推导:

ints := []int64{1, 2, 3}

floats := []float64{1.1, 2.2, 3.3}

fmt.Println(SumNumbers(ints))    // 6,编译器知道 T 是 int64

fmt.Println(SumNumbers(floats)) // 6.6,编译器知道 T 是 float64

这代码看着就清爽,强迫症表示很满意。

复杂场景:自定义类型约束

如果你的泛型函数类型约束比较复杂,每次都写int64 | float64 | string | ...,看着就头大。

Go 允许你把约束写成 interface:

// 定义一个数字约束

type Number interface {

  int64 | float64

}

// 用自定义约束重构

func SumNumbers[T Number](numbers []T) T {

  var s T

  for _, v := range numbers {

      s += v

  }

  return s

}

这样写起来就像在说:"这个泛型函数,T 只要是 Number 家族的就行。"

代码的可读性和可维护性直接起飞。

Go 泛型的底层黑科技

很多人担心泛型会有性能问题,Go 团队显然也考虑到了这一点。

他们搞了个很骚的实现:GC Shape Monomorphization + Dictionaries

听着像天书?我用人话给你解释。

GC Shape Monomorphization:按形状生成代码

Go 编译器不会为每个类型都生成一份代码,而是按类型的"GC 形状"来生成。

啥是 GC 形状?简单说就是:大小、对齐方式、有没有指针。

举个例子:

•int32、uint32、float32都是 4 字节、无指针 同一形状,复用代码

•*int、*string都是指针类型 同一形状,复用代码

Dictionary:同形状不同行为

但问题来了:int和float32虽然可能同形状,但加法操作不一样啊。

Go 编译器用了一种叫 "Dictionary" 的黑科技:

编译器偷偷在函数调用时传了一个隐藏参数,里面记录了类型特定的信息(比如方法地址、操作函数)。

实际影响:性能和体积都稳如老狗

性能:接近原生代码

• 算术操作:和非泛型代码一个速度

• 方法调用:有点像 interface 调用,但基本可以忽略

二进制体积:不会膨胀成气球

因为同形状的类型复用代码,二进制体积控制得很好。

不像 C++ 模板,一用泛型,二进制直接翻倍。

总结:什么时候用哪个?

虽然any和interface{}技术上一样,但 Go 团队给我们传递了一个信号:

记住这条铁律:看到any想到泛型,看到interface{}想到动态类型。

最后一句掏心窝子的话

any的出现,标志着 Go 类型系统的一次进化。

它不是为了技术上的不同,而是为了语义上的清晰。

代码写出来是给人看的,不是给机器看的。

语义清晰了,维护起来就不头疼;维护不头疼,你就能准时下班。

这才是我们追求的终极目标,不是吗?

如果这篇文章让你对 Go 泛型有了新认识,点个赞再走呗。

你有没有乱用any和interface{}?

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OhMJr3bvRvF00e5cajzdWVPQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券