您的位置:首頁(yè) > 軟件教程 > 教程 > golang的類型轉(zhuǎn)換

golang的類型轉(zhuǎn)換

來(lái)源:好特整理 | 時(shí)間:2024-09-30 10:04:56 | 閱讀:82 |  標(biāo)簽: a Golang GO   | 分享到:

今天我們來(lái)說(shuō)說(shuō)一個(gè)大家每天都在做但很少深入思考的操作——類型轉(zhuǎn)換。 本文索引 一行奇怪的代碼 go的類型轉(zhuǎn)換 數(shù)值類型之間互相轉(zhuǎn)換 unsafe相關(guān)的轉(zhuǎn)換 字符串到byte和rune切片的轉(zhuǎn)換 slice轉(zhuǎn)換成數(shù)組 底層類型相同時(shí)的轉(zhuǎn)換 別的語(yǔ)言里是個(gè)啥情況 總結(jié) 一行奇怪的代碼 事情始于年初時(shí)我對(duì)

今天我們來(lái)說(shuō)說(shuō)一個(gè)大家每天都在做但很少深入思考的操作——類型轉(zhuǎn)換。

本文索引

  • 一行奇怪的代碼
  • go的類型轉(zhuǎn)換
    • 數(shù)值類型之間互相轉(zhuǎn)換
    • unsafe相關(guān)的轉(zhuǎn)換
    • 字符串到byte和rune切片的轉(zhuǎn)換
    • slice轉(zhuǎn)換成數(shù)組
    • 底層類型相同時(shí)的轉(zhuǎn)換
  • 別的語(yǔ)言里是個(gè)啥情況
  • 總結(jié)

一行奇怪的代碼

事情始于年初時(shí)我對(duì)標(biāo)準(zhǔn)庫(kù)sync做一些改動(dòng)的時(shí)候。

改動(dòng)會(huì)用到標(biāo)準(zhǔn)庫(kù)在1.19新添加的 atomic.Pointer ,出于謹(jǐn)慎,我在進(jìn)行變更之前泛泛通讀了一遍它的代碼,然而一行代碼引起了我的注意:

// 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,這個(gè)我在 golang拾遺:實(shí)現(xiàn)一個(gè)不可復(fù)制類型 詳細(xì)講解過(guò)。

引起我注意的地方是 _ [0]*T ,它是個(gè)匿名字段,且長(zhǎng)度為零的數(shù)組不會(huì)占用內(nèi)存。這并不影響我要修改的代碼,但它的作用是什么引起了我的好奇。

還好這個(gè)字段自己的注釋給出了答案:這個(gè)字段是為了防止錯(cuò)誤的類型轉(zhuǎn)換。什么樣的類型轉(zhuǎn)換需要加這個(gè)字段來(lái)封鎖呢。帶著疑問(wèn)我點(diǎn)開(kāi)了給出的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
	}
}

例子程序會(huì)導(dǎo)致內(nèi)存錯(cuò)誤,在Linux環(huán)境上它會(huì)有很大概率導(dǎo)致段錯(cuò)誤。為什么呢?因?yàn)閎ig的索引值大大超過(guò)了small的范圍,而我們實(shí)際上在Pointer只存了一個(gè)small對(duì)象,所以在最后的循環(huán)那里我們發(fā)生了索引越界,而且go并沒(méi)有檢測(cè)到這個(gè)越界。

當(dāng)然,go也沒(méi)有義務(wù)去檢測(cè)這種越界,因?yàn)橛昧藆nsafe(atomic.Pointer是對(duì)unsafe.Pointer的包裝)之后類型安全和內(nèi)存安全就只能靠用戶自己來(lái)負(fù)責(zé)了。

