欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang拾遺之實(shí)現(xiàn)一個(gè)不可復(fù)制類型詳解

 更新時(shí)間:2023年02月20日 14:28:12   作者:apocelipes  
在這篇文章中我們將實(shí)現(xiàn)一個(gè)無(wú)法被復(fù)制的類型,順便加深對(duì)引用類型、值傳遞以及指針的理解。文中的示例代碼講解詳細(xì),感興趣的可以了解一下

如何復(fù)制一個(gè)對(duì)象

不考慮IDE提供的代碼分析和go vet之類的靜態(tài)分析工具,golang里幾乎所有的類型都能被復(fù)制。

// 基本標(biāo)量類型和指針
var i int = 1
iCopy := i
str := "string"
strCopy := str
 
pointer := &i
pointerCopy := pointer
iCopy2 := *pointer // 解引用后進(jìn)行復(fù)制
 
// 結(jié)構(gòu)體和數(shù)組
arr := [...]int{1, 2, 3}
arrCopy := arr
 
type Obj struct {
    i int
}
obj := Obj{}
objCopy := obj

除了這些,golang還有函數(shù)和引用類型(slice、map、interface),這些類型也可以被復(fù)制,但稍有不同:

func f() {...}
f1 := f
f2 := f1
 
fmt.Println(f1, f2) // 0xabcdef 0xabcdef 打印出來(lái)的值是一樣的
fmt.Println(&f1 == &f2) // false 雖然值一樣,但確實(shí)是兩個(gè)不同的變量

這里并沒(méi)有真正復(fù)制處三份f的代碼,f1和f2均指向f,f的代碼始終只會(huì)有一份。map、slice和interface與之類似:

m := map[int]string{
    0: "a",
    1: "b",
}
mCopy := m // 兩者引用同樣的數(shù)據(jù)
mCopy[0] := "unknown"
m[0] == "unknown" // True
// slice的復(fù)制和map相同

interface是比較另類的,它的行為要分兩種情況:

s := "string"
var i1 any = s
var i2 any = s
// 當(dāng)把非指針和接口類型的值賦值給interface,會(huì)導(dǎo)致原來(lái)的對(duì)象被復(fù)制一份
 
s := "string"
var i1 any = s
var i2 any = i2
// 當(dāng)把接口賦值給接口,底層引用的數(shù)據(jù)不會(huì)被復(fù)制,i1會(huì)復(fù)制s,i2此時(shí)和i1共有一個(gè)s的副本
 
ss := "string but pass by pointer"
var i3 any = &ss
var i4 any = i3
// i3和i4均引用ss,此時(shí)ss沒(méi)有被復(fù)制,但指向ss的指針的值被復(fù)制了兩次

上面的結(jié)果會(huì)一定程度上被編譯優(yōu)化干擾,比如少數(shù)情況下編譯器可以確認(rèn)賦值給接口的值從來(lái)沒(méi)被修改并且生命周期不比源對(duì)象長(zhǎng),則可能不會(huì)進(jìn)行復(fù)制。

所以這里有個(gè)小提示:如果要賦值給接口的數(shù)據(jù)比較大,那么最好以指針的形式賦值給接口,復(fù)制指針比復(fù)制大量的數(shù)據(jù)更高效。

為什么要禁止復(fù)制

從上一節(jié)可以看到,允許復(fù)制時(shí)會(huì)在某些情況下“闖禍”。比如:

1.淺拷貝的問(wèn)題很容易出現(xiàn),比如例子里的map和slice的淺拷貝問(wèn)題,這可能會(huì)導(dǎo)致數(shù)據(jù)被意外修改

2.意外復(fù)制了大量數(shù)據(jù),導(dǎo)致性能問(wèn)題

3.在需要共享狀態(tài)的地方錯(cuò)誤的使用了副本,導(dǎo)致?tīng)顟B(tài)不一致從而產(chǎn)生嚴(yán)重問(wèn)題,比如sync.Mutex,復(fù)制一個(gè)鎖并使用其副本會(huì)導(dǎo)致死鎖

