亚洲国产日韩欧美一区二区三区,精品亚洲国产成人av在线,国产99视频精品免视看7,99国产精品久久久久久久成人热,欧美日韩亚洲国产综合乱

目錄
什麼是原子運(yùn)算?
原子運(yùn)算的使用場(chǎng)景是什麼?
原子運(yùn)算是怎麼實(shí)現(xiàn)的?
x86 LOCK 的時(shí)候發(fā)生了什么
原子操作有什么特征?
go 里面有哪些原子操作?
增減(Add)
比較并交換(CompareAndSwap)
載入(Load)
存儲(chǔ)(Store)
交換(Swap)
原子操作任意類(lèi)型的值 - atomic.Value
atomic.Value 源碼分析
Load - 讀取
Store - 存儲(chǔ)
Swap - 交換
CompareAndSwap - 比較并交換
其他原子類(lèi)型
原子操作與互斥鎖比較
性能比較
go 的 sync 包中的原子操作
sync.Map 中的原子操作
sync.WaitGroup 中的原子操作
CAS 操作有失敗必然有成功
總結(jié)
首頁(yè) 後端開(kāi)發(fā) Golang 什麼是原子操作?深入淺析go中的原子操作

什麼是原子操作?深入淺析go中的原子操作

Mar 28, 2023 pm 07:04 PM
go 後端 原子操作

什麼是原子操作?深入淺析go中的原子操作

在我們前面的一些介紹 sync 套件相關(guān)的文章中,我們應(yīng)該也發(fā)現(xiàn)了,其中有不少地方使用了原子操作。 例如 sync.WaitGroup、sync.Mapsync.Pool,這些結(jié)構(gòu)體的實(shí)作中都有原子運(yùn)算的身影。 原子操作在並發(fā)程式設(shè)計(jì)中是一種非常重要的操作,它可以保證並發(fā)安全,而且效率也很高。 本文將會(huì)深入探討 go 中原子操作的原理、使用場(chǎng)景、用法等內(nèi)容。

什麼是原子運(yùn)算?

原子運(yùn)算是變數(shù)層級(jí)的互斥鎖。

如果讓我用一句話(huà)來(lái)說(shuō)明什麼是原子操作,那就是:原子操作是變數(shù)層級(jí)的互斥鎖。 簡(jiǎn)單來(lái)說(shuō),就是同一時(shí)刻,只能有一個(gè) CPU 對(duì)變數(shù)進(jìn)行讀取或?qū)懭搿?當(dāng)我們想要對(duì)某個(gè)變數(shù)做並發(fā)安全的修改,除了使用官方提供的 Mutex#,還可以使用 sync/atomic 套件的原子操作, 它能夠保證對(duì)變數(shù)的讀取或修改期間不會(huì)被其他的協(xié)程所影響。

我們可以用下圖來(lái)表示:

什麼是原子操作?深入淺析go中的原子操作

說(shuō)明:在上圖中,我們有三個(gè)CPU 邏輯核,其中CPU 1 正在對(duì)變數(shù)v 做原子操作,這時(shí)候CPU 2 和CPU 3 不能對(duì)v 做任何操作, 在 CPU 1 作業(yè)完成後,CPU 2 和 CPU 3 可以取得到 v 的最新值。

從這個(gè)角度看,我們可以把 sync/atomic 套件中的原子操作看成是變數(shù)層級(jí)的互斥鎖。 就是說(shuō),在 go 中,當(dāng)一個(gè)協(xié)程對(duì)變數(shù)做原子操作時(shí),其他協(xié)程不能對(duì)這個(gè)變數(shù)做任何操作,直到這個(gè)協(xié)程操作完成。

原子運(yùn)算的使用場(chǎng)景是什麼?

拿一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明原子操作的使用場(chǎng)景:

func TestAtomic(t *testing.T) {
	var sum = 0
	var wg sync.WaitGroup
	wg.Add(1000)

	// 啟動(dòng) 1000 個(gè)協(xié)程,每個(gè)協(xié)程對(duì) sum 做加法操作
	for i := 0; i < 1000; i++ {
		go func() {
			defer wg.Done()
			sum++
		}()
	}

	// 等待所有的協(xié)程都執(zhí)行完畢
	wg.Wait()
	fmt.Println(sum) // 這里輸出多少呢?
}

我們可以在自己的電腦上運(yùn)行這段程式碼,看看輸出的結(jié)果是多少。 不出意外的話(huà),應(yīng)該每次可能都不一樣,而且應(yīng)該也不是 1000,這是為什麼呢?

這是因?yàn)?,CPU 在對(duì)sum 做加法的時(shí)候,需要先將sum 目前的值讀取到CPU 的暫存器中,然後再加法操作,最後再寫(xiě)回記憶體。 如果有兩個(gè)CPU 同時(shí)取了sum 的值,然後都進(jìn)行了加法操作,然後都再寫(xiě)回記憶體中,那麼就會(huì)導(dǎo)致sum 的值被覆寫(xiě),從而導(dǎo)致結(jié)果不正確。

舉個(gè)例子,目前記憶體中的 sum 為 1,然後兩個(gè) CPU 同時(shí)取了這個(gè) 1 來(lái)做加法,然後都得到了結(jié)果 2, 然後這兩個(gè) CPU 將各自的計(jì)算結(jié)果寫(xiě)回記憶體中,那麼記憶體中的 sum 就變成了 2,而不是 3。

在這種場(chǎng)景下,我們可以使用原子操作來(lái)實(shí)現(xiàn)並發(fā)安全的加法操作:

func TestAtomic1(t *testing.T) {
	// 將 sum 的類(lèi)型改成 int32,因?yàn)樵硬僮髦荒茚槍?duì) int32、int64、uint32、uint64、uintptr 這幾種類(lèi)型
	var sum int32 = 0
	var wg sync.WaitGroup
	wg.Add(1000)

    // 啟動(dòng) 1000 個(gè)協(xié)程,每個(gè)協(xié)程對(duì) sum 做加法操作
	for i := 0; i < 1000; i++ {
		go func() {
			defer wg.Done()
			// 將 sum++ 改成下面這樣
			atomic.AddInt32(&sum, 1)
		}()
	}

	wg.Wait()
	fmt.Println(sum) // 輸出 1000
}

在上面這個(gè)例子中,我們每次執(zhí)行都能得到 1000 這個(gè)結(jié)果。

