亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

GoLang string類型深入分析

 更新時(shí)間:2023年01月28日 14:00:28   作者:i-neojos  
string 作為 go 語言中的基礎(chǔ)類型,其實(shí)有一些需要反復(fù)揣摩的,可能是我們使用的場景太簡單,也可能是我們不需要那可憐的一點(diǎn)優(yōu)化來提高性能,對(duì)它也就沒那么上心了

文章運(yùn)行環(huán)境:go version go1.16.6 darwin/amd64

并發(fā)不安全

看下面的代碼,大家覺得會(huì)輸出什么?大多數(shù)人應(yīng)該都會(huì)覺得輸出""、abc、neoj 這三種情況,但真實(shí)的情況并不是這樣,真實(shí)情況是只輸出 “” 空字符串。

結(jié)合日常的工作,類似這種并發(fā)操作同一個(gè)變量的情況也比較常見,為什么業(yè)務(wù)沒有發(fā)生異常問題?

var name string = ""
func main() {
	go func() {
		for {
			name = "abc"
		}
	}()
	go func() {
		for {
			name = "neoj"
		}
	}()
	for {
		fmt.Println(name)
	}
}

1.14 之后引入了 G 搶占式調(diào)度,那為什么代碼中的兩個(gè)協(xié)程沒有執(zhí)行呢?其實(shí)是編譯器做了優(yōu)化,這兩個(gè)協(xié)程被省略掉了。

我們對(duì)代碼做一點(diǎn)調(diào)整,在協(xié)程中加一行空的輸出,輸出結(jié)果中出現(xiàn)了一些特例,比如:neo、abca。其中,neo 字符串長度等于 abc 的長度,而 abca 的長度等于 neoj 的長度。

var name string = ""
func main() {
	go func() {
		for {
			name = "abc"
			fmt.Printf("")
		}
	}()
	go func() {
		for {
			name = "neoj"
			fmt.Printf("")
		}
	}()
	for {
		if name != "abc" && name != "neoj" {
			fmt.Println(name)
		}
	}
}

例子說明,string 的賦值并不是原子的。

Go 語言中 string 的內(nèi)存結(jié)果如下,它包含兩部分:Data 表示實(shí)際的數(shù)據(jù)部分,而 Len 表示字符串的長度。

所以,通過方法 len 來計(jì)算字符串的長度并不會(huì)有性能開銷,len 方法會(huì)直接返回結(jié)構(gòu)體的 Len 屬性;而傳遞字符串類型的參數(shù),使用指針類型和值類型,性能上也不會(huì)有太大差別。

type StringHeader struct {
	Data uintptr
	Len  int
}

字符串的并發(fā)不安全,主要就是給這兩個(gè)字段的賦值,沒有辦法保證原子性。參考 runtime/string.go 中的源碼,我們可以了解字符串生成過程。

并發(fā)賦值的情況下,Data 指向的地址和 Len 無法保證一一對(duì)應(yīng)。所以,通過 Data 獲取到內(nèi)存的首地址,通過 Len 去讀取指定長度的內(nèi)存時(shí),就會(huì)出現(xiàn)內(nèi)存讀取異常的情況。

func rawstring(size int) (s string, b []byte) {
	p := mallocgc(uintptr(size), nil, false)
	stringStructOf(&s).str = p
	stringStructOf(&s).len = size
	*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}
	return
}

rawstring 函數(shù)在字符串拼接的時(shí)候被調(diào)用,我們代碼中創(chuàng)建一個(gè)字符串類型,每次都生成一份新的內(nèi)存空間。特別強(qiáng)調(diào),創(chuàng)建和字符串賦值需要區(qū)分開來。賦值的過程其實(shí)是值拷貝,拷貝的便是 StringHeader 結(jié)構(gòu)體。

var name string = ""
func main() {
	blog := name
	fmt.Println(blog)
}

上面的變量 blog 是 name 的值拷貝,底層指向的字符串是同一塊內(nèi)存空間。這個(gè)賦值過程中,發(fā)生拷貝的只是外層的 StringHeader 對(duì)象。

Go 中通過 unsafe 包可以強(qiáng)制對(duì)內(nèi)存數(shù)據(jù)做類型轉(zhuǎn)換,我們將 blog 和 name 的內(nèi)存地址打印出來比較一下。最終打印輸出兩個(gè)變量的地址和Data地址??梢钥闯觯x值前后,Data指向的地址并沒有發(fā)生變化。