4.根據(jù)業(yè)務(wù)或者其他需求,某類型的對(duì)象只允許存在一個(gè)實(shí)例,這時(shí)復(fù)制顯然是被禁止的

顯然在一些情況下禁止復(fù)制是合情合理的,這也是為什么我會(huì)寫這篇文章。

但具體情況具體分析,不是說(shuō)復(fù)制就是萬(wàn)惡之源,什么時(shí)候該支持復(fù)制,什么時(shí)候應(yīng)該禁止,應(yīng)該結(jié)合自己的實(shí)際情況。

運(yùn)行時(shí)檢測(cè)實(shí)現(xiàn)禁止復(fù)制

想在別的語(yǔ)言中禁止某個(gè)類型被復(fù)制,方法有很多,用c++舉一例:

struct NoCopy {
    NoCopy(const NoCopy &) = delete;
    NoCopy &operator=(const NoCopy &) = delete;
};

可惜在golang里不支持這么做。

另外,因?yàn)間olang沒(méi)有運(yùn)算符重載,所以很難在賦值的階段就進(jìn)行攔截,所以我們的側(cè)重點(diǎn)在于“復(fù)制之后可以盡快檢測(cè)到”。

所以我們先實(shí)現(xiàn)在對(duì)象被復(fù)制后報(bào)錯(cuò)的功能。雖然不如c++編譯期就可以禁止復(fù)制那樣優(yōu)雅,但也算實(shí)現(xiàn)了功能,至少不什么都沒(méi)有要強(qiáng)一些。

初步嘗試

那么如何直到對(duì)象是否被復(fù)制了?很簡(jiǎn)單,看它的地址就行了,地址一樣那必然是同一個(gè)對(duì)象,不一樣了那說(shuō)明復(fù)制出一個(gè)新的對(duì)象了。

順著這個(gè)思路,我們需要一個(gè)機(jī)制來(lái)保存對(duì)象第一次創(chuàng)建時(shí)的地址,并在后續(xù)進(jìn)行比較,于是第一版代碼誕生了:

import "unsafe"
 
type noCopy struct {
    p uintptr
}
 
func (nc *noCopy) check() {
    if uintptr(unsafe.Pointer(nc)) != nc.p {
        panic("copied")
    }
}

邏輯比較清晰,每次調(diào)用check來(lái)檢查當(dāng)前的調(diào)用者的地址和保存地址是否相同,如果不同就panic。

為什么沒(méi)有創(chuàng)建這個(gè)類型的方法?因?yàn)槲覀儧](méi)法得知自己被其他類型創(chuàng)建時(shí)的地址,所以這塊得讓其他使用noCopy的類型代勞。

使用的時(shí)候需要把noCopy嵌入自己的struct,注意不能以指針的形式嵌入:

type SomethingCannotCopy struct {
    noCopy
    ...
}
 
func (s *SomethingCannotCopy) DoWork() {
    s.check()
    fmt.Println("do something")
}
 
func NewSomethingCannotCopy() *SomethingCannotCopy {
    s := &SomethingCannotCopy{
        // 一些初始化
    }
    // 綁定地址
    s.noCopy.p = unsafe.Pointer(&s.noCopy)
    return s
}

注意初始化部分的代碼,在這里我們需要把noCopy對(duì)象的地址綁定進(jìn)去?,F(xiàn)在可以實(shí)現(xiàn)運(yùn)行時(shí)檢測(cè)了:

func main() {
    s1 := NewSomethingCannotCopy()
    pointer := s1
    s1Copy := *s1 // 這里實(shí)際上進(jìn)行了復(fù)制,但需要調(diào)用方法的時(shí)候才能檢測(cè)到
    pointer.DoWork() // 正常打印出信息
    s1Copy.DoWork() // panic
}