因?yàn)槭褂迷硬僮鞯臅r(shí)候,同一時(shí)刻只能有一個(gè) CPU 對(duì)變數(shù)進(jìn)行讀取或?qū)懭?,所以就不?huì)出現(xiàn)上面的問(wèn)題了。

所以很多需要對(duì)變數(shù)做並發(fā)讀寫(xiě)的地方,我們都可以考慮一下,是否可以使用原子操作來(lái)實(shí)現(xiàn)並發(fā)安全的操作(而不是使用互斥鎖,互斥鎖效率相比原子操作較低)。

原子操作的使用場(chǎng)景也是和互斥鎖類(lèi)似的,但是不一樣的是,我們的鎖粒度只是一個(gè)變數(shù)而已。也就是說(shuō),當(dāng)我們不允許多個(gè) CPU 同時(shí)對(duì)變數(shù)進(jìn)行讀寫(xiě)的時(shí)候(保證變數(shù)在同一時(shí)刻只能一個(gè) CPU 運(yùn)算),就可以使用原子運(yùn)算。

原子運(yùn)算是怎麼實(shí)現(xiàn)的?

看完上面原子操作的介紹,有沒(méi)有覺(jué)得原子操作很神奇,居然有這麼好用的東西。那它到底是怎麼實(shí)現(xiàn)的呢?

一般情況下,原子操作的實(shí)作需要特殊的 CPU 指令或系統(tǒng)呼叫。 這些指令或系統(tǒng)呼叫可以保證在執(zhí)行期間不會(huì)被其他操作或事件中斷,從而保證操作的原子性。

例如,在 x86 架構(gòu)的 CPU 中,可以使用 LOCK 前綴來(lái)實(shí)現(xiàn)原子操作。LOCK 前綴可以與其他指令一起使用,用于鎖定內(nèi)存總線(xiàn),防止其他 CPU 訪問(wèn)同一內(nèi)存地址,從而實(shí)現(xiàn)原子操作。 在使用 LOCK 前綴的指令執(zhí)行期間,CPU 會(huì)將當(dāng)前處理器緩存中的數(shù)據(jù)寫(xiě)回到內(nèi)存中,并鎖定該內(nèi)存地址, 防止其他 CPU 修改該地址的數(shù)據(jù)(所以原子操作總是可以讀取到最新的數(shù)據(jù))。 一旦當(dāng)前 CPU 對(duì)該地址的操作完成,CPU 會(huì)釋放該內(nèi)存地址的鎖定,其他 CPU 才能繼續(xù)對(duì)該地址進(jìn)行訪問(wèn)。

x86 LOCK 的時(shí)候發(fā)生了什么

我們?cè)賮?lái)捋一下上面的內(nèi)容,看看 LOCK 前綴是如何實(shí)現(xiàn)原子操作的:

  1. CPU 會(huì)將當(dāng)前處理器緩存中的數(shù)據(jù)寫(xiě)回到內(nèi)存中。(因此我們總能讀取到最新的數(shù)據(jù))
  2. 然后鎖定該內(nèi)存地址,防止其他 CPU 修改該地址的數(shù)據(jù)。
  3. 一旦當(dāng)前 CPU 對(duì)該地址的操作完成,CPU 會(huì)釋放該內(nèi)存地址的鎖定,其他 CPU 才能繼續(xù)對(duì)該地址進(jìn)行訪問(wèn)。

其他架構(gòu)的 CPU 可能會(huì)略有不同,但是原理是一樣的。

原子操作有什么特征?

  1. 不會(huì)被中斷:原子操作是一個(gè)不可分割的操作,要么全部執(zhí)行,要么全部不執(zhí)行,不會(huì)出現(xiàn)中間狀態(tài)。這是保證原子性的基本前提。同時(shí),原子操作過(guò)程中不會(huì)有上下文切換的過(guò)程。
  2. 操作對(duì)象是共享變量:原子操作通常是對(duì)共享變量進(jìn)行的,也就是說(shuō),多個(gè)協(xié)程可以同時(shí)訪問(wèn)這個(gè)變量,因此需要采用原子操作來(lái)保證數(shù)據(jù)的一致性和正確性。
  3. 并發(fā)安全:原子操作是并發(fā)安全的,可以保證多個(gè)協(xié)程同時(shí)進(jìn)行操作時(shí)不會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題(雖然說(shuō)是同時(shí),但是實(shí)際上在操作那個(gè)變量的時(shí)候是互斥的)。
  4. 無(wú)需加鎖:原子操作不需要使用互斥鎖來(lái)保證數(shù)據(jù)的一致性和正確性,因此可以避免互斥鎖的使用帶來(lái)的性能損失。
  5. 適用場(chǎng)景比較局限:原子操作適用于操作單個(gè)變量,如果需要同時(shí)并發(fā)讀寫(xiě)多個(gè)變量,可能需要考慮使用互斥鎖。

go 里面有哪些原子操作?

在 go 中,主要有以下幾種原子操作:Add、CompareAndSwapLoad、StoreSwap。

增減(Add)

  1. 用于進(jìn)行增加或減少的原子操作,函數(shù)名以 Add 為前綴,后綴針對(duì)特定類(lèi)型的名稱(chēng)。
  2. 原子增被操作的類(lèi)型只能是數(shù)值類(lèi)型,即 int32、int64、uint32、uint64、uintptr
  3. 原子增減函數(shù)的第一個(gè)參數(shù)為原值,第二個(gè)參數(shù)是要增減多少。
  4. 方法:
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

int32int64 的第二個(gè)參數(shù)可以是負(fù)數(shù),這樣就可以做原子減法了。

比較并交換(CompareAndSwap)

也就是我們常見(jiàn)的 CAS,在 CAS 操作中,會(huì)需要拿舊的值跟 old 比較,如果相等,就將 new 賦值給 addr。 如果不相等,則不做任何操作。最后返回一個(gè) bool 值,表示是否成功 swap。

也就是說(shuō),這個(gè)操作可能是不成功的。這很正常,在并發(fā)環(huán)境下,多個(gè)協(xié)程對(duì)同一個(gè)變量進(jìn)行操作,肯定會(huì)存在競(jìng)爭(zhēng)的情況。 在這種情況下,偶爾的失敗是正常的,我們只需要在失敗的時(shí)候,重新嘗試即可。 因?yàn)樵硬僮餍枰臅r(shí)間往往是比較短的,因此在失敗的時(shí)候,我們可以通過(guò)自旋的方式來(lái)再次進(jìn)行嘗試。