這里根本上的問(wèn)題在于, atomic.Pointer[small] atomic.Pointer[big] 之間沒(méi)有任何關(guān)聯(lián),它們應(yīng)該是完全不同的類型不應(yīng)該發(fā)生轉(zhuǎn)換(如果對(duì)此有疑惑,可以搜索下類型構(gòu)造器相關(guān)的資料,通常這種泛型的類型構(gòu)造器產(chǎn)生的類型之間是不應(yīng)該有任何關(guān)聯(lián)性的),尤其是go是一門強(qiáng)類型語(yǔ)言,類似的事情在c++無(wú)法通過(guò)編譯而在python里則會(huì)運(yùn)行時(shí)報(bào)錯(cuò)。

但事實(shí)是在沒(méi)添加開(kāi)頭的那個(gè)字段前這種轉(zhuǎn)換是合法的而且在泛型類型中很容易出現(xiàn)。

到這里你可能還是有點(diǎn)云里霧里,不過(guò)沒(méi)關(guān)系,看完下一節(jié)你會(huì)云開(kāi)霧散的。

go的類型轉(zhuǎn)換

golang里不存在隱式類型轉(zhuǎn)換,因此想要將一個(gè)類型的值轉(zhuǎn)換成另一個(gè)類型,只能用這樣的表達(dá)式 Type(value) 。表達(dá)式會(huì)把value復(fù)制一份然后轉(zhuǎn)換成Type類型。

對(duì)于無(wú)類型常量規(guī)則要稍微靈活一些,它們可以在上下文里自動(dòng)轉(zhuǎn)換成相應(yīng)的類型,詳見(jiàn)我的另一篇文章 golang中的無(wú)類型常量 。

拋開(kāi)常量和cgo,golang的類型轉(zhuǎn)換可以分為好幾類,我們先來(lái)看一些比較常見(jiàn)的類型。

數(shù)值類型之間互相轉(zhuǎn)換

這是相當(dāng)常見(jiàn)的轉(zhuǎn)換。

這個(gè)其實(shí)沒(méi)什么好說(shuō)的,大家應(yīng)該每天都會(huì)寫(xiě)類似的代碼:

c := int(a+b)
d := float64(c)

數(shù)值類型之間可以相互轉(zhuǎn)換,整數(shù)和浮點(diǎn)之間也會(huì)按照相應(yīng)的規(guī)則進(jìn)行轉(zhuǎn)換。數(shù)值在必要的時(shí)候會(huì)發(fā)生回繞/截?cái)唷?

這個(gè)轉(zhuǎn)換相對(duì)來(lái)說(shuō)也比較安全,唯一要注意的是溢出。

unsafe相關(guān)的轉(zhuǎn)換

unsafe.Pointer 和所有的指針類型之間都可以互相轉(zhuǎn)換,但從 unsafe.Pointer 轉(zhuǎn)換回來(lái)不保證類型安全。

unsafe.Pointer uintptr 之間也可以互相轉(zhuǎn)換,后者主要是一些系統(tǒng)級(jí)api需要使用。

這些轉(zhuǎn)換在go的runtime以及一些重度依賴系統(tǒng)編程的代碼里經(jīng)常出現(xiàn)。這些轉(zhuǎn)換很危險(xiǎn),建議非必要不使用。

字符串到byte和rune切片的轉(zhuǎn)換

這個(gè)轉(zhuǎn)換的出現(xiàn)頻率應(yīng)該僅次于數(shù)值轉(zhuǎn)換:

fmt.Println([]byte("hello"))
fmt.Println(string([]byte{104, 101, 108, 108, 111}))

這個(gè)轉(zhuǎn)換go做了不少優(yōu)化,所以有時(shí)候行為和普通的類型轉(zhuǎn)換有點(diǎn)出入,比如很多時(shí)候數(shù)據(jù)復(fù)制會(huì)被優(yōu)化掉。

rune就不舉例了,代碼上沒(méi)有太大的差別。

slice轉(zhuǎn)換成數(shù)組

go1.20之后允許slice轉(zhuǎn)換成數(shù)組,在復(fù)制范圍內(nèi)的slice的元素會(huì)被復(fù)制:

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]