解釋下原理:當(dāng)SomethingCannotCopy被復(fù)制的時(shí)候,noCopy也會(huì)被復(fù)制,因此復(fù)制出來(lái)的noCopy的地址和原先的那個(gè)是不一樣的,但他們內(nèi)部記錄的p是一樣的,這樣當(dāng)被復(fù)制出來(lái)的noCopy對(duì)象調(diào)用check方法的時(shí)候就會(huì)觸發(fā)panic。這也是為什么不要用指針形式嵌入它的原因。

功能實(shí)現(xiàn)了,但代碼實(shí)在是太丑,而且耦合嚴(yán)重:只要用了noCopy,就必須在創(chuàng)建對(duì)象的同時(shí)初始化noCopy的實(shí)例,noCopy的初始化邏輯會(huì)侵入到其他對(duì)象的初始化邏輯中,這樣的設(shè)計(jì)是不能接受的。

更好的實(shí)現(xiàn)

那么有沒(méi)有更好的實(shí)現(xiàn)?答案是有的,而且在標(biāo)準(zhǔn)庫(kù)里。

標(biāo)準(zhǔn)庫(kù)的信號(hào)量sync.Cond是禁止復(fù)制的,而且比Mutex更為嚴(yán)格,因?yàn)閺?fù)制它比復(fù)制鎖更容易導(dǎo)致死鎖和崩潰,所以標(biāo)準(zhǔn)庫(kù)加上了運(yùn)行時(shí)的動(dòng)態(tài)檢查。

主要代碼如下:

type Cond struct {
    // L is held while observing or changing the condition
    L Locker
    ...
    // 復(fù)制檢查
    checker copyChecker
}
 
// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
        return &Cond{L: l}
}
 
func (c *Cond) Signal() {
    // 檢查自己是否被復(fù)制
    c.checker.check()
    runtime_notifyListNotifyOne(&c.notify)
}

checker實(shí)現(xiàn)了運(yùn)行時(shí)檢測(cè)是否被復(fù)制,但初始化的時(shí)候并不需要特殊處理這個(gè)checker,這是用了什么手法做到的呢?

看代碼:

type copyChecker uintptr
 
func (c *copyChecker) check() {
    if uintptr(*c) != uintptr(unsafe.Pointer(c)) && // step 1
            !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) && // step 2
            uintptr(*c) != uintptr(unsafe.Pointer(c)) { //step 3
        panic("sync.Cond is copied")
    }
}

看著很復(fù)雜,連原子操作都來(lái)了,這都是啥啊。但別怕,我給你捋一捋就明白了。

首先是checker初始化之后第一次調(diào)用:

  • 當(dāng)check第一次被調(diào)用,c的值肯定是0,而這時(shí)候c是有真實(shí)的地址的,所以step 1失敗,進(jìn)入step 2;
  • 用原子操作把c的值設(shè)置成自己的地址值,注意只有c的值是0的時(shí)候才能完成設(shè)置,因?yàn)檫@里c的值是0,所以交換成功,step 2是False,判斷流程直接結(jié)束;
  • 因?yàn)椴慌懦€有別的goroutine拿著這個(gè)checker在做檢測(cè),所以step 2是會(huì)失敗的,這是要進(jìn)入step 3;
  • step 3再次比較c的值和它自己的地址是否相同,相同說(shuō)明多個(gè)goroutine共用了一個(gè)checker,沒(méi)有發(fā)生復(fù)制,所以檢測(cè)通過(guò)不會(huì)panic。
  • 如果step 3的比較發(fā)現(xiàn)不相等,那么說(shuō)明被復(fù)制了,直接panic

然后我們?cè)倏雌渌闆r下checker的流程:

  • 這時(shí)候c的值不是0,如果沒(méi)發(fā)生復(fù)制,那么step 1的結(jié)果是False,判斷流程結(jié)束,不會(huì)panic;
  • 如果c的值和自己的地址不一樣,會(huì)進(jìn)入step 2,因?yàn)檫@里c的值不為0,所以表達(dá)式結(jié)果一定是True,所以進(jìn)入step 3;
  • step 3和step 1一樣,結(jié)果是True,地址不同說(shuō)明被復(fù)制,這時(shí)候if里面的語(yǔ)句會(huì)執(zhí)行,因此panic。