在這種情況下,如果不自旋,那就需要將這個(gè)協(xié)程掛起,等待其他協(xié)程完成操作,然后再次嘗試。這個(gè)過(guò)程相比自旋可能會(huì)更加耗時(shí)。 因?yàn)楹苡锌赡苓@次原子操作不成功,下一次就成功了。如果我們每次都將協(xié)程掛起,那么效率就會(huì)大大降低。

for + 原子操作的方式,在 go 的 sync 包中很多地方都有使用,比如 sync.Map,sync.Pool 等。 這也是使用原子操作時(shí)一個(gè)非常常見(jiàn)的使用模式。

CompareAndSwap 的功能:

  1. 用于比較并交換的原子操作,函數(shù)名以 CompareAndSwap 為前綴,后綴針對(duì)特定類(lèi)型的名稱(chēng)。
  2. 原子比較并交換被操作的類(lèi)型可以是數(shù)值類(lèi)型或指針類(lèi)型,即 int32、int64、uint32、uint64、uintptrunsafe.Pointer
  3. 原子比較并交換函數(shù)的第一個(gè)參數(shù)為原值指針,第二個(gè)參數(shù)是要比較的值,第三個(gè)參數(shù)是要交換的值。
  4. 方法:
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

載入(Load)

原子性的讀取操作接受一個(gè)對(duì)應(yīng)類(lèi)型的指針值,返回該指針指向的值。原子性讀取意味著讀取值的同時(shí),當(dāng)前計(jì)算機(jī)的任何 CPU 都不會(huì)進(jìn)行針對(duì)值的讀寫(xiě)操作。

如果不使用原子 Load,當(dāng)使用 v := value 這種賦值方式為變量 v 賦值時(shí),讀取到的 value 可能不是最新的,因?yàn)樵谧x取操作時(shí)其他協(xié)程對(duì)它的讀寫(xiě)操作可能會(huì)同時(shí)發(fā)生。

Load 操作有下面這些:

func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

存儲(chǔ)(Store)

Store 可以將 val 值保存到 *addr 中,Store 操作是原子性的,因此在執(zhí)行 Store 操作時(shí),當(dāng)前計(jì)算機(jī)的任何 CPU 都不會(huì)進(jìn)行針對(duì) *addr 的讀寫(xiě)操作。

  1. 原子性存儲(chǔ)會(huì)將 val 值保存到 *addr 中。
  2. 與讀操作對(duì)應(yīng)的寫(xiě)入操作,sync/atomic 提供了與原子值載入 Load 函數(shù)相對(duì)應(yīng)的原子值存儲(chǔ) Store 函數(shù),原子性存儲(chǔ)函數(shù)均以 Store 為前綴。

Store 操作有下面這些:

func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintpre, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

交換(Swap)

SwapStore 有點(diǎn)類(lèi)似,但是它會(huì)返回 *addr 的舊值。

func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

原子操作任意類(lèi)型的值 - atomic.Value

從上一節(jié)中,我們知道了在 go 中原子操作可以操作 int32int64、uint32、uint64、uintptr、unsafe.Pointer 這些類(lèi)型的值。 但是在實(shí)際開(kāi)發(fā)中,我們的類(lèi)型還有很多,比如 string、struct 等等,那這些類(lèi)型的值如何進(jìn)行原子操作呢?答案是使用 atomic.Value。

atomic.Value 是一個(gè)結(jié)構(gòu)體,它的內(nèi)部有一個(gè) any 類(lèi)型的字段,存儲(chǔ)了我們要原子操作的值,也就是一個(gè)任意類(lèi)型的值。

atomic.Value 支持以下操作:

  • Load:原子性的讀取 Value 中的值。
  • Store:原子性的存儲(chǔ)一個(gè)值到 Value 中。
  • Swap:原子性的交換 Value 中的值,返回舊值。
  • CompareAndSwap:原子性的比較并交換 Value 中的值,如果舊值和 old 相等,則將 new 存入 Value 中,返回 true,否則返回 false

atomic.Value 的這些操作跟上面講到的那些操作其實(shí)差不多,只不過(guò) atomic.Value 可以操作任意類(lèi)型的值。 那 atomic.Value 是如何實(shí)現(xiàn)的呢?

atomic.Value 源碼分析

atomic.Value 是一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體只有一個(gè)字段:

// Value 提供一致類(lèi)型值的原子加載和存儲(chǔ)。
type Value struct {
	v any
}

Load - 讀取

Load 返回由最近的 Store 設(shè)置的值。如果還沒(méi)有 Store 過(guò)任何值,則返回 nil

// Load 返回由最近的 Store 設(shè)置的值。
func (v *Value) Load() (val any) {
	// atomic.Value 轉(zhuǎn)換為 efaceWords
	vp := (*efaceWords)(unsafe.Pointer(v))

	// 判斷 atomic.Value 的類(lèi)型
	typ := LoadPointer(&vp.typ)
	// 第一次 Store 還沒(méi)有完成,直接返回 nil
	if typ == nil || typ == unsafe.Pointer(&firstStoreInProgress) {
		// firstStoreInProgress 是一個(gè)特殊的變量,存儲(chǔ)到 typ 中用來(lái)表示第一次 Store 還沒(méi)有完成
		return nil
	}

	// 獲取 atomic.Value 的值
	data := LoadPointer(&vp.data)
	// 將 val 轉(zhuǎn)換為 efaceWords 類(lèi)型
	vlp := (*efaceWords)(unsafe.Pointer(&val))
	// 分別賦值給 val 的 typ 和 data
	vlp.typ = typ
	vlp.data = data
	return
}

atomic.Value 的源碼中,我們都可以看到 efaceWords 的身影,它實(shí)際上代表的是 interface{}/any 類(lèi)型:

// 表示一個(gè) interface{}/any 類(lèi)型
type efaceWords struct {
	typ  unsafe.Pointer
	data unsafe.Pointer
}

看到這里我們會(huì)不會(huì)覺(jué)得很困惑,直接返回 val 不就可以了嗎?為什么要將 val 轉(zhuǎn)換為 efaceWords 類(lèi)型呢?

