golang的类型转换
今天我们来说说一个大家每天都在做但很少深入思考的操作——类型转换。
本文索引
一行奇怪的代码
事情始于年初时我对标准库sync做一些改动的时候。
改动会用到标准库在1.19新添加的atomic.Pointer
,出于谨慎,我在进行变更之前泛泛通读了一遍它的代码,然而一行代码引起了我的注意:
// A Pointer is an atomic pointer of type *T. The zero value is a nil *T.
type Pointer[T any] struct {
// Mention *T in a field to disallow conversion between Pointer types.
// See go.dev/issue/56603 for more details.
// Use *T, not T, to avoid spurious recursive type definition errors.
_ [0]*T
_ noCopy
v unsafe.Pointer
}
并不是noCopy,这个我在golang拾遗:实现一个不可复制类型详细讲解过。
引起我注意的地方是_ [0]*T
,它是个匿名字段,且长度为零的数组不会占用内存。这并不影响我要修改的代码,但它的作用是什么引起了我的好奇。
还好这个字段自己的注释给出了答案:这个字段是为了防止错误的类型转换。什么样的类型转换需要加这个字段来封锁呢。带着疑问我点开了给出的issue链接,然后看到了下面的例子:
package main
import (
"math"
"sync/atomic"
)
type small struct {
small [64]byte
}
type big struct {
big [math.MaxUint16 * 10]byte
}
func main() {
a := atomic.Pointer[small]{}
a.Store(&small{})
b := atomic.Pointer[big](a) // type conversion
big := b.Load()
for i := range big.big {
big.big[i] = 1
}
}
例子程序会导致内存错误,在Linux环境上它会有很大概率导致段错误。为什么呢?因为big的索引值大大超过了small的范围,而我们实际上在Pointer只存了一个small对象,所以在最后的循环那里我们发生了索引越界,而且go并没有检测到这个越界。
当然,go也没有义务去检测这种越界,因为用了unsafe(atomic.Pointer是对unsafe.Pointer的包装)之后类型安全和内存安全就只能靠用户自己来负责了。
这里根本上的问题在于,atomic.Pointer[small]
和atomic.Pointer[big]
之间没有任何关联,它们应该是完全不同的类型不应该发生转换(如果对此有疑惑,可以搜索下类型构造器相关的资料,通常这种泛型的类型构造器产生的类型之间是不应该有任何关联性的),尤其是go是一门强类型语言,类似的事情在c++无法通过编译而在python里则会运行时报错。
但事实是在没添加开头的那个字段前这种转换是合法的而且在泛型类型中很容易出现。
到这里你可能还是有点云里雾里,不过没关系,看完下一节你会云开雾散的。
go的类型转换
golang里不存在隐式类型转换,因此想要将一个类型的值转换成另一个类型,只能用这样的表达式Type(value)
。表达式会把value复制一份然后转换成Type类型。
对于无类型常量规则要稍微灵活一些,它们可以在上下文里自动转换成相应的类型,详见我的另一篇文章golang中的无类型常量。
抛开常量和cgo,golang的类型转换可以分为好几类,我们先来看一些比较常见的类型。
数值类型之间互相转换
这是相当常见的转换。
这个其实没什么好说的,大家应该每天都会写类似的代码:
c := int(a+b)
d := float64(c)
数值类型之间可以相互转换,整数和浮点之间也会按照相应的规则进行转换。数值在必要的时候会发生回绕/截断。
这个转换相对来说也比较安全,唯一要注意的是溢出。
unsafe相关的转换
unsafe.Pointer
和所有的指针类型之间都可以互相转换,但从unsafe.Pointer
转换回来不保证类型安全。
unsafe.Pointer
和uintptr
之间也可以互相转换,后者主要是一些系统级api需要使用。
这些转换在go的runtime以及一些重度依赖系统编程的代码里经常出现。这些转换很危险,建议非必要不使用。
字符串到byte和rune切片的转换
这个转换的出现频率应该仅次于数值转换:
fmt.Println([]byte("hello"))
fmt.Println(string([]byte{104, 101, 108, 108, 111}))
这个转换go做了不少优化,所以有时候行为和普通的类型转换有点出入,比如很多时候数据复制会被优化掉。
rune就不举例了,代码上没有太大的差别。
slice转换成数组
go1.20之后允许slice转换成数组,在复制范围内的slice的元素会被复制:
s := []int{1,2,3,4,5}
a := [3]int(s)
a[2] = 100
fmt.Println(s) // [1 2 3 4 5]
fmt.Println(a) // [1 2 100]
如果数组的长度超过了slice的长度(注意不是cap),则会panic。转换成数组的指针也是可以的,规则完全相同。
底层类型相同时的转换
上面讨论的几种虽然很常见,但其实都可以算是特例。因为这些转换只限于特定的类型之间且编译器会识别这些转换并生成不同的代码。
但go其实还允许一类更宽泛的不需要那么多特殊处理的转换:底层类型相同的类型之间可以互相转换。
举个例子:
type A struct {
a int
b *string
c bool
}
type B struct {
a int
b *string
c bool
}
type B1 struct {
a1 int
b *string
c bool
}
type A1 B
type C int
type D int
A和B是完全不同的类型,但它们的底层类型都是struct{a int;b *string;c bool;}
。C和D也是完全不同的类型,但它们的底层类型都是int。A1派生自B,A1和B有着相同的底层类型,所有A1和A也有相同的底层类型。B1因为有个字段的名字和别人都不一样,所以没人和它的底层类型相同。
粗暴一点说,底层类型(underlying type)是各种内置类型(int,string,slice,map,...)以及struct{...}
(字段名和是否export会被考虑进去)。内置类型和struct{...}
的底层类型就是自己。
只要底层类型相同,类型之间就能互相转换:
func main() {
text := "hello"
a := A{1, &text, false}
a1 := A1(a)
fmt.Printf("%#v\n", a1) // main.A1{a:1, b:(*string)(0xc000014070), c:false}
}
A1和B还能算有点关系,但和A是真的八竿子打不着,我们的程序可以编译并且运行的很好。这就是底层类型相同的类型之间可以互相转换的规则导致的。
另外struct tag在转换中是会被忽略的,因此只要字段名字和类型相同,不管tag是不是相同的都可以进行转换。
这条规则允许了一些没有关系的类型进行双向的转换,咋一看好像这个规则是在乱来,但这玩意儿也不是完全没用:
type IP []byte
考虑这样一个类型,IP可以表示为一串byte的序列,这是RFC文档上明确说明的,所以我们这么定义合情合理(事实上大家也都是这么干的)。因为是byte的序列,所以我们自然会把一些处理byte切片的方法/函数用在IP上以实现代码复用和简化开发。
问题是这些代码都假定自己的参数/返回值是[]byte
而不是IP,我们知道IP其实就是[]byte
,但go不允许隐式类型转换,所以直接拿IP的值去掉这些函数是不行的。考虑一下如果没有底层类型相同的类型之间可以相互转换这个规则,我们要怎么复用这些函数呢,肯定只能走一些unsafe的歪门邪道了。与其这样不如允许[]byte(ip)
和IP(bytes)
的转换。
为啥不限制住只允许像IP
和[]byte
之间这样的转换呢?因为这样会导致类型检查变得复杂还要拖累编译速度,go最看重的就是编译器代码简单以及编译速度快,自然不愿意多检查这些东西,不如直接放开标准让底层类型相同类型的互相转换来的简单快捷。
但这个规则是很危险的,正是它导致了前面说的atomic.Pointer
的问题。
我们看下初版的atomic.Pointer
的代码:
type Pointer[T any] struct {
_ noCopy
v unsafe.Pointer
}
类型参数只是在Store
和Load
的时候用来进行unsafe.Pointer
到正常指针之间的类型转换的。这会导致一个致命缺陷:所有atomic.Pointer
都会有相同的底层类型struct{_ noCopy;v unsafe.Pointer;}
。
所以不管是atomic.Pointer[A]
,atomic.Pointer[B]
还是atomic.Pointer[small]
和atomic.Pointer[big]
,它们都有相同的底层类型,它们之间可以任意进行转换。
这下就彻底乱了套,虽说用户得自己为unsafe负责,但这种明摆着的甚至本来就不该编译通过的错误现在却可以在用户毫无防备的情况下出现在代码里——普通开发者可不会花时间关心标准库是怎么实现的所以不知道atomic.Pointer
和unsafe有什么关系。
go的开发者最后添加了_ [0]*T
,这样对于实例化的每一个atomic.Pointer
,只要T不同,它们的底层类型就会不同,上面的错误的类型转换就不可能发生。而且选用*T
还能防止自引用导致atomic.Pointer[atomic.Pointer[...]]
这样的代码编译报错。
现在你应该也能理解为什么我说泛型类型最容易遇见这种问题了:只要你的泛型类型是个结构体或者其他复合类型,但在字段或者复合类型中没有使用到泛型类型参数,那么从这个泛型类型实例化出来的所有类型就有可能有相同的底层类型,从而允许issue里描述的那种完全错误的类型转换出现。
别的语言里是个啥情况
对于结构化类型语言,像go这样底层类型相同就可以互相转换属于基操,不同语言会适当放宽/限制这种转换。说白了就是只认结构不认其他的,结构相同的东西你怎么折腾都算是同一类。因此issue描述的问题在这些语言里属于not even wrong这个级别,需要改变设计来回避类似的问题。
对于使用名义类型系统的语言,名字相同的算同一类不同的哪怕结构上一样也是不同类型。顺带一提,c++、golang、rust都属于这一类型。golang的底层类型虽然在类型转换和类型约束上表现得像结构化类型,但总体行为上仍然偏向于名义类型,官方并没有明确定义自己到底是哪种类型系统,所以权当是我的一家之言也行。
完全的结构化类型语言不怎么多见,我们就以常见的名义类型语言c++和使用鸭子类型的python为例。
在python中我们可以自定义类型的构造函数,因此可以在构造函数中实现类型转换的逻辑,如果我们没有自定义构造函数或者其他的可以返回新类型的类方法,那两个类型之间默认是无法进行转换。所以在python中是不会出现和go一样的问题的。
c++和python类似,用户不自定义的话默认不会存在任何转换途径。和python不一样的地方在于c++除了构造函数之外还有转换运算符并且支持在规则限制下的隐式转换。用户需要自己定义转换构造函数/转换运算符并且在语法规则的限制下才能实现两个不同类型间的转换,这个转换是单向还是双向和python一样由用户自己控制。所以c++中也不存在go的问题。
还有rust、Java、...我就不一一列举了。
总而言之这也是go大道至简的一个侧面——创造一些别的语言里很难出现的问题然后用简洁的手段去修复。
总结
我们复习了go里的类型转换,还顺便踩了一个相关的坑。
在这里给几个建议:
- 想用泛型又不想踩坑:尽量在结构体字段或者复合类型里使用泛型类型参数,使用
_ [0]*T
这样的字段不仅使代码难以理解,还会让类型的初始化变麻烦,不到atomic.Pointer
这样万不得以的时候我并不推荐使用。 - 不用泛型但害怕别的类型和自己的类型有相同的底层类型:不用怕,在自定义类型上少用类型转换的语法就行了,如果你真的需要在相关自定义类型之间转换,定义一些
toTypeA
之类的方法,这样转换过程就是你控制的不再是go默认的了。 - 在内置类型和基于这些类型的自定义类型之间转换:这个没啥好担心的,因为本就是你就是我我就是你的关系。实在觉得不舒服可以不用
type T []int
,把类型定义换成type T struct { data []int }
,代价除了代码变啰嗦外还有很多接受切片参数的函数和range循环没法直接用了。
像go这样在简单的语法规则里暗藏杀机的语言还是挺有意思的,如果只想着速成的话指不定什么时候就踩到地雷了。
golang的类型转换的更多相关文章
- Golang的类型转换实战案例
Golang的类型转换实战案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.数据类型概述 基础数据类型概述,博主推荐阅读: 布尔型: https://www.cnblogs. ...
- 8. golang 基本类型转换
golang 类型转换只能显性转换 不能自动转换 基本数据类型间的转换 var x1 int = 2 var x2 int16 var x3 int8 x2 = 200 + x1 x3 = 200 + ...
- golang数据类型转换
int--string //string到int value_int,err:=strconv.Atoi(string) //int到string str:=strconv.Itoa(value_in ...
- golang强制类型转换
github.com/Unknwon/com包的使用 package main import ( "fmt" "github.com/Unknwon/com" ...
- Golang处理数据库的nil数据
在用golang获取数据库的数据的时候,难免会遇到可控field.这个时候拿到的数据如果直接用string, time.Time这样的类型来解析的话会遇到panic. 那么如何处理这个问题呢,第一个出 ...
- Go语言的类型转换和类型断言
https://my.oschina.net/chai2010/blog/161418 https://studygolang.com/articles/9335 类型转换.类型断言和类型切换 ht ...
- golang学习笔记13 Golang 类型转换整理 go语言string、int、int64、float64、complex 互相转换
golang学习笔记13 Golang 类型转换整理 go语言string.int.int64.float64.complex 互相转换 #string到intint,err:=strconv.Ato ...
- Go语言的基本类型转换-golang类型转换
简单整理一下Golang的基本类型转换,作为备忘: 1.整形到字符串: var i int = 1 var s string 3. s = strconv.Itoa(i) 或者 s = Format ...
- Golang类型转换
类型转换 1.int转string strconv.Itoa(i) 2.string转int i, err = strconv.Atoi(s) 或者 i, err = ParseInt(s, 10, ...
- Golang 类型转换,断言和显式强制转换
1 前言 类型转换,可以用断言(只能使用在interface{}类型转换成其它类型)和显式类型强制转换(常规是用于基本类型) 2 代码 //graphql-go func(params graphql ...
随机推荐
- 最佳 AI 翻译工作流:全世界最信达雅的翻译
吴恩达老师提出了一种反思翻译的大语言模型 (LLM) AI 翻译工作流程--GitHub - andrewyng/translation-agent,具体工作流程如下: 提示一个 LLM 将文本从 s ...
- 【ue源码】定制-蓝图部分
今天在更新UE的伤害系统的时候出现了一个问题: 多个地方频繁调用一个函数,而这个函数肯定做优化,具体就是,把对应数据放入到队列,另外一个地方慢慢消费这个队列, 那么问题出现了,我使用的UE只有Sing ...
- AI知识库这事儿FastGPT是专业的
在搭建AI知识库这事儿上,有不少成熟的框架,我推荐使用FastGPT.这篇文章笔者就使用过的两款平台做个比较,FastGPT和百度千帆平台. 1. 为什么要搭建知识库 随着企业的运营,企业的私有数据越 ...
- docker基础学习总结
docker是一个快速安装部署的容器,快捷简单.可以隔离是他的优点 docker也拥有仓库:dockerhub,存储和管理镜像的平台 我们利用docker安装时就是在里面下载镜像,镜像不仅包含应用本身 ...
- M1安装Anaconda遇到的问题
1. 安装时报错:"Anaconda3 is already installed in /opt/anaconda3. Use 'conda update anaconda3' to upd ...
- 【Vue】NPM构建的一些问题解决
9418端口已经不再支持未授权的GIT协议 C:\Users\Administrator\Desktop\wss-taskcore-web>npm install npm ERR! Error ...
- 【Vue】Re08 模块化
一.JS的演变: 1.简单的页面控制,改变HTML标签和CSS样式 2.AJAX异步请求,控制前后数据传输问题 3.更强大的功能,几乎依赖JS实现 工程管理问题: 简单页面只需要在页面嵌入script ...
- Attempting to use uninitialized value beta2_power -------TensorFlow报错
版本: Python=3.7 TensorFlow=1.14 具体代码: init=[tf.global_variables_initializer(), tf.local_variables_ini ...
- 基于SiliconCloud快速体验GraphRag.Net
SiliconCloud介绍 SiliconCloud 基于优秀的开源基础模型,提供高性价比的 GenAI 服务. 不同于多数大模型云服务平台只提供自家大模型 API,SiliconCloud上架了包 ...
- Binance 如何使用 Quickwit 构建 100PB 日志服务(Quickwit 博客)
三年前,我们开源了 Quickwit,一个面向大规模数据集的分布式搜索引擎.我们的目标很宏大:创建一种全新的全文搜索引擎,其成本效率比 Elasticsearch 高十倍,配置和管理显著更简单,并且能 ...