搞得這么麻煩,其實(shí)就是為了能干干凈凈地初始化。這樣任何類型都只需要帶上checker作為自己的字段就行,不用關(guān)心它是這么初始化的。

還有個(gè)小問(wèn)題,為什么設(shè)置checker的值需要原子操作,但讀取就不用呢?

因?yàn)樽x取一個(gè)uintptr的值,在現(xiàn)代的x86和arm處理器上只要一個(gè)指令,所以要么讀到過(guò)時(shí)的值要么讀到最新的值,不會(huì)讀到錯(cuò)誤的或者寫了一半的不完整的值,對(duì)于讀到舊值的情況(主要出現(xiàn)在第一次調(diào)用check的時(shí)候),還有step 3做進(jìn)一步的檢查,因此不會(huì)影響整個(gè)檢測(cè)邏輯。而“比較并交換”顯然一條指令做不完,如果在中間步驟被打斷那么整個(gè)操作的結(jié)果很可能就是錯(cuò)的,從而影響整個(gè)檢測(cè)邏輯,所以必須要用原子操作才行。

那么在讀取的時(shí)候也使用atomic.Load行嗎?當(dāng)然行,但一是這么做仍然避免不了step 3的檢測(cè),可以思考下是為什么;二是原子操作相比直接讀取會(huì)帶來(lái)性能損失,在這里不使用原子操作也能保證正確性的情況下這是得不償失的。

性能

因?yàn)槭沁\(yùn)行時(shí)檢測(cè),所以我們得看看會(huì)對(duì)性能帶來(lái)多少影響。我們使用改進(jìn)版的checker。

type CheckBench struct {
    num uint64
    checker copyChecker
}
 
func (c *CheckBench) CheckCopy() {
    c.checker.check()
    c.num++
}
 
// 不進(jìn)行檢測(cè)
func (c *CheckBench) NoCheck() {
    c.num++
}
 
func BenchmarkCheckBench_NoCheck(b *testing.B) {
    c := CheckBench{}
    for i := 0; i < b.N; i++ {
        for j := 0; j < 50; j++ {
            c.NoCheck()
        }
    }
}
 
func BenchmarkCheckBench_WithCheck(b *testing.B) {
    c := CheckBench{}
    for i := 0; i < b.N; i++ {
        for j := 0; j < 50; j++ {
            c.CheckCopy()
        }
    }
}

測(cè)試結(jié)果如下:

cpu: Intel(R) Core(TM) i5-10200H CPU @ 2.40GHz
BenchmarkCheckBench_NoCheck-8           17689137                68.36 ns/op
BenchmarkCheckBench_WithCheck-8         17563833                66.04 ns/op

幾乎可以忽略不計(jì),因?yàn)槲覀冞@里沒(méi)有發(fā)生復(fù)制,所以幾乎每次檢測(cè)都是通過(guò)的,這對(duì)cpu的分支預(yù)測(cè)非常友好,所以性能損耗幾乎可以忽略。

所以我們給cpu添點(diǎn)堵,讓分支預(yù)測(cè)沒(méi)那么容易:

func BenchmarkCheckBench_WithCheck(b *testing.B) {
    for i := 0; i < b.N; i++ {
        c := &CheckBench{}
        for j := 0; j < 50; j++ {
            c.CheckCopy()
        }
    }
}
 
func BenchmarkCheckBench_NoCheck(b *testing.B) {
    for i := 0; i < b.N; i++ {
        c := &CheckBench{}
        for j := 0; j < 50; j++ {
            c.NoCheck()
        }
    }
}

現(xiàn)在分支預(yù)測(cè)沒(méi)那么容易了而且要多付出初始化時(shí)使用atomic的代價(jià),測(cè)試結(jié)果會(huì)變成這樣:

