網(wǎng)站首頁 編程語言 正文
1.前言
開發(fā)中,[]byte類型和string類型需要互相轉換的場景并不少見,直接的想法是像下面這樣進行強制類型轉換:
a := "Kylin Lab" b := []byte(a) fmt.Println(a)//Kylin Lab fmt.Println(b)//[75 121 108 105 110 32 76 97 98]
如果接下來需要對b進行修改,那么這樣轉換就沒什么問題,但是如果只是因為類型不合適,并不需要對轉換后的變量做任何修改,那這樣轉換就顯得不劃算了。我們知道,[]byte和string的內(nèi)存布局如下圖所示:
可以看到它們都有一個底層數(shù)組來存儲變量數(shù)據(jù),而類型本身只記錄這個數(shù)組的起始地址。如果采用強制類型轉換的方式把a轉換為b,那么就會重新分配b使用的底層數(shù)組。然后把a的底層數(shù)組內(nèi)容拷貝到b的底層數(shù)組。如果字符串內(nèi)容很多,多占用這許多字節(jié)的內(nèi)存不說,還要耗費時間做拷貝,所以就顯得很不合適了。
要是可以讓b重復使用a的底層數(shù)組,那就好了。強轉不行,就到了unsafe上場的時候了~
2.指針類型轉換
unsafe提供的第一件法寶就是指針類型轉換。我們知道像下面這樣的指針類型轉換是編譯不通過的。
a := "Kylin Lab" var b []byte tmp := (*string)(&b) //cannot convert &b (type *[]byte) to type *string
但是你可以把任意一個指針類型轉換為unsafe.Pointer類型,再把unsafe.Pointer類型轉換為任意指針類型,就像下面這樣是可以正常執(zhí)行的:
tmp := (*string)(unsafe.Pointer(&b))
現(xiàn)在我們通過unsafe.Pointer把b的指針轉換為*string類型,我們可以放心的這樣做,是因為我們知道slice的底層布局與string是兼容的,b的前兩項內(nèi)容與a相同,都是一個uintptr和一個int。可參見reflect包中關于這兩個類型的定義:
//reflect/value.go type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int }
我們知道上面這個例子中 變量b只初始化了變量結構,并未初始化底層數(shù)組,元素個數(shù)和容量都為0。
接下來,我們把a賦值給tmp:
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a fmt.Println(a) //Kylin Lab fmt.Println(b) //[75 121 108 105 110 32 76 97 98] fmt.Println(*tmp) //Kylin Lab fmt.Println(tmp) //0xc000004078 fmt.Printf("%p\n", &a) //0xc00005a250 fmt.Printf("%p\n", &b) //0xc000004078 fmt.Println(&a) //0xc00005a250 fmt.Println(&b)//&[75 121 108 105 110 32 76 97 98]
現(xiàn)在你猜怎么著,我們已經(jīng)在變量b中重復使用了a的底層數(shù)組,元素個數(shù)也填好了~
不過還沒完,b的容量還為0呢!怎么修改它呢?我們能拿到b的地址,也知道data和len各占8字節(jié)(64位下),只要把b的指針加上16字節(jié)就是cap的起始地址。可問題是Go語言的指針支持做加減運算嗎?不支持!
這時候就要拿出unsafe提供的第二件法寶了!
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //0
//unsafe/unsafe.go package unsafe type ArbitraryType int type IntegerType int//引用不會出錯 type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr func Add(ptr Pointer, len IntegerType) Pointer func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
//builtin/builtin.go // uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr uintptr // IntegerType is here for the purposes of documentation only. It is a stand-in // for any integer type: int, uint, int8 etc. type IntegerType int//引用會出錯
3.指針運算
Go語言不支持指針直接進行運算,也是為了保障程序運行安全,防止出現(xiàn)莫名其妙的、玄之又玄的bug。
不過unsafe.Pointer可以和各種指針類型相互轉換,也可以轉換為uintptr類型,uintptr本質(zhì)上就是一個無符號整型,所以它是可以進行運算的。 繼續(xù)上面的例子,我們可以把b的指針轉換為unsafe.Pointer,再進一步轉換為uintptr。
(uintptr)(unsafe.Pointer(&b))
現(xiàn)在就把b的地址轉換為uintptr類型了,64位下,如果把它加上16,就是b的容量的起始地址了。
(uintptr)(unsafe.Pointer(&b)) + 16
即便如此,我們也不能直接通過uintptr來修改b的容量,因為它不是指針類型,而且也不能直接轉換為指針類型。但是可以通過unsafe.Pointer類型中轉一下。
tmp2 := (*int)(unsafe.Pointer((uintptr)(unsafe.Pointer(&b)) + 16))
現(xiàn)在才算是拿到了b的容量的指針,再通過這個*int修改b的容量就OK了~
*tmp2 = len(b)
目前為止,我們已經(jīng)借助unsafe的兩個法寶,成功完成了string到[]byte的轉換,并且復用了a的底層數(shù)組。
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a tmp2 := (*int)(unsafe.Pointer((uintptr)(unsafe.Pointer(&b)) + 16)) *tmp2 = len(b) fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //9
上面tmp2賦值這一行很長,也很繞。
注:雖然下面可以編譯過,但是一定不要像下面這樣先使用uintptr類型的臨時變量來存儲一個地址,然后才把它轉換為某個指針類型。
tmp2 := (uintptr)(unsafe.Pointer(&b)) + 16 capPtr := (*int)(unsafe.Pointer(tmp2))
這是因為uintptr只是一個存儲著地址的無符號整型而已,它不是指針,如果垃圾回收為了減少內(nèi)存碎片而移動了一些變量,內(nèi)存關聯(lián)到的指針類型的值是會一并修改的,但是uintptr并不會,這就可能出現(xiàn)一些神奇的bug,所以這一行只能這么繞著寫。
除此之外,這個硬編碼的“16”怎么看都顯得格外不和諧。有沒有什么好方法,可以獲取程序運行平臺中一個類型的大小呢?這就要用到unsafe提供的第三個法寶了~
4.獲取大小和偏移
unsafe.Sizeof可以拿到任意類型的大小,unsafe.Alignof可以拿到任意類型的對齊邊界。按照reflect.SliceHeader的定義,我們這里可以用unsafe.Sizeof來獲取uintptr和int的大小,b的起始地址偏移這么多就是第三個字段Cap的地址了。
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a tmp2 := (*int)(unsafe.Pointer((uintptr)(unsafe.Pointer(&b)) + unsafe.Sizeof(uintptr(1)) + unsafe.Sizeof(1))) *tmp2 = len(b) fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //9
不過這樣還是存在投機的成分,別忘了內(nèi)存對齊哦~
這里這樣寫可行,是因為我們知道uintptr和int的大小不是4字節(jié)就是8字節(jié),無論哪一種,都會緊挨著第三個字段,不會出現(xiàn)因內(nèi)存對齊而形成的間隙。
所以unsafe還有一個unsafe.Offsetof方法可以獲得結構體中某個字段距離結構體起始地址的偏移值,這樣就可以確定結構體成員正確的位置了。
為了試試這個方法,我們要把b的指針轉換為reflect.SliceHeader類型,其實也可以自己定義一個SliceHeader類型,但這不是有現(xiàn)成的可以直接拿來用嘛~
bPtr := (*reflect.SliceHeader)(unsafe.Pointer(&b))
然后獲取Cap字段在結構體內(nèi)的偏移值:
unsafe.Offsetof(bPtr.Cap)
再然后,就是把這個字段的地址轉換為*int,然后修改它的值了:
a := "Kylin Lab" var b []byte tmp := (*string)(unsafe.Pointer(&b)) *tmp = a bPtr := (*reflect.SliceHeader)(unsafe.Pointer(&b)) tmp2 := (*int)(unsafe.Pointer((uintptr)(unsafe.Pointer(&b)) + unsafe.Offsetof(bPtr.Cap))) *tmp2 = len(b) fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //9
我們?yōu)榱硕嘟榻B一些unsafe的功能,刻意繞了個遠~
其實都把b轉換為reflect.SliceHeader結構體了,改個字段值哪里要這么麻煩!!!我們大可以這樣做:
strHeader := (*reflect.StringHeader)(unsafe.Pointer(&a)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
這樣通過strHeader和sliceHeader想操作哪個字段都很方便。
a := "Kylin Lab" var b []byte strHeader := (*reflect.StringHeader)(unsafe.Pointer(&a)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sliceHeader.Data = strHeader.Data sliceHeader.Len = strHeader.Len sliceHeader.Cap = strHeader.Len fmt.Println(len(a)) //9 fmt.Println(len(b)) //9 fmt.Println(cap(b)) //9
5.關于string
關于string,我們還要啰嗦一點,Go語言中string變量的內(nèi)容默認是不會被修改的,而我們通過給string變量整體賦新值的方式來改變它的內(nèi)容時,實際上會重新分配它的底層數(shù)組。
而string類型字面量的底層數(shù)組會被分配到只讀數(shù)據(jù)段,在我們的例子中,b復用了a的底層數(shù)組,所以就不能再像下面這樣修改b的內(nèi)容了,否則執(zhí)行階段會發(fā)生錯誤。
a := "Kylin Lab" var b []byte strHeader := (*reflect.StringHeader)(unsafe.Pointer(&a)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sliceHeader.Data = strHeader.Data sliceHeader.Len = strHeader.Len sliceHeader.Cap = strHeader.Len b[0] = 'k' /*運行報錯: unexpected fault address 0x6d1875 fatal error: fault [signal 0xc0000005 code=0x1 addr=0x6d1875 pc=0x6c013a]*/
而運行時動態(tài)拼接而成的string變量,它的底層數(shù)組不在只讀數(shù)據(jù)段,而是由Go語言在語法層面阻止對字符串內(nèi)容的修改行為。
a := "Kylin Lab" //string字面量 c := "Hello " + a //動態(tài)拼接的字符串 c[0] = 'h' // cannot assign to c[0] 編譯時報錯
a := "Kylin Lab" //string字面量 a[0] = 'h' // cannot assign to c[0] 編譯時報錯
若我們利用unsafe讓一個[]byte復用這個字符串c的底層數(shù)組,就可以繞過Go語法層面的限制,修改底層數(shù)組的內(nèi)容了。
但是盡量不要這樣做,如果不確定這個字符串會在哪里用到的話~
a := "Kylin Lab" c := "Hello" + a var s []byte strHeader := (*reflect.StringHeader)(unsafe.Pointer(&c)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&s)) sliceHeader.Data = strHeader.Data sliceHeader.Len = strHeader.Len sliceHeader.Cap = strHeader.Len s[0] = 'h' fmt.Println(c) //hello Kylin Lab fmt.Println(a) //Kylin Lab fmt.Println(string(s)) //hello Kylin Lab
原文鏈接:https://blog.csdn.net/qq_53267860/article/details/126829492
相關推薦
- 2022-11-06 Swift使用SnapKit模仿Kingfisher第三方擴展優(yōu)化_Swift
- 2022-04-18 Android?app本地切換logo和名稱_Android
- 2022-06-21 C語言實現(xiàn)順序表的全操作詳解_C 語言
- 2022-06-12 Docker容器中數(shù)據(jù)卷volumes的使用_docker
- 2023-05-06 Python執(zhí)行ping操作的簡單方法_python
- 2022-05-20 Spring注入bean的常用的六種方式
- 2023-01-09 基于Python實現(xiàn)拉格朗日插值法_python
- 2023-05-22 python使用ctypes調(diào)用第三方庫時出現(xiàn)undefined?symbol錯誤詳解_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結構-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支