如果數(shù)組的長(zhǎng)度超過(guò)了slice的長(zhǎng)度(注意不是cap),則會(huì)panic。轉(zhuǎn)換成數(shù)組的指針也是可以的,規(guī)則完全相同。

底層類型相同時(shí)的轉(zhuǎn)換

上面討論的幾種雖然很常見(jiàn),但其實(shí)都可以算是特例。因?yàn)檫@些轉(zhuǎn)換只限于特定的類型之間且編譯器會(huì)識(shí)別這些轉(zhuǎn)換并生成不同的代碼。

但go其實(shí)還允許一類更寬泛的不需要那么多特殊處理的轉(zhuǎn)換:底層類型相同的類型之間可以互相轉(zhuǎn)換。

舉個(gè)例子:

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因?yàn)橛袀(gè)字段的名字和別人都不一樣,所以沒(méi)人和它的底層類型相同。

粗暴一點(diǎn)說(shuō),底層類型(underlying type)是各種內(nèi)置類型(int,string,slice,map,...)以及 struct{...} (字段名和是否export會(huì)被考慮進(jìn)去)。內(nèi)置類型和 struct{...} 的底層類型就是自己。

只要底層類型相同,類型之間就能互相轉(zhuǎn)換:

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還能算有點(diǎn)關(guān)系,但和A是真的八竿子打不著,我們的程序可以編譯并且運(yùn)行的很好。這就是底層類型相同的類型之間可以互相轉(zhuǎn)換的規(guī)則導(dǎo)致的。

另外struct tag在轉(zhuǎn)換中是會(huì)被忽略的,因此只要字段名字和類型相同,不管tag是不是相同的都可以進(jìn)行轉(zhuǎn)換。

這條規(guī)則允許了一些沒(méi)有關(guān)系的類型進(jìn)行雙向的轉(zhuǎn)換,咋一看好像這個(gè)規(guī)則是在亂來(lái),但這玩意兒也不是完全沒(méi)用:

type IP []byte

考慮這樣一個(gè)類型,IP可以表示為一串byte的序列,這是RFC文檔上明確說(shuō)明的,所以我們這么定義合情合理(事實(shí)上大家也都是這么干的)。因?yàn)槭莃yte的序列,所以我們自然會(huì)把一些處理byte切片的方法/函數(shù)用在IP上以實(shí)現(xiàn)代碼復(fù)用和簡(jiǎn)化開(kāi)發(fā)。

問(wèn)題是這些代碼都假定自己的參數(shù)/返回值是 []byte 而不是IP,我們知道IP其實(shí)就是 []byte ,但go不允許隱式類型轉(zhuǎn)換,所以直接拿IP的值去掉這些函數(shù)是不行的。考慮一下如果沒(méi)有底層類型相同的類型之間可以相互轉(zhuǎn)換這個(gè)規(guī)則,我們要怎么復(fù)用這些函數(shù)呢,肯定只能走一些unsafe的歪門邪道了。與其這樣不如允許 []byte(ip) IP(bytes) 的轉(zhuǎn)換。

為啥不限制住只允許像 IP []byte 之間這樣的轉(zhuǎn)換呢?因?yàn)檫@樣會(huì)導(dǎo)致類型檢查變得復(fù)雜還要拖累編譯速度,go最看重的就是編譯器代碼簡(jiǎn)單以及編譯速度快,自然不愿意多檢查這些東西,不如直接放開(kāi)標(biāo)準(zhǔn)讓底層類型相同類型的互相轉(zhuǎn)換來(lái)的簡(jiǎn)單快捷。

但這個(gè)規(guī)則是很危險(xiǎn)的,正是它導(dǎo)致了前面說(shuō)的 atomic.Pointer 的問(wèn)題。

我們看下初版的 atomic.Pointer 的代碼:

type Pointer[T any] struct {
    _ noCopy
    v unsafe.Pointer
}

類型參數(shù)只是在 Store Load 的時(shí)候用來(lái)進(jìn)行 unsafe.Pointer 到正常指針之間的類型轉(zhuǎn)換的。這會(huì)導(dǎo)致一個(gè)致命缺陷:所有 atomic.Pointer 都會(huì)有相同的底層類型 struct{_ noCopy;v unsafe.Pointer;} 。