cpu: Intel(R) Core(TM) i5-10200H CPU @ 2.40GHz
BenchmarkCheckBench_WithCheck-8         15552717                74.84 ns/op
BenchmarkCheckBench_NoCheck-8           26441635                44.74 ns/op

差不多會(huì)慢40%。當(dāng)然,實(shí)際的代碼不會(huì)有這么極端,所以最壞可能也只會(huì)產(chǎn)生20%的影響,通常不太會(huì)成為性能瓶頸,運(yùn)行時(shí)檢測(cè)是否有影響還需結(jié)核profile。

優(yōu)點(diǎn)和缺點(diǎn)

優(yōu)點(diǎn):

  • 只要調(diào)用check,肯定能檢查出是否被復(fù)制
  • 簡(jiǎn)單

缺點(diǎn):

  • 所有的方法里都需要調(diào)用check,新加方法忘了調(diào)用的話就無(wú)法檢測(cè)
  • 只能在被復(fù)制出來(lái)的新對(duì)象那檢測(cè)到復(fù)制操作,原先那個(gè)對(duì)象上check始終是沒(méi)問(wèn)題的,這樣不是嚴(yán)格禁止了復(fù)制,但大多數(shù)時(shí)間沒(méi)問(wèn)題,可以接受
  • 如果只復(fù)制了對(duì)象沒(méi)調(diào)用任何對(duì)象上的方法,也無(wú)法檢測(cè)到復(fù)制,這種情況比較少見(jiàn)
  • 有潛在性能損耗,雖然很多時(shí)候可以得到充分優(yōu)化損耗沒(méi)那么夸張

靜態(tài)檢測(cè)實(shí)現(xiàn)禁止復(fù)制

動(dòng)態(tài)檢測(cè)的缺點(diǎn)不少,能不能像c++那樣編譯期就禁止復(fù)制呢?

利用Locker接口不可復(fù)制實(shí)現(xiàn)靜態(tài)檢測(cè)

也可以,但得配合靜態(tài)代碼檢測(cè)工具,比如自帶的go vet。看下代碼:

// 實(shí)現(xiàn)sync.Locker接口
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
 
type SomethingCannotCopy struct {
    noCopy
}

這樣就行了,不需要再添加其他的代碼。解釋下原理:任何實(shí)現(xiàn)了sync.Locker的類型都不應(yīng)該被拷貝,靜態(tài)代碼檢測(cè)會(huì)檢測(cè)出這些情況并報(bào)錯(cuò)。

所以類似下邊的代碼都是無(wú)法通過(guò)靜態(tài)代碼檢測(cè)的:

func f(s SomethingCannotCopy) {
    // 報(bào)錯(cuò),因?yàn)閰?shù)會(huì)導(dǎo)致復(fù)制
    // 返回SomethingCannotCopy也是不行的
}
 
func (s SomethingCannotCopy) Method() {
    // 報(bào)錯(cuò),因?yàn)榉侵羔橆愋徒邮掌鲿?huì)導(dǎo)致復(fù)制
}
 
func main() {
    s := SomethingCannotCopy{}
    sCopy := s // 報(bào)錯(cuò)
    sInterface := any(s) // 報(bào)錯(cuò)
    sPointer := &s // OK
    sCopy2 := *sPointer // 報(bào)錯(cuò)
    sInterface2 := any(sPointer) // OK
    sCopy3 := *(sInterface2.(*SomethingCannotCopy)) // 報(bào)錯(cuò)
}

基本上涵蓋了所以會(huì)產(chǎn)生復(fù)制操作的地方,基本能在編譯期完成檢測(cè)。

如果跳過(guò)go vet,直接使用go run或者go build,那么上面的代碼可以正常編譯并運(yùn)行。

優(yōu)點(diǎn)和缺點(diǎn)

因?yàn)橹挥徐o態(tài)檢測(cè),因此沒(méi)有什么運(yùn)行時(shí)開(kāi)銷,所以性能這節(jié)就不需要費(fèi)筆墨了。主要來(lái)看下這種方案的優(yōu)缺點(diǎn)。