type StringHeader struct {
	Data uintptr
	Len  int
}
var name string = "g"
func main() {
	blog := name
	n := (*StringHeader)(unsafe.Pointer(&name))
	b := (*StringHeader)(unsafe.Pointer(&blog))
	fmt.Println(&n, n.Data)    // 0xc00018a020 17594869
	fmt.Println(&b, b.Data)    // 0xc00018a028 17594869
}

string 并發(fā)不安全讀寫,會(huì)導(dǎo)致線上服務(wù)偶發(fā) panic。比如使用 json 對(duì)內(nèi)存異常的 string 做序列化的時(shí)候。下面的例子中,其中一個(gè)協(xié)程用來賦值為空,非常容易復(fù)現(xiàn) panic。

type People struct {
	Name string
}
var p *People = new(People)
func main() {
	go func() {
		for {
			p.Name = ""
		}
	}()
	go func() {
		for {
			p.Name = "neoj"
		}
	}()
	for {
		_, _ = json.Marshal(p)
	}
}

下面是 panic 的堆棧信息,空字符串的 Data 指向的是 nil 的地址,而并發(fā)導(dǎo)致 Len 字段有值,最終導(dǎo)致發(fā)生 panic。

競態(tài)競爭

對(duì)同一個(gè)變量并發(fā)讀寫,如果沒有使用輔助的同步操作,就會(huì)出現(xiàn)不符合預(yù)期的情況。直白的講,我們開發(fā)完一個(gè)程序之后,針對(duì)同樣的輸入,會(huì)輸出什么結(jié)果,我們是不確定的。

可以參考 The Go Memory Model 的介紹,強(qiáng)調(diào)一下數(shù)據(jù)競爭的概念:

A data race is defined as a write to a memory location happening concurrently with another read or write to that same location, unless all the accesses involved are atomic data accesses as provided by the sync/atomic package

幸運(yùn)的是,Go 已經(jīng)集成了現(xiàn)成的工具來診斷數(shù)據(jù)競爭:-race。在 go build、或者直接執(zhí)行的時(shí)候,指定 -race 屬性,系統(tǒng)會(huì)做數(shù)據(jù)競爭檢測(cè),并打印輸出。

以最近的代碼為例,如果你使用的也是 goland 編譯器,只需要在 Run Configurations / Go tool arguments 中指定 -race 屬性,運(yùn)行程序,就會(huì)出現(xiàn)下面的檢測(cè)結(jié)果:

面對(duì)生產(chǎn)環(huán)境,-race 有比較嚴(yán)重的性能開銷,我們最好是開發(fā)環(huán)境做競態(tài)檢測(cè)。

-race 是通過編譯器注入代碼來執(zhí)行檢測(cè)的,在函數(shù)執(zhí)行前、執(zhí)行后都會(huì)做內(nèi)存統(tǒng)計(jì)。也就是說:只有被執(zhí)行到的代碼才能被檢測(cè)到。所以,如果開發(fā)階段做競態(tài)檢測(cè)的話,一定要保證代碼被執(zhí)行到了。

再加上埋點(diǎn)的內(nèi)存統(tǒng)計(jì)也是有策略的,也不可能保證存在數(shù)據(jù)競爭的代碼就一定會(huì)被檢測(cè)出來,最好可以多執(zhí)行幾次來避免這種情況。

字符串優(yōu)化

因字符串并發(fā)讀寫導(dǎo)致的 panic,很容易被 Go 的字符串優(yōu)化帶偏。

我在第一次遇到這種情況的時(shí)候,想到的居然是:會(huì)不會(huì)是底層優(yōu)化導(dǎo)致的。因?yàn)榘l(fā)生 panic 的代碼用到了 map 的數(shù)據(jù)結(jié)構(gòu)。這種想法很快被我用測(cè)試用例排除了。

[]byte 到 string 類型轉(zhuǎn)換是比較常規(guī)的操作,正常情況下,轉(zhuǎn)換都會(huì)申請(qǐng)了一份新的內(nèi)存空間。但 Go 為了提高性能,在某些場景下 string 和 []byte 會(huì)共用一份內(nèi)存空間,這種場景下也能寫亂內(nèi)存。