這是因?yàn)?go 中的原子操作只能操作 int32int64、uint32uint64、uintptrunsafe.Pointer 這些類(lèi)型的值, 不支持 interface{} 類(lèi)型,但是如果了解 interface{} 底層結(jié)構(gòu)的話(huà),我們就知道 interface{} 底層其實(shí)就是一個(gè)結(jié)構(gòu)體, 它有兩個(gè)字段,一個(gè)是 type,一個(gè)是 data,type 用來(lái)存儲(chǔ) interface{} 的類(lèi)型,data 用來(lái)存儲(chǔ) interface{} 的值。 而且這兩個(gè)字段都是 unsafe.Pointer 類(lèi)型的,所以其實(shí)我們可以對(duì) interface{}typedata 分別進(jìn)行原子操作, 這樣最終其實(shí)也可以達(dá)到了原子操作 interface{} 的目的了,是不是非常地巧妙呢?

Store - 存儲(chǔ)

StoreValue 的值設(shè)置為 val。對(duì)給定值的所有存儲(chǔ)調(diào)用必須使用相同具體類(lèi)型的值。不一致類(lèi)型的存儲(chǔ)會(huì)發(fā)生恐慌,Store(nil) 也會(huì) panic。

// Store 將 Value 的值設(shè)置為 val。
func (v *Value) Store(val any) {
	// 不能存儲(chǔ) nil 值
	if val == nil {
		panic("sync/atomic: store of nil value into Value")
	}
	// atomic.Value 轉(zhuǎn)換為 efaceWords
	vp := (*efaceWords)(unsafe.Pointer(v))
	// val 轉(zhuǎn)換為 efaceWords
	vlp := (*efaceWords)(unsafe.Pointer(&val))
	
	// 自旋進(jìn)行原子操作,這個(gè)過(guò)程不會(huì)很久,開(kāi)銷(xiāo)相比互斥鎖小
	for {
		// LoadPointer 可以保證獲取到的是最新的
		typ := LoadPointer(&vp.typ)
		// 第一次 store 的時(shí)候 typ 還是 nil,說(shuō)明是第一次 store
		if typ == nil {
			// 嘗試開(kāi)始第一次 Store。
			// 禁用搶占,以便其他 goroutines 可以自旋等待完成。
			// (如果允許搶占,那么其他 goroutine 自旋等待的時(shí)間可能會(huì)比較長(zhǎng),因?yàn)榭赡軙?huì)需要進(jìn)行協(xié)程調(diào)度。)
			runtime_procPin()
			// 搶占失敗,意味著有其他 goroutine 成功 store 了,允許搶占,再次嘗試 Store
			// 這也是一個(gè)原子操作。
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
				runtime_procUnpin()
				continue
			}
			// 完成第一次 store
			// 因?yàn)橛?firstStoreInProgress 標(biāo)識(shí)的保護(hù),所以下面的兩個(gè)原子操作是安全的。
			StorePointer(&vp.data, vlp.data) // 存儲(chǔ)值(原子操作)
			StorePointer(&vp.typ, vlp.typ)   // 存儲(chǔ)類(lèi)型(原子操作)
			runtime_procUnpin()              // 允許搶占
			return
		}

		// 另外一個(gè) goroutine 正在進(jìn)行第一次 Store。自旋等待。
		if typ == unsafe.Pointer(&firstStoreInProgress) {
			continue
		}

		// 第一次 Store 已經(jīng)完成了,下面不是第一次 Store 了。
		// 需要檢查當(dāng)前 Store 的類(lèi)型跟第一次 Store 的類(lèi)型是否一致,不一致就 panic。
		if typ != vlp.typ {
			panic("sync/atomic: store of inconsistently typed value into Value")
		}

		// 后續(xù)的 Store 只需要 Store 值部分就可以了。
		// 因?yàn)?atomic.Value 只能保存一種類(lèi)型的值。
		StorePointer(&vp.data, vlp.data)
		return
	}
}

Store 中,有以下幾個(gè)注意的點(diǎn):

  1. 使用 firstStoreInProgress 來(lái)確保第一次 Store 的時(shí)候,只有一個(gè) goroutine 可以進(jìn)行 Store 操作,其他的 goroutine需要自旋等待。如果沒(méi)有這個(gè)保護(hù),那么存儲(chǔ) typdata 的時(shí)候就會(huì)出現(xiàn)競(jìng)爭(zhēng)(因?yàn)樾枰獌蓚€(gè)原子操作),導(dǎo)致數(shù)據(jù)不一致。在這里其實(shí)可以將 firstStoreInProgress 看作是一個(gè)互斥鎖。
  2. 在進(jìn)行第一次 Store 的時(shí)候,會(huì)將當(dāng)前的 goroutine 和 P 綁定,這樣拿到 firstStoreInProgress 鎖的協(xié)程就可以盡快地完成第一次 Store操作,這樣一來(lái),其他的協(xié)程也不用等待太久。
  3. 在第一次 Store 的時(shí)候,會(huì)有兩個(gè)原子操作,分別存儲(chǔ)類(lèi)型和值,但是因?yàn)橛?firstStoreInProgress 的保護(hù),所以這兩個(gè)原子操作本質(zhì)上是對(duì) interface{} 的一個(gè)原子存儲(chǔ)操作。
  4. 其他協(xié)程在看到有 firstStoreInProgress 標(biāo)識(shí)的時(shí)候,就會(huì)自旋等待,直到第一次 Store 完成。
  5. 在后續(xù)的 Store 操作中,只需要存儲(chǔ)值就可以了,因?yàn)?atomic.Value 只能保存一種類(lèi)型的值。

Swap - 交換

SwapValue 的值設(shè)置為 new 并返回舊值。對(duì)給定值的所有交換調(diào)用必須使用相同具體類(lèi)型的值。同時(shí),不一致類(lèi)型的交換會(huì)發(fā)生恐慌,Swap(nil) 也會(huì) panic。