優(yōu)點(diǎn):

  • 實(shí)現(xiàn)非常簡(jiǎn)單,代碼很簡(jiǎn)練,基本無(wú)侵入性
  • 依賴靜態(tài)檢測(cè),不影響運(yùn)行時(shí)性能
  • golang自帶檢測(cè)工具:go vet
  • 可檢測(cè)到的case比運(yùn)行時(shí)檢測(cè)多

缺點(diǎn):

  • 最大的缺點(diǎn),盡管靜態(tài)檢測(cè)會(huì)報(bào)錯(cuò),但仍然可以正常編譯執(zhí)行
  • 不是每個(gè)測(cè)試環(huán)境和CI都配備了靜態(tài)檢測(cè),所以很難強(qiáng)制保證類型沒(méi)有被復(fù)制
  • 會(huì)導(dǎo)致類型實(shí)現(xiàn)sync.Locker,然而很多時(shí)候我們的類型并不是類似鎖的資源,使用這個(gè)接口只是為了靜態(tài)檢測(cè),這會(huì)帶來(lái)代碼被誤用的風(fēng)險(xiǎn)

標(biāo)準(zhǔn)庫(kù)也使用的這套方案,建議仔細(xì)閱讀這個(gè)issue里的討論。

更進(jìn)一步

看過(guò)運(yùn)行時(shí)檢測(cè)和靜態(tài)檢測(cè)兩種方案之后,我們會(huì)發(fā)現(xiàn)這些做法多少都有些問(wèn)題,不盡如人意。

所以我們還是要追求一種更好用的,更符合golang風(fēng)格的做法。幸運(yùn)的是,這樣的做法是存在的。

利用package和interface進(jìn)行封裝

首先我們創(chuàng)建一個(gè)worker包,里面定義一個(gè)Worker接口,包中的數(shù)據(jù)對(duì)外以Worker接口的形式提供:

package worker
 
import (
    "fmt"
)
 
// 對(duì)外只提供接口來(lái)訪問(wèn)數(shù)據(jù)
type Worker interface {
    Work()
}
 
// 內(nèi)部類型不導(dǎo)出,以接口的形式供外部使用
type normalWorker struct {
    // data members
}
func (*normalWorker) Work() {
    fmt.Println("I am a normal worker.")
}
func NewNormalWorker() Worker {
    return &normalWorker{}
}
 
type specialWorker struct {
    // data members
}
func (*specialWorker) Work() {
    fmt.Println("I am a special worker.")
}
func NewSpecialWorker() Worker {
    return &specialWorker{}
}

worker包對(duì)外只提供Worker接口,用戶可以使用NewNormalWorker和NewSpecialWorker來(lái)生成不同種類的worker,用戶不需要關(guān)心具體的返回類型,只要使用得到的Worker接口即可。

這么做的話,在worker包之外是看不到normalWorker和specialWorker這兩個(gè)類型的,所以沒(méi)法靠反射和類型斷言取出接口引用的數(shù)據(jù);因?yàn)槲覀儌鹘o接口的是指針,因此源數(shù)據(jù)不會(huì)被復(fù)制;同時(shí)我們?cè)诘谝还?jié)提到過(guò),把一個(gè)接口賦值給另一個(gè)接口(worker包之外你只能這么做),底層被引用的數(shù)據(jù)不會(huì)被復(fù)制,因此在包外始終不會(huì)在這兩個(gè)類型上產(chǎn)生復(fù)制的行為。

因此下面這樣的代碼是不可能通過(guò)編譯的:

func main() {
    w := worker.NewSpecialWorker()
    // worker.specialWorker 在worker包以外不可見(jiàn),因此編譯錯(cuò)誤
    wCopy := *(w.(*worker.specialWorker))
    wCopy.Work()
}

優(yōu)點(diǎn)和缺點(diǎn)

