Go語言中字符串賦值中的問題與解決方法
字符串的拼接方式
使用 +
號
使用 +
號拼接字符串的方式,每次拼接都會創(chuàng)建一個新的字符串,然后將原來的字符串復制到新的字符串中,這樣會導致大量的內存分配和復制操作,性能較差。
字符串格式化函數(shù) fmt.Sprintf
函數(shù)
預分配 bytes.Buffer
緩沖區(qū)
func BufferCapResize() { var str = "abcd" var buf bytes.Buffer // 預分配內存 buf.Grow(4 * 10000) // 如果沒有這一行,當長度不夠了就會擴容 cap := 0 for i := 0; i < 10000; i++ { if buf.Cap() != cap { println("cap:", buf.Cap()) cap = buf.Cap() } buf.WriteString(str) } }
- 預分配
strings.Builder
構建器 - 預分配
[]byte
- 使用
strings.Join
函數(shù)
strings.Builder
和 bytes.Buffer
底層都是一個字節(jié)數(shù)組,但是 bytes.Buffer
在轉換字符串的時候,需要重新申請內存空間,而strings.Builder
是直接將底層的 bytes
轉換成字符串進行返回
string(b.buf[b.off:])
直接強轉unsafe.String(unsafe.Slice(b.buf), len(b.buf))
零拷貝轉換
字符串內存泄露
對字符進行截取時指向同一塊內存空間
如何避免:
- 將子字符串轉換成字節(jié)切片,在轉成
string
- 截取后再前面拼接一個新字符串
- 使用
strings.Builder
對新字符串進行重新構造
定義一個很長的字符串 s := strings.Repeat("a", 1<<20)
,
賦值
①
②
④
打印的是同一個地址 ③
打印的是一個 nil
字符串在賦值的時候不會發(fā)生拷貝,只是改變底層的指針指向
原始的字符串 s
即使被重新賦值為空字符串,但是 s2
依然指向原來的字符串,所以原始的地址不會被釋放
func main() { ptr := (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0xc000180000 Assign() } func Assign() { s2 := s ptr := (*reflect.StringHeader)(unsafe.Pointer(&s2)) fmt.Println("Assign:", unsafe.Pointer(ptr.Data)) // ② 0xc000180000 s := "" ptr = (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Println("s pointer", unsafe.Pointer(ptr.Data)) // ③ nil ptr = (*reflect.StringHeader)(unsafe.Pointer(&s2)) fmt.Println("Assign", unsafe.Pointer(ptr.Data)) // ④ 0xc000180000 _ = s2 }
通過引用賦值
不管是通過引用賦值,還是值賦值,最終都是指向同一個地址
func main() { ptr := (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0xc000180000 AssignPointer() } func AssignPointer() { s2 := &s // 通過引用賦值 ptr := (*reflect.StringHeader)(unsafe.Pointer(s2)) fmt.Println("AssignPointer:", unsafe.Pointer(ptr.Data)) // ② 0xc000180000 _ = s2 }
字符串截取
s2
是截取 s
字符串的前 20
位,這樣 s2
和 s
的起始地址是一樣的
這種解決很容易導致內存泄露,因為字符串 s
申請的空間是非常大的,s
在不使用的情況下,也是不會被回收的,因為 s2
指向了 s
的地址
func main() { ptr := (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0xc000100000 StringSlice() } func StringSlice() { s2 := s[:20] ptr := (*reflect.StringHeader)(unsafe.Pointer(&s2)) fmt.Println("StringSlice:", unsafe.Pointer(ptr.Data)) // ② 0xc000100000 _ = s2 }
字符串傳遞
字符串傳遞到函數(shù)內部,不管是指針傳遞還是值傳遞,字符串實際內容在內存中的地址是相同的
func main() { ptr := (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0x49b7ba f1(s) f2(&s) } func f1(s string) string { ptr := unsafe.StringData(s) fmt.Println("f1:", ptr) // ② 0x49b7ba return s } func f2(s *string) *string { ptr := unsafe.StringData(*s) fmt.Println("f2:", ptr) // ③ 0x49b7ba return s }
你可能會發(fā)現(xiàn),網(wǎng)上說傳遞指針才不會發(fā)生拷貝,傳遞值是會發(fā)生拷貝,但為什么現(xiàn)在無論是傳遞指針還是傳遞值,字符串都沒有發(fā)生拷貝
這是因為 &s
打印的是函數(shù)參數(shù) s
在棧上的地址,每次函數(shù)調用都會不同
func main() { ptr := (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0x49b7ba fmt.Println("s 地址:", &s) // 0x528650 f1(s) f2(&s) } func f1(s string) string { fmt.Println("f1 s:", &s) // 0xc000014070 ptr := unsafe.StringData(s) fmt.Println("f1:", ptr) // 0x49b7ba return s } func f2(s *string) *string { fmt.Println("f2 s:", s) // 0x528650 ptr := unsafe.StringData(*s) fmt.Println("f2:", ptr) // 0x49b7ba return s }
改變字符串地址的方式
1.強轉
func StringSlice1(s string) string { fmt.Println("string:", unsafe.StringData(s)) // 0xc000100000 s1 := string([]byte(s[:20])) ptr := unsafe.StringData(s1) fmt.Println("StringSlice1:", ptr) // 0xc0000bc000 return s1 }
2.改變首字符,就能改變 s1
的地址
func StringSlice2(s string) string { fmt.Println("string:", unsafe.StringData(s)) // 0xc000100000 s1 := (" " + s[:20])[1:] ptr := unsafe.StringData(s1) fmt.Println("StringSlice2:", ptr) // 0xc000018199 return s1 }
3.使用 StringsBuilder
改變 s1
的地址
func StringSliceUseBuilder(s string) string { fmt.Println("string:", unsafe.StringData(s)) // 0xc000100000 var b strings.Builder b.Grow(20) b.WriteString(s[:20]) s1 := b.String() ptr := unsafe.StringData(s1) fmt.Println("StringSliceUseBuilder:", ptr) // 0xc0000b0000 return s1 }
字符切換零拷貝轉換
雖然同是切片操作,但是 s1
會改變地址,而 s2
不會改變地址
s1
強轉為字符串的指針類型s2
:先對s
進行取指,取指之后將它轉成字符串切片指針類型,然后在獲取指針的內容
所以 s2
的方法是零拷貝轉換
func main() { ptr := (*reflect.StringHeader)(unsafe.Pointer(&s)) fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // 0xc000180000 } func stringToBytes() { s1 := []byte(s) fmt.Println("s1: ", unsafe.SliceData(s1)) // 0xc000280000 s2 := *(*[]byte)(unsafe.Pointer(&s)) fmt.Println("s2: ", unsafe.SliceData(s2)) // 0xc000180000 }
到此這篇關于Go語言中字符串賦值中的問題與解決方法的文章就介紹到這了,更多相關Go字符串賦值內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
golang實現(xiàn)一個簡單的websocket聊天室功能
這篇文章主要介紹了golang實現(xiàn)一個簡單的websocket聊天室功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-10-10Golang程序漏洞檢測器govulncheck的安裝和使用
govulncheck 是一個命令行工具,可以幫助 Golang 開發(fā)者快速找到項目代碼和依賴的模塊中的安全漏洞,該工具可以分析源代碼和二進制文件,識別代碼中對這些漏洞的任何直接或間接調用,本文就給大家介紹一下govulncheck安裝和使用,需要的朋友可以參考下2023-09-09Go 結構體、數(shù)組、字典和 json 字符串的相互轉換方法
今天小編就為大家分享一篇Go 結構體、數(shù)組、字典和 json 字符串的相互轉換方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-08-08Go語言規(guī)范context?類型的key用法示例解析
這篇文章主要為大家介紹了Go語言規(guī)范context?類型的key用法示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08