// slicebytetostringtmp returns a "string" referring to the actual []byte bytes.
//
func slicebytetostringtmp(ptr *byte, n int) (str string) {
	if raceenabled && n > 0 {
		racereadrangepc(unsafe.Pointer(ptr),
			uintptr(n),
			getcallerpc(),
			funcPC(slicebytetostringtmp))
	}
	if msanenabled && n > 0 {
		msanread(unsafe.Pointer(ptr), uintptr(n))
	}
	stringStructOf(&str).str = unsafe.Pointer(ptr)
	stringStructOf(&str).len = n
	return
}

程序中出現(xiàn)問題,還是要先充分審查自己開發(fā)的代碼

到此這篇關(guān)于GoLang string類型深入分析的文章就介紹到這了,更多相關(guān)Go string內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Go語言中init函數(shù)與匿名函數(shù)使用淺析

    Go語言中init函數(shù)與匿名函數(shù)使用淺析

    這篇文章主要介紹了Go語言中init函數(shù)與匿名函數(shù)使用淺析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧
    2023-01-01
  • Golang 處理浮點(diǎn)數(shù)遇到的精度問題(使用decimal)

    Golang 處理浮點(diǎn)數(shù)遇到的精度問題(使用decimal)

    本文主要介紹了Golang 處理浮點(diǎn)數(shù)遇到的精度問題,不使用decimal會(huì)出大問題,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-02-02
  • golang flag簡單用法

    golang flag簡單用法

    本篇文章介紹了golang flag包的一個(gè)簡單的用法,希望通過一個(gè)簡單的實(shí)例,能讓大家了解它的用法,從中獲得啟發(fā)
    2018-09-09
  • Go?panic的三種產(chǎn)生方式細(xì)節(jié)探究

    Go?panic的三種產(chǎn)生方式細(xì)節(jié)探究

    這篇文章主要介紹了Go?panic的三種產(chǎn)生方式細(xì)節(jié)探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • 圖文詳解go語言反射實(shí)現(xiàn)原理

    圖文詳解go語言反射實(shí)現(xiàn)原理

    這篇文章主要介紹了圖文詳解go語言反射實(shí)現(xiàn)原理,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧,需要的朋友可以參考下
    2020-02-02
  • Go1.18新特性之泛型使用三步曲(小結(jié))

    Go1.18新特性之泛型使用三步曲(小結(jié))

    本文主要介紹了Go1.18新特性之泛型,是Go1.18的新特性,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-04-04
  • Golang中Channel實(shí)戰(zhàn)技巧與一些說明

    Golang中Channel實(shí)戰(zhàn)技巧與一些說明

    channel是Go語言內(nèi)建的first-class類型,也是Go語言與眾不同的特性之一,下面這篇文章主要給大家介紹了關(guān)于Golang中Channel實(shí)戰(zhàn)技巧與一些說明的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-11-11
  • Golang如何自定義logrus日志保存為日志文件

    Golang如何自定義logrus日志保存為日志文件

    這篇文章主要給大家介紹了關(guān)于Golang如何自定義logrus日志保存為日志文件的相關(guān)資料,logrus是目前Github上star數(shù)量最多的日志庫,logrus功能強(qiáng)大,性能高效,而且具有高度靈活性,提供了自定義插件的功能,很多開源項(xiàng)目都是用了logrus來記錄其日志,需要的朋友可以參考下
    2024-02-02
  • 使用Go語言實(shí)現(xiàn)心跳機(jī)制

    使用Go語言實(shí)現(xiàn)心跳機(jī)制

    心跳最典型的應(yīng)用場景是是探測(cè)服務(wù)是否存活,這篇文章主要來和大家介紹一下如何使用Go語言實(shí)現(xiàn)一個(gè)簡單的心跳程序,感興趣的可以了解下
    2024-01-01
  • Go中阻塞以及非阻塞操作實(shí)現(xiàn)(Goroutine和main Goroutine)

    Go中阻塞以及非阻塞操作實(shí)現(xiàn)(Goroutine和main Goroutine)

    本文主要介紹了Go中阻塞以及非阻塞操作實(shí)現(xiàn)(Goroutine和main Goroutine),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-05-05

最新評(píng)論