這樣就實(shí)現(xiàn)了worker包之外的禁止復(fù)制,下面來(lái)看看優(yōu)缺點(diǎn)。

優(yōu)點(diǎn):

  • 不需要額外的靜態(tài)檢查工具在編譯代碼前執(zhí)行檢查
  • 不需要運(yùn)行時(shí)動(dòng)態(tài)檢測(cè)是否被復(fù)制
  • 不會(huì)實(shí)現(xiàn)自己不需要的接口類型導(dǎo)致污染方法集
  • 符合golang開(kāi)發(fā)中的習(xí)慣做法

缺點(diǎn):

  • 并沒(méi)有讓類型本身不可復(fù)制,而是靠封裝屏蔽了大部分可能導(dǎo)致復(fù)制的情況
  • 這些worker類型在包內(nèi)是可見(jiàn)的,如果在包內(nèi)修改代碼時(shí)不注意可能會(huì)導(dǎo)致復(fù)制這些類型的值,所以要么包內(nèi)也都用Woker接口,要么參考上一節(jié)添加靜態(tài)檢查
  • 有些場(chǎng)景下不需要接口或者因?yàn)樾阅芤罂量潭褂貌涣私涌?,這種做法就行不通了,比如標(biāo)準(zhǔn)庫(kù)sync里的類型為了性能大部分都是暴露出來(lái)給外部直接使用的

綜合來(lái)說(shuō),這種方案是實(shí)現(xiàn)成本最低的。

總結(jié)

現(xiàn)在我們有三種方式防止我們的類型被復(fù)制:

  • 運(yùn)行時(shí)檢測(cè)
  • 靜態(tài)代碼檢測(cè)
  • 通過(guò)接口封裝避免暴露類型,從而避免被復(fù)制

一共三種方案,選擇困難癥仿佛要發(fā)作了。別著急,我們一起看看標(biāo)準(zhǔn)庫(kù)是怎么做的:

  • 標(biāo)準(zhǔn)庫(kù)的sync.Cond同時(shí)使用了方案一和方案二,因?yàn)樵O(shè)計(jì)者確實(shí)很不希望條件變量被復(fù)制
  • sync.Mutex、sync.Pool和sync.WaitGroup使用了方案二,需要配合go vet
  • 方案三在標(biāo)準(zhǔn)庫(kù)中應(yīng)用最廣泛,然而多數(shù)是處于設(shè)計(jì)和封裝的考慮,并不是為了禁止copy,但復(fù)制crypto包下的那些Hash和Cipher確實(shí)沒(méi)什么意義會(huì)帶來(lái)誤用,正好借著方案三避免了這些問(wèn)題

綜合來(lái)看首選的應(yīng)該是方案三;但也有需要使用方案二的時(shí)候,比如sync包中的那些同步機(jī)構(gòu);使用最少的是方案一,盡可能地不要設(shè)計(jì)出類似的代碼。

還有一點(diǎn)需要注意,如果你的類型里有字段是sync.Pool、sync.WaitGroup、sync.RWMutex、sync.Mutex、sync.Cond、sync.Map或sync.Once,那么這個(gè)類型本身也是不可復(fù)制的,也不需要額外實(shí)現(xiàn)禁止復(fù)制的功能,因?yàn)槟切┳侄巫詭Я恕?/p>

最后,我只想說(shuō)golang的語(yǔ)言技能實(shí)在是太簡(jiǎn)陋了,想只依賴語(yǔ)言特性實(shí)現(xiàn)禁止復(fù)制的功能不太現(xiàn)實(shí),更多的還是需要靠“設(shè)計(jì)”。