所以不管是 atomic.Pointer[A] , atomic.Pointer[B] 還是 atomic.Pointer[small] atomic.Pointer[big] ,它們都有相同的底層類型,它們之間可以任意進(jìn)行轉(zhuǎn)換。

這下就徹底亂了套,雖說(shuō)用戶得自己為unsafe負(fù)責(zé),但這種明擺著的甚至本來(lái)就不該編譯通過(guò)的錯(cuò)誤現(xiàn)在卻可以在用戶毫無(wú)防備的情況下出現(xiàn)在代碼里——普通開(kāi)發(fā)者可不會(huì)花時(shí)間關(guān)心標(biāo)準(zhǔn)庫(kù)是怎么實(shí)現(xiàn)的所以不知道 atomic.Pointer 和unsafe有什么關(guān)系。

go的開(kāi)發(fā)者最后添加了 _ [0]*T ,這樣對(duì)于實(shí)例化的每一個(gè) atomic.Pointer ,只要T不同,它們的底層類型就會(huì)不同,上面的錯(cuò)誤的類型轉(zhuǎn)換就不可能發(fā)生。而且選用 *T 還能防止自引用導(dǎo)致 atomic.Pointer[atomic.Pointer[...]] 這樣的代碼編譯報(bào)錯(cuò)。

現(xiàn)在你應(yīng)該也能理解為什么我說(shuō)泛型類型最容易遇見(jiàn)這種問(wèn)題了:只要你的泛型類型是個(gè)結(jié)構(gòu)體或者其他復(fù)合類型,但在字段或者復(fù)合類型中沒(méi)有使用到泛型類型參數(shù),那么從這個(gè)泛型類型實(shí)例化出來(lái)的所有類型就有可能有相同的底層類型,從而允許issue里描述的那種完全錯(cuò)誤的類型轉(zhuǎn)換出現(xiàn)。

別的語(yǔ)言里是個(gè)啥情況

對(duì)于結(jié)構(gòu)化類型語(yǔ)言,像go這樣底層類型相同就可以互相轉(zhuǎn)換屬于基操,不同語(yǔ)言會(huì)適當(dāng)放寬/限制這種轉(zhuǎn)換。說(shuō)白了就是只認(rèn)結(jié)構(gòu)不認(rèn)其他的,結(jié)構(gòu)相同的東西你怎么折騰都算是同一類。因此issue描述的問(wèn)題在這些語(yǔ)言里屬于not even wrong這個(gè)級(jí)別,需要改變?cè)O(shè)計(jì)來(lái)回避類似的問(wèn)題。

對(duì)于使用名義類型系統(tǒng)的語(yǔ)言,名字相同的算同一類不同的哪怕結(jié)構(gòu)上一樣也是不同類型。順帶一提,c++、golang、rust都屬于這一類型。golang的底層類型雖然在類型轉(zhuǎn)換和類型約束上表現(xiàn)得像結(jié)構(gòu)化類型,但總體行為上仍然偏向于名義類型,官方并沒(méi)有明確定義自己到底是哪種類型系統(tǒng),所以權(quán)當(dāng)是我的一家之言也行。

完全的結(jié)構(gòu)化類型語(yǔ)言不怎么多見(jiàn),我們就以常見(jiàn)的名義類型語(yǔ)言c++和使用鴨子類型的python為例。

在python中我們可以自定義類型的構(gòu)造函數(shù),因此可以在構(gòu)造函數(shù)中實(shí)現(xiàn)類型轉(zhuǎn)換的邏輯,如果我們沒(méi)有自定義構(gòu)造函數(shù)或者其他的可以返回新類型的類方法,那兩個(gè)類型之間默認(rèn)是無(wú)法進(jìn)行轉(zhuǎn)換。所以在python中是不會(huì)出現(xiàn)和go一樣的問(wèn)題的。