// Swap 將 Value 的值設(shè)置為 new 并返回舊值。
func (v *Value) Swap(new any) (old any) {
	// 不能存儲(chǔ) nil 值
	if new == nil {
		panic("sync/atomic: swap of nil value into Value")
	}

	// atomic.Value 轉(zhuǎn)換為 efaceWords
	vp := (*efaceWords)(unsafe.Pointer(v))
	// new 轉(zhuǎn)換為 efaceWords
	np := (*efaceWords)(unsafe.Pointer(&new))
	
	// 自旋進(jìn)行原子操作,這個(gè)過(guò)程不會(huì)很久,開(kāi)銷(xiāo)相比互斥鎖小
	for {
		// 下面這部分代碼跟 Store 一樣,不細(xì)說(shuō)了。
		// 這部分代碼是進(jìn)行第一次存儲(chǔ)的代碼。
		typ := LoadPointer(&vp.typ)
		if typ == nil {
			runtime_procPin()
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
				runtime_procUnpin()
				continue
			}
			StorePointer(&vp.data, np.data)
			StorePointer(&vp.typ, np.typ)
			runtime_procUnpin()
			return nil
		}
		if typ == unsafe.Pointer(&firstStoreInProgress) {
			continue
		}
		if typ != np.typ {
			panic("sync/atomic: swap of inconsistently typed value into Value")
		}

		// ---- 下面是 Swap 的特有邏輯 ----
		// op 是返回值
		op := (*efaceWords)(unsafe.Pointer(&old))
		// 返回舊的值
		op.typ, op.data = np.typ, SwapPointer(&vp.data, np.data)
		return old
	}
}

CompareAndSwap - 比較并交換

CompareAndSwapValue 的值與 old 比較,如果相等則設(shè)置為 new 并返回 true,否則返回 false。 對(duì)給定值的所有比較和交換調(diào)用必須使用相同具體類(lèi)型的值。同時(shí),不一致類(lèi)型的比較和交換會(huì)發(fā)生恐慌,CompareAndSwap(nil, nil) 也會(huì) panic。

// CompareAndSwap 比較并交換。
func (v *Value) CompareAndSwap(old, new any) (swapped bool) {
	// 注意:old 是可以為 nil 的,new 不能為 nil。
	// old 是 nil 表示是第一次進(jìn)行 Store 操作。
	if new == nil {
		panic("sync/atomic: compare and swap of nil value into Value")
	}

	// atomic.Value 轉(zhuǎn)換為 efaceWords
	vp := (*efaceWords)(unsafe.Pointer(v))
	// new 轉(zhuǎn)換為 efaceWords
	np := (*efaceWords)(unsafe.Pointer(&new))
	// old 轉(zhuǎn)換為 efaceWords
	op := (*efaceWords)(unsafe.Pointer(&old))

	// old 和 new 類(lèi)型必須一致,且不能為 nil
	if op.typ != nil && np.typ != op.typ {
		panic("sync/atomic: compare and swap of inconsistently typed values")
	}

	// 自旋進(jìn)行原子操作,這個(gè)過(guò)程不會(huì)很久,開(kāi)銷(xiāo)相比互斥鎖小
	for {
		// LoadPointer 可以保證獲取到的 typ 是最新的
		typ := LoadPointer(&vp.typ)
		if typ == nil { // atomic.Value 是 nil,還沒(méi) Store 過(guò)
			// 準(zhǔn)備進(jìn)行第一次 Store,但是傳遞進(jìn)來(lái)的 old 不是 nil,compare 這一步就失敗了。直接返回 false
			if old != nil {
				return false
			}

			// 下面這部分代碼跟 Store 一樣,不細(xì)說(shuō)了。 
			// 這部分代碼是進(jìn)行第一次存儲(chǔ)的代碼。
			runtime_procPin()
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
				runtime_procUnpin()
				continue
			}
			StorePointer(&vp.data, np.data)
			StorePointer(&vp.typ, np.typ)
			runtime_procUnpin()
			return true
		}
		if typ == unsafe.Pointer(&firstStoreInProgress) {
			continue
		}
		if typ != np.typ {
			panic("sync/atomic: compare and swap of inconsistently typed value into Value")
		}

		// 通過(guò)運(yùn)行時(shí)相等性檢查比較舊版本和當(dāng)前版本。
		// 這允許對(duì)值類(lèi)型進(jìn)行比較,這是包函數(shù)所沒(méi)有的。
		// 下面的 CompareAndSwapPointer 僅確保 vp.data 自 LoadPointer 以來(lái)沒(méi)有更改。
		data := LoadPointer(&vp.data)
		var i any
		(*efaceWords)(unsafe.Pointer(&i)).typ = typ
		(*efaceWords)(unsafe.Pointer(&i)).data = data
		if i != old { // atomic.Value 跟 old 不相等
			return false
		}
		// 只做 val 部分的 cas 操作
		return CompareAndSwapPointer(&vp.data, data, np.data)
	}
}

這里需要特別說(shuō)明的只有最后那個(gè)比較相等的判斷,也就是 data := LoadPointer(&vp.data) 以及往后的幾行代碼。 在開(kāi)發(fā) atomic.Value 第一版的時(shí)候,那個(gè)開(kāi)發(fā)者其實(shí)是將這幾行寫(xiě)成 CompareAndSwapPointer(&vp.data, old.data, np.data) 這種形式的。 但是在舊的寫(xiě)法中,會(huì)存在一個(gè)問(wèn)題,如果我們做 CAS 操作的時(shí)候,如果傳遞的參數(shù) old 是一個(gè)結(jié)構(gòu)體的值這種類(lèi)型,那么這個(gè)結(jié)構(gòu)體的值是會(huì)被拷貝一份的, 同時(shí)再會(huì)被轉(zhuǎn)換為 interface{}/any 類(lèi)型,這個(gè)過(guò)程中,其實(shí)參數(shù)的 olddata 部分指針指向的內(nèi)存跟 vp.data 指向的內(nèi)存是不一樣的。 這樣的話(huà),CAS 操作就會(huì)失敗,這個(gè)時(shí)候就會(huì)返回 false,但是我們本意是要比較它的值,出現(xiàn)這種結(jié)果顯然不是我們想要的。

將值作為 interface{} 參數(shù)使用的時(shí)候,會(huì)存在一個(gè)將值轉(zhuǎn)換為 interface{} 的過(guò)程。具體我們可以看看 interface{} 的實(shí)現(xiàn)原理。

所以,在上面的實(shí)現(xiàn)中,會(huì)將舊值的 typdata 賦值給一個(gè) any 類(lèi)型的變量, 然后使用 i != old 這種方式進(jìn)行判斷,這樣就可以實(shí)現(xiàn)在比較的時(shí)候,比較的是值,而不是由值轉(zhuǎn)換為 interface{} 后的指針。

其他原子類(lèi)型