以上就是Golang拾遺之實(shí)現(xiàn)一個(gè)不可復(fù)制類型詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang不可復(fù)制類型的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • GoFrame?gtree樹(shù)形結(jié)構(gòu)的使用技巧示例

    GoFrame?gtree樹(shù)形結(jié)構(gòu)的使用技巧示例

    這篇文章主要為大家介紹了GoFrame?gtree樹(shù)形結(jié)構(gòu)的使用技巧示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • go使用SQLX操作MySQL數(shù)據(jù)庫(kù)的教程詳解

    go使用SQLX操作MySQL數(shù)據(jù)庫(kù)的教程詳解

    sqlx 是 Go 語(yǔ)言中一個(gè)流行的操作數(shù)據(jù)庫(kù)的第三方包,它提供了對(duì) Go 標(biāo)準(zhǔn)庫(kù) database/sql 的擴(kuò)展,簡(jiǎn)化了操作數(shù)據(jù)庫(kù)的步驟,下面我們就來(lái)學(xué)習(xí)一下go如何使用SQLX實(shí)現(xiàn)MySQL數(shù)據(jù)庫(kù)的一些基本操作吧
    2023-11-11
  • go將request?body綁定到不同的結(jié)構(gòu)體中教程

    go將request?body綁定到不同的結(jié)構(gòu)體中教程

    這篇文章主要為大家介紹了go將request?body綁定到不同的結(jié)構(gòu)體中教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-10-10
  • 詳解Go使用Viper和YAML管理配置文件

    詳解Go使用Viper和YAML管理配置文件

    在軟件開(kāi)發(fā)中,配置管理是一項(xiàng)基本但至關(guān)重要的任務(wù),它涉及到如何有效地管理應(yīng)用程序的配置變量,本文將探討如何使用Viper庫(kù)配合YAML配置文件來(lái)實(shí)現(xiàn)高效的配置管理,感興趣的可以了解下
    2024-04-04
  • golang的HTTP基本認(rèn)證機(jī)制實(shí)例詳解

    golang的HTTP基本認(rèn)證機(jī)制實(shí)例詳解

    這篇文章主要介紹了golang的HTTP基本認(rèn)證機(jī)制,結(jié)合實(shí)例形式較為詳細(xì)的分析了HTTP請(qǐng)求響應(yīng)的過(guò)程及認(rèn)證機(jī)制實(shí)現(xiàn)技巧,需要的朋友可以參考下
    2016-07-07
  • golang利用redis和gin實(shí)現(xiàn)保存登錄狀態(tài)校驗(yàn)登錄功能

    golang利用redis和gin實(shí)現(xiàn)保存登錄狀態(tài)校驗(yàn)登錄功能

    這篇文章主要介紹了golang利用redis和gin實(shí)現(xiàn)保存登錄狀態(tài)校驗(yàn)登錄功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2024-01-01
  • golang使用OpenTelemetry實(shí)現(xiàn)跨服務(wù)全鏈路追蹤詳解

    golang使用OpenTelemetry實(shí)現(xiàn)跨服務(wù)全鏈路追蹤詳解

    這篇文章主要為大家介紹了golang使用OpenTelemetry實(shí)現(xiàn)跨服務(wù)全鏈路追蹤詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • golang獲取用戶輸入的幾種方式

    golang獲取用戶輸入的幾種方式

    這篇文章給大家介紹了golang獲取用戶輸入的幾種方式,文中通過(guò)代碼示例給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友跟著小編一起來(lái)學(xué)習(xí)吧
    2024-01-01
  • GoLang strings.Builder底層實(shí)現(xiàn)方法詳解

    GoLang strings.Builder底層實(shí)現(xiàn)方法詳解

    自從學(xué)習(xí)go一個(gè)月以來(lái),我多少使用了一下strings.Builder,略有心得。你也許知道它,特別是你了解bytes.Buffer的話。所以我在此分享一下我的心得,并希望能對(duì)你有所幫助
    2022-10-10
  • go-zero自定義中間件的幾種方式

    go-zero自定義中間件的幾種方式

    首先 go-zero 已經(jīng)為我們提供了很多的中間件的實(shí)現(xiàn),但有時(shí)難免有需求需要自定義,這里介紹幾種自定義的方法,文中通過(guò)代碼示例講解的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下
    2024-07-07

最新評(píng)論