c++和python類似,用戶不自定義的話默認(rèn)不會(huì)存在任何轉(zhuǎn)換途徑。和python不一樣的地方在于c++除了構(gòu)造函數(shù)之外還有轉(zhuǎn)換運(yùn)算符并且支持 在規(guī)則限制下的隱式轉(zhuǎn)換 。用戶需要自己定義轉(zhuǎn)換構(gòu)造函數(shù)/轉(zhuǎn)換運(yùn)算符并且在語(yǔ)法規(guī)則的限制下才能實(shí)現(xiàn)兩個(gè)不同類型間的轉(zhuǎn)換,這個(gè)轉(zhuǎn)換是單向還是雙向和python一樣由用戶自己控制。所以c++中也不存在go的問(wèn)題。

還有rust、Java、...我就不一一列舉了。

總而言之這也是go大道至簡(jiǎn)的一個(gè)側(cè)面——?jiǎng)?chuàng)造一些別的語(yǔ)言里很難出現(xiàn)的問(wèn)題然后用簡(jiǎn)潔的手段去修復(fù)。

總結(jié)

我們復(fù)習(xí)了go里的類型轉(zhuǎn)換,還順便踩了一個(gè)相關(guān)的坑。

在這里給幾個(gè)建議:

  • 想用泛型又不想踩坑:盡量在結(jié)構(gòu)體字段或者復(fù)合類型里使用泛型類型參數(shù),使用 _ [0]*T 這樣的字段不僅使代碼難以理解,還會(huì)讓類型的初始化變麻煩,不到 atomic.Pointer 這樣萬(wàn)不得以的時(shí)候我并不推薦使用。
  • 不用泛型但害怕別的類型和自己的類型有相同的底層類型:不用怕,在自定義類型上少用類型轉(zhuǎn)換的語(yǔ)法就行了,如果你真的需要在相關(guān)自定義類型之間轉(zhuǎn)換,定義一些 toTypeA 之類的方法,這樣轉(zhuǎn)換過(guò)程就是你控制的不再是go默認(rèn)的了。
  • 在內(nèi)置類型和基于這些類型的自定義類型之間轉(zhuǎn)換:這個(gè)沒(méi)啥好擔(dān)心的,因?yàn)楸揪褪悄憔褪俏椅揖褪悄愕年P(guān)系。實(shí)在覺(jué)得不舒服可以不用 type T []int ,把類型定義換成 type T struct { data []int } ,代價(jià)除了代碼變啰嗦外還有很多接受切片參數(shù)的函數(shù)和range循環(huán)沒(méi)法直接用了。

像go這樣在簡(jiǎn)單的語(yǔ)法規(guī)則里暗藏殺機(jī)的語(yǔ)言還是挺有意思的,如果只想著速成的話指不定什么時(shí)候就踩到地雷了。

小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認(rèn)同期限觀點(diǎn)或證實(shí)其描述。

a 1.0
a 1.0
類型:休閑益智  運(yùn)營(yíng)狀態(tài):正式運(yùn)營(yíng)  語(yǔ)言:中文   

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動(dòng)

《alittletotheleft》官網(wǎng)正版是一款備受歡迎的休閑益智整理游戲。玩家的任務(wù)是對(duì)日常生活中的各種雜亂物
Go v1.62
Go v1.62
類型:動(dòng)作冒險(xiǎn)  運(yùn)營(yíng)狀態(tài):正式運(yùn)營(yíng)  語(yǔ)言:中文   

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動(dòng)

GoEscape是一款迷宮逃脫休閑闖關(guān)游戲。在這款游戲中,玩家可以挑戰(zhàn)大量關(guān)卡,通過(guò)旋轉(zhuǎn)屏幕的方式幫助球球

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機(jī)版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號(hào)!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請(qǐng)發(fā)郵件[email protected]

湘ICP備2022002427號(hào)-10 湘公網(wǎng)安備:43070202000427號(hào)© 2013~2024 haote.com 好特網(wǎng)