我們現(xiàn)在知道了,atomic.Value 可以對(duì)任意類(lèi)型做原子操作。 而對(duì)于其他的原子類(lèi)型,比如 int32、int64、uint32uint64、uintptr、unsafe.Pointer 等, 其實(shí)在 go 中也提供了包裝的類(lèi)型,讓我們可以以對(duì)象的方式來(lái)操作這些類(lèi)型。

對(duì)應(yīng)的類(lèi)型如下:

  • atomic.Bool:這個(gè)比較特別,但底層實(shí)際上是一個(gè) uint32 類(lèi)型的值。我們對(duì) atomic.Bool 做原子操作的時(shí)候,實(shí)際上是對(duì) uint32 做原子操作。
  • atomic.Int32int32 類(lèi)型的包裝類(lèi)型
  • atomic.Int64int64 類(lèi)型的包裝類(lèi)型
  • atomic.Uint32uint32 類(lèi)型的包裝類(lèi)型
  • atomic.Uint64uint64 類(lèi)型的包裝類(lèi)型
  • atomic.Uintptruintptr 類(lèi)型的包裝類(lèi)型
  • atomic.Pointerunsafe.Pointer 類(lèi)型的包裝類(lèi)型

這幾種類(lèi)型的實(shí)現(xiàn)的代碼基本一樣,除了類(lèi)型不一樣,我們可以看看 atomic.Int32 的實(shí)現(xiàn):

// An Int32 is an atomic int32. The zero value is zero.
type Int32 struct {
	_ noCopy
	v int32
}

// Load atomically loads and returns the value stored in x.
func (x *Int32) Load() int32 { return LoadInt32(&x.v) }

// Store atomically stores val into x.
func (x *Int32) Store(val int32) { StoreInt32(&x.v, val) }

// Swap atomically stores new into x and returns the previous value.
func (x *Int32) Swap(new int32) (old int32) { return SwapInt32(&x.v, new) }

// CompareAndSwap executes the compare-and-swap operation for x.
func (x *Int32) CompareAndSwap(old, new int32) (swapped bool) {
	return CompareAndSwapInt32(&x.v, old, new)
}

可以看到,atomic.Int32 的實(shí)現(xiàn)都是基于 atomic 包中 int32 類(lèi)型相關(guān)的原子操作函數(shù)來(lái)實(shí)現(xiàn)的。

原子操作與互斥鎖比較

那我們有了互斥鎖,為什么還要有原子操作呢?我們進(jìn)行比較一下就知道了:


原子操作互斥鎖
保護(hù)的范圍變量代碼塊
保護(hù)的粒度
性能
如何實(shí)現(xiàn)的硬件指令軟件層面實(shí)現(xiàn),邏輯較多

如果我們只需要對(duì)某一個(gè)變量做并發(fā)讀寫(xiě),那么使用原子操作就可以了,因?yàn)樵硬僮鞯男阅鼙然コ怄i高很多。 但是如果我們需要對(duì)多個(gè)變量做并發(fā)讀寫(xiě),那么就需要用到互斥鎖了,這種場(chǎng)景往往是在一段代碼中對(duì)不同變量做讀寫(xiě)。

性能比較

我們前面這個(gè)表格提到了原子操作與互斥鎖性能上有差異,我們寫(xiě)幾行代碼來(lái)進(jìn)行比較一下:

// 系統(tǒng)信息 cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
// 10.13 ns/op
func BenchmarkMutex(b *testing.B) {
   var mu sync.Mutex

   for i := 0; i < b.N; i++ {
      mu.Lock()
      mu.Unlock()
   }
}

// 5.849 ns/op
func BenchmarkAtomic(b *testing.B) {
   var sum atomic.Uint64

   for i := 0; i < b.N; i++ {
      sum.Add(uint64(1))
   }
}

在對(duì) Mutex 的性能測(cè)試中,我只是寫(xiě)了簡(jiǎn)單的 Lock()UnLock() 操作,因?yàn)檫@種比較才算是對(duì) Mutex 本身的測(cè)試,而在 Atomic 的性能測(cè)試中,對(duì) sum 做原子累加的操作。最終結(jié)果是,使用 Atomic 的操作耗時(shí)大概比 Mutex 少了 40% 以上。

在實(shí)際開(kāi)發(fā)中,Mutex 保護(hù)的臨界區(qū)內(nèi)往往有更多操作,也就意味著 Mutex 鎖需要耗費(fèi)更長(zhǎng)的時(shí)間才能釋放,也就是會(huì)需要耗費(fèi)比上面這個(gè) 40% 還要多的時(shí)間另外一個(gè)協(xié)程才能獲取到 Mutex 鎖。

go 的 sync 包中的原子操作

在文章的開(kāi)頭,我們就說(shuō)了,在 go 的 sync.Mapsync.Pool 中都有用到了原子操作,本節(jié)就來(lái)看一看這些操作。

sync.Map 中的原子操作

sync.Map 中使用到了一個(gè) entry 結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中大部分操作都是原子操作,我們可以看看它下面這兩個(gè)方法的定義:

// 刪除 entry
func (e *entry) delete() (value any, ok bool) {
	for {
		p := e.p.Load()
		// 已經(jīng)被刪除了,不需要再刪除
		if p == nil || p == expunged {
			return nil, false
		}
		// 刪除成功
		if e.p.CompareAndSwap(p, nil) {
			return *p, true
		}
	}
}

// 如果條目尚未刪除,trySwap 將交換一個(gè)值。
func (e *entry) trySwap(i *any) (*any, bool) {
	for {
		p := e.p.Load()
		// 已經(jīng)被刪除了
		if p == expunged {
			return nil, false
		}
		// swap 成功
		if e.p.CompareAndSwap(p, i) {
			return p, true
		}
	}
}

我們可以看到一個(gè)非常典型的特征就是 for + CompareAndSwap 的組合,這個(gè)組合在 entry 中出現(xiàn)了很多次。

如果我們也需要對(duì)變量做并發(fā)讀寫(xiě),也可以嘗試一下這種 for + CompareAndSwap 的組合。

sync.WaitGroup 中的原子操作

sync.WaitGroup 中有一個(gè)類(lèi)型為 atomic.Uint64state 字段,這個(gè)變量是用來(lái)記錄 WaitGroup 的狀態(tài)的。 在實(shí)際使用中,它的高 32 位用來(lái)記錄 WaitGroup 的計(jì)數(shù)器,低 32 位用來(lái)記錄 WaitGroupWaiter 的數(shù)量,也就是等待條件變量滿(mǎn)足的協(xié)程數(shù)量。

