ToB企服应用市场:ToB评测及商务社交产业平台

标题: golang的类型转换 [打印本页]

作者: 老婆出轨    时间: 2024-9-30 06:40
标题: golang的类型转换
今天我们来说说一个大家天天都在做但很少深入思考的操作——类型转换。
  本文索引

  
一行奇怪的代码

事情始于年初时我对尺度库sync做一些改动的时候。
改动会用到尺度库在1.19新添加的atomic.Pointer,出于谨慎,我在举行变更之前泛泛通读了一遍它的代码,然而一行代码引起了我的注意:
  1. // A Pointer is an atomic pointer of type *T. The zero value is a nil *T.
  2. type Pointer[T any] struct {
  3.     // Mention *T in a field to disallow conversion between Pointer types.
  4.     // See go.dev/issue/56603 for more details.
  5.     // Use *T, not T, to avoid spurious recursive type definition errors.
  6.     _ [0]*T
  7.     _ noCopy
  8.     v unsafe.Pointer
  9. }
复制代码
并不是noCopy,这个我在golang拾遗:实现一个不可复制类型具体讲解过。
引起我注意的地方是_ [0]*T,它是个匿名字段,且长度为零的数组不会占用内存。这并不影响我要修改的代码,但它的作用是什么引起了我的好奇。
还好这个字段自己的解释给出了答案:这个字段是为了防止错误的类型转换。什么样的类型转换需要加这个字段来封锁呢。带着疑问我点开了给出的issue链接,然后看到了下面的例子:
  1. package main
  2. import (
  3.         "math"
  4.         "sync/atomic"
  5. )
  6. type small struct {
  7.         small [64]byte
  8. }
  9. type big struct {
  10.         big [math.MaxUint16 * 10]byte
  11. }
  12. func main() {
  13.         a := atomic.Pointer[small]{}
  14.         a.Store(&small{})
  15.         b := atomic.Pointer[big](a) // type conversion
  16.         big := b.Load()
  17.         for i := range big.big {
  18.                 big.big[i] = 1
  19.         }
  20. }
复制代码
例子程序会导致内存错误,在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的类型转换可以分为好几类,我们先来看一些比较常见的类型。
数值类型之间互相转换

这是相当常见的转换。
这个实在没什么好说的,大家应该天天都会写类似的代码:
  1. c := int(a+b)
  2. d := float64(c)
复制代码
数值类型之间可以相互转换,整数和浮点之间也会按照相应的规则举行转换。数值在必要的时候会发生回绕/截断。
这个转换相对来说也比较安全,唯一要注意的是溢出。
unsafe相关的转换

unsafe.Pointer和所有的指针类型之间都可以互相转换,但从unsafe.Pointer转换返来不保证类型安全。
unsafe.Pointer和uintptr之间也可以互相转换,后者主要是一些体系级api需要利用。
这些转换在go的runtime以及一些重度依赖体系编程的代码里经常出现。这些转换很伤害,建议非必要不利用。
字符串到byte和rune切片的转换

这个转换的出现频率应该仅次于数值转换:
  1. fmt.Println([]byte("hello"))
  2. fmt.Println(string([]byte{104, 101, 108, 108, 111}))
复制代码
这个转换go做了不少优化,所以有时候举动和普通的类型转换有点出入,比如许多时候数据复制会被优化掉。
rune就不举例了,代码上没有太大的差别。
slice转换成数组

go1.20之后允许slice转换成数组,在复制范围内的slice的元素会被复制:
  1. s := []int{1,2,3,4,5}
  2. a := [3]int(s)
  3. a[2] = 100
  4. fmt.Println(s)  // [1 2 3 4 5]
  5. fmt.Println(a)  // [1 2 100]
复制代码
如果数组的长度凌驾了slice的长度(注意不是cap),则会panic。转换成数组的指针也是可以的,规则完全雷同。
底层类型雷同时的转换

上面讨论的几种虽然很常见,但实在都可以算是特例。因为这些转换只限于特定的类型之间且编译器会识别这些转换并生成差别的代码。
但go实在还允许一类更宽泛的不需要那么多特殊处理的转换:底层类型雷同的类型之间可以互相转换。
举个例子:
  1. type A struct {
  2.     a int
  3.     b *string
  4.     c bool
  5. }
  6. type B struct {
  7.     a int
  8.     b *string
  9.     c bool
  10. }
  11. type B1 struct {
  12.     a1 int
  13.     b *string
  14.     c bool
  15. }
  16. type A1 B
  17. type C int
  18. 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{...}的底层类型就是自己。
只要底层类型雷同,类型之间就能互相转换:
  1. func main() {
  2.     text := "hello"
  3.     a := A{1, &text, false}
  4.     a1 := A1(a)
  5.     fmt.Printf("%#v\n", a1) // main.A1{a:1, b:(*string)(0xc000014070), c:false}
  6. }
复制代码
A1和B还能算有点关系,但和A是真的八竿子打不着,我们的程序可以编译而且运行的很好。这就是底层类型雷同的类型之间可以互相转换的规则导致的。
别的struct tag在转换中是会被忽略的,因此只要字段名字和类型雷同,不管tag是不是雷同的都可以举行转换。
这条规则允许了一些没有关系的类型举行双向的转换,咋一看似乎这个规则是在瞎搅,但这玩意儿也不是完全没用:
  1. 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的代码:
  1. type Pointer[T any] struct {
  2.     _ noCopy
  3.     v unsafe.Pointer
  4. }
复制代码
类型参数只是在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里的类型转换,还顺便踩了一个相关的坑。
在这里给几个建议:
像go这样在简朴的语法规则里暗藏杀机的语言还是挺有意思的,如果只想着速成的话指不定什么时候就踩到地雷了。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4