如果不使用一個(gè)變量來(lái)記錄這兩個(gè)值,那么我們就需要使用兩個(gè)變量來(lái)記錄,這樣就會(huì)導(dǎo)致我們需要對(duì)兩個(gè)變量做并發(fā)讀寫(xiě), 在這種情況下,我們就需要使用互斥鎖來(lái)保護(hù)這兩個(gè)變量,這樣就會(huì)導(dǎo)致性能的下降。

而使用一個(gè)變量來(lái)記錄這兩個(gè)值,我們就可以使用原子操作來(lái)保護(hù)這個(gè)變量,這樣就可以保證并發(fā)讀寫(xiě)的安全性,同時(shí)也能得到更好的性能:

// WaitGroup 的 Add 函數(shù):高 32 位加上 delta
state := wg.state.Add(uint64(delta) << 32)

// WaitGroup 的 Wait 函數(shù):低 32 位加 1
// 等待者的數(shù)量加 1
wg.state.CompareAndSwap(state, state+1)

CAS 操作有失敗必然有成功

當(dāng)然這里是指指向同一行 CAS 代碼的時(shí)候(也就是有競(jìng)爭(zhēng)的時(shí)候),如果是指向不同行 CAS 代碼的時(shí)候,那么就不一定了。 比如下面這個(gè)例子,我們把前面計(jì)算 sum 的例子改一改,改成用 CAS 操作來(lái)完成:

func TestCas(t *testing.T) {
	var sum int32 = 0
	var wg sync.WaitGroup
	wg.Add(1000)

	for i := 0; i < 1000; i++ {
		go func() {
			defer wg.Done()
			// 這一行是有可能會(huì)失敗的
			atomic.CompareAndSwapInt32(&sum, sum, sum+1)
		}()
	}

	wg.Wait()
	fmt.Println(sum) // 不是 1000
}

在這個(gè)例子中,我們把 atomic.AddInt32(&sum, 1) 改成了 atomic.CompareAndSwapInt32(&sum, sum, sum+1), 這樣就會(huì)導(dǎo)致有可能會(huì)有多個(gè) goroutine 同時(shí)執(zhí)行到 atomic.CompareAndSwapInt32(&sum, sum, sum+1) 這一行代碼, 這樣肯定會(huì)有不同的 goroutine 同時(shí)拿到一個(gè)相同的 sum 的舊值,那么在這種情況下,就會(huì)導(dǎo)致 CAS 操作失敗。 也就是說(shuō),將 sum 替換為 sum + 1 的操作可能會(huì)失敗。

失敗意味著什么呢?意味著另外一個(gè)協(xié)程序先把 sum 的值加 1 了,這個(gè)時(shí)候其實(shí)我們不應(yīng)該在舊的 sum 上加 1 了, 而是應(yīng)該在最新的 sum 上加上 1,那我們應(yīng)該怎么做呢?我們可以在 CAS 操作失敗的時(shí)候,重新獲取 sum 的值, 然后再次嘗試 CAS 操作,直到成功為止:

func TestCas(t *testing.T) {
	var sum int32 = 0
	var wg sync.WaitGroup
	wg.Add(1000)

	for i := 0; i < 1000; i++ {
		go func() {
			defer wg.Done()
			
			// cas 失敗的時(shí)候,重新獲取 sum 的值進(jìn)行計(jì)算。
			// cas 成功則返回。
			for {
				if atomic.CompareAndSwapInt32(&sum, sum, sum+1) {
					return
				}
			}
		}()
	}

	wg.Wait()
	fmt.Println(sum)
}

總結(jié)

原子操作是并發(fā)編程中非常重要的一個(gè)概念,它可以保證并發(fā)讀寫(xiě)的安全性,同時(shí)也能得到更好的性能。

最后,總結(jié)一下本文講到的內(nèi)容:

  • 原子操作是更底層的操作,它保護(hù)的是單一變量,而互斥鎖可以保護(hù)一個(gè)程式碼片段,它們的使用場(chǎng)景是不一樣的。
  • 原子操作需要透過(guò) CPU 指令來(lái)實(shí)現(xiàn),而互斥鎖是在軟體層面實(shí)現(xiàn)的。
  • go 裡面的原子運(yùn)算有以下這些:
    • Add:原子增減
    • CompareAndSwap:原子比較並交換
    • Load:原子讀取
    • Store:原子寫(xiě)入
    • Swap:原子交換
  • go 裡面所有型別都能使用原子運(yùn)算,只是不同型別的原子運(yùn)算所使用的函數(shù)不太一樣。
  • atomic.Value 可以用來(lái)原子操作任意型別的變數(shù)。
  • go 裡面有些底層實(shí)作也使用了原子操作,像是:
    • sync.WaitGroup:使用原子運(yùn)算來(lái)確保計(jì)數(shù)器和等待者數(shù)量的並發(fā)讀寫(xiě)安全性。
    • sync.Mapentry 結(jié)構(gòu)體中基本上所有運(yùn)算都有原子運(yùn)算的身影。
  • 原子操作有失敗必然有成功(說(shuō)的是同一行CAS 操作),如果CAS 操作失敗了,那麼我們可以重新取得舊值,然後再次嘗試CAS 操作,直到成功為止。

總的來(lái)說(shuō),原子操作本身其實(shí)沒(méi)有太複雜的邏輯,我們理解了它的原理之後,就可以很容易的使用它了。

推薦學(xué)習(xí):Golang教學(xué)

以上是什麼是原子操作?深入淺析go中的原子操作的詳細(xì)內(nèi)容。更多資訊請(qǐng)關(guān)注PHP中文網(wǎng)其他相關(guān)文章!

本網(wǎng)站聲明
本文內(nèi)容由網(wǎng)友自願(yuàn)投稿,版權(quán)歸原作者所有。本站不承擔(dān)相應(yīng)的法律責(zé)任。如發(fā)現(xiàn)涉嫌抄襲或侵權(quán)的內(nèi)容,請(qǐng)聯(lián)絡(luò)admin@php.cn

熱AI工具

Undress AI Tool

Undress AI Tool

免費(fèi)脫衣圖片

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅(qū)動(dòng)的應(yīng)用程序,用於創(chuàng)建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線(xiàn)上人工智慧工具。

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費(fèi)的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費(fèi)的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強(qiáng)大的PHP整合開(kāi)發(fā)環(huán)境

Dreamweaver CS6

Dreamweaver CS6

視覺(jué)化網(wǎng)頁(yè)開(kāi)發(fā)工具

SublimeText3 Mac版

SublimeText3 Mac版

神級(jí)程式碼編輯軟體(SublimeText3)

熱門(mén)話(huà)題

Laravel 教程
1597
29
PHP教程
1488
72
Switch語(yǔ)句如何運(yùn)行? Switch語(yǔ)句如何運(yùn)行? Jul 30, 2025 am 05:11 AM

Go的switch語(yǔ)句默認(rèn)不會(huì)貫穿執(zhí)行,匹配到第一個(gè)條件後自動(dòng)退出。 1.switch以關(guān)鍵字開(kāi)始並可帶一個(gè)值或不帶值;2.case按順序從上到下匹配,僅運(yùn)行第一個(gè)匹配項(xiàng);3.可通過(guò)逗號(hào)列出多個(gè)條件來(lái)匹配同一case;4.不需要手動(dòng)添加break,但可用fallthrough強(qiáng)制貫穿;5.default用於未匹配到的情況,通常放最後。

使用上下文軟件包進(jìn)行取消和超時(shí) 使用上下文軟件包進(jìn)行取消和超時(shí) Jul 29, 2025 am 04:08 AM

USECONTEXTTOPROPAGATECELLATION ANDDEADEADLINESACROSSGOROUTINES,ENABLINGCOOPERATIVECELLATIONININHTTPSERVERS,背景任務(wù),andChainedCalls.2.withContext.withContext.withCancel(),CreatseAcancellableBableBablebableBableBableBablebableContExtandAndCandExtandCallCallCancelLcancel()

建立表演者為第三方API的客戶(hù) 建立表演者為第三方API的客戶(hù) Jul 30, 2025 am 01:09 AM

使用專(zhuān)用且配置合理的HTTP客戶(hù)端,設(shè)置超時(shí)和連接池以提升性能和資源利用率;2.實(shí)現(xiàn)帶指數(shù)退避和抖動(dòng)的重試機(jī)制,僅對(duì)5xx、網(wǎng)絡(luò)錯(cuò)誤和429狀態(tài)碼重試,並遵守Retry-After頭;3.對(duì)靜態(tài)數(shù)據(jù)如用戶(hù)信息使用緩存(如sync.Map或Redis),設(shè)置合理TTL,避免重複請(qǐng)求;4.使用信號(hào)量或rate.Limiter限制並發(fā)和請(qǐng)求速率,防止被限流或封禁;5.將API封裝為接口,便於測(cè)試、mock和添加日誌、追蹤等中間件;6.通過(guò)結(jié)構(gòu)化日誌和指標(biāo)監(jiān)控請(qǐng)求時(shí)長(zhǎng)、錯(cuò)誤率、狀態(tài)碼和重試次數(shù),結(jié)合Op

如何在Go中正確複製切片 如何在Go中正確複製切片 Jul 30, 2025 am 01:28 AM

要正確複製Go中的切片,必須創(chuàng)建新的底層數(shù)組,而不是直接賦值;1.使用make和copy函數(shù):dst:=make([]T,len(src));copy(dst,src);2.使用append與nil切片:dst:=append([]T(nil),src...);這兩種方法都能實(shí)現(xiàn)元素級(jí)別的複制,避免共享底層數(shù)組,確保修改互不影響,而直接賦值dst=src會(huì)導(dǎo)致兩者引用同一數(shù)組,不屬於真正複製。

如何將template.parsefs與GO嵌入? 如何將template.parsefs與GO嵌入? Jul 30, 2025 am 12:35 AM

使用template.ParseFS與embed包可將HTML模板編譯進(jìn)二進(jìn)製文件。 1.導(dǎo)入embed包並用//go:embedtemplates/.html將模板文件嵌入embed.FS變量;2.調(diào)用template.Must(template.ParseFS(templateFS,"templates/.html"))解析所有匹配的模板文件;3.在HTTP處理器中通過(guò)tmpl.ExecuteTemplate(w,"home.html",nil)渲染指定

符文是什麼? 符文是什麼? Jul 31, 2025 am 02:15 AM

Aruneingoisaunicodecodepointrepointreporentedasanint32,使用了tocortloctlyhandhandlenternationCharacters; 1. userunesInesinSteadofbyTestoavoidSplittingMulti-bydeunicodecharacters; 2. 2. loopoverstringswithrangetogetrogetogetogetrogeTringsWithRangetogetrounes,notbyters; 3.converteranemantermaneflymantofelymanteranemantermanterantoflyman [] []

與時(shí)間和日期一起工作 與時(shí)間和日期一起工作 Jul 30, 2025 am 02:51 AM

Go使用time.Time結(jié)構(gòu)體處理日期和時(shí)間,1.格式化和解析使用參考時(shí)間“2006-01-0215:04:05”對(duì)應(yīng)“MonJan215:04:05MST2006”,2.創(chuàng)建日期使用time.Date(year,month,day,hour,min,sec,nsec,loc)並指定時(shí)區(qū)如time.UTC,3.時(shí)區(qū)處理通過(guò)time.LoadLocation加載位置並用time.ParseInLocation解析帶時(shí)區(qū)的時(shí)間,4.時(shí)間運(yùn)算使用Add、AddDate和Sub方法進(jìn)行加減和計(jì)算間隔,

如何在GO中導(dǎo)入本地軟件包? 如何在GO中導(dǎo)入本地軟件包? Jul 30, 2025 am 04:47 AM

要正確導(dǎo)入本地包,需使用Go模塊並遵循目錄結(jié)構(gòu)與導(dǎo)入路徑匹配原則。 1.使用gomodinit初始化模塊,如gomodinitexample.com/myproject;2.將本地包放在子目錄中,如mypkg/utils.go,包聲明為packagemypkg;3.在main.go中通過(guò)完整模塊路徑導(dǎo)入,如import"example.com/myproject/mypkg";4.避免相對(duì)導(dǎo)入、路徑不匹配或命名衝突;5.對(duì)於模塊外的包可使用replace指令。只要確保模塊初始化

See all articles