最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

淺析Golang如何利用泛型編寫更安全的代碼

 更新時(shí)間:2025年12月03日 09:59:01   作者:apocelipes  
從Go 1.18正式引入泛型,再到Go 1.21大量泛型函數(shù)/類型進(jìn)入標(biāo)準(zhǔn)庫開始已經(jīng)過去了三年,這篇文章要說的是泛型在強(qiáng)化代碼安全性和健壯性方面的應(yīng)用,感興趣的可以了解下

從Go 1.18正式引入泛型,再到Go 1.21大量泛型函數(shù)/類型進(jìn)入標(biāo)準(zhǔn)庫開始已經(jīng)過去了三年。盡管有著不支持類型特化、不支持泛型方法、實(shí)現(xiàn)方式有少量運(yùn)行時(shí)開銷、使用指針類型時(shí)不夠直觀等限制,泛型編程還是在golang社區(qū)和各種項(xiàng)目中遍地開花甚至碩果累累了。

不過也因?yàn)榉盒凸δ苌系姆N種限制,大多數(shù)代碼中對其的應(yīng)用仍然只停留在最基本的層面——僅僅減少重復(fù)代碼上。但golang泛型的威力遠(yuǎn)不止如此,即使不能進(jìn)行復(fù)雜的類型編程,泛型也可以讓你的代碼變得更安全、更健壯。

這篇文章要說的是泛型在強(qiáng)化代碼安全性和健壯性方面的應(yīng)用。

強(qiáng)化代碼類型安全

第一個(gè)應(yīng)用是強(qiáng)化類型安全,讓類型錯(cuò)誤盡可能在編譯階段就全部暴露出來。

我手上正好有這樣一個(gè)系統(tǒng),系統(tǒng)里有A、B、C三種不同類型的消息,我們的系統(tǒng)只接收C類型的消息,也只發(fā)送A或者B類型的消息。每種消息都實(shí)現(xiàn)了自己的序列化方法,當(dāng)然為了例子足夠簡潔,這里我做了很大的簡化:

type A struct {
	ID   uint64
	Name string
}

func (a *A) Encode() string {
	return fmt.Sprintf("A: %#v", a)
}

type B struct {
	Name      string
	Age       uint32
	CompanyID uint32
}

func (b *B) Encode() string {
	return fmt.Sprintf("B: %#v", b)
}

type C struct {
	RequestID string
	Name      string
}

func (c *C) Encode() string {
	return fmt.Sprintf("C: %#v", c)
}

如果意外發(fā)送了C類型的消息,其他的服務(wù)會(huì)出現(xiàn)錯(cuò)誤。

A和B類型的消息只是字段不太一樣,發(fā)送的邏輯是完全相同的,所以很自然我們?yōu)榱薉RY原則會(huì)寫出下面這樣的代碼:

type Encoder interface {
    Encode() string
}

func SendMessage(msg Encoder) {
    fmt.Println(msg.Encode())
    // 其他一些發(fā)送數(shù)據(jù)和校驗(yàn)的邏輯
}

這是最自然不過的,既然邏輯都一樣,而且A和B的操作確實(shí)有一定關(guān)聯(lián)性,那么我們就沒必要把發(fā)送代碼寫兩遍,定義一個(gè)能同時(shí)容納A和B的接口,再把接口作為SendMessage的參數(shù)類型即可。

這樣的代碼其實(shí)是很不安全的,因?yàn)镃也實(shí)現(xiàn)了Encoder接口,所以函數(shù)可以錯(cuò)誤地發(fā)送C導(dǎo)致整個(gè)系統(tǒng)崩潰。

作為泛型時(shí)代之前的解決辦法,我們只能在函數(shù)中加上類型斷言或者type switch,但這會(huì)帶來不小的運(yùn)行時(shí)開銷,同時(shí)也不能避免代碼被誤用,根本原因在于我們不能控制接口被哪些類型實(shí)現(xiàn),因此無法避免一個(gè)我們不期望的類型被作為參數(shù)傳入。

有了泛型情況就不一樣了,我們現(xiàn)在可以在編譯階段就檢查出所有誤用并且?guī)缀醪恍枰Ц哆\(yùn)行時(shí)開銷。

然而想實(shí)現(xiàn)這個(gè)效果會(huì)很難,你可能會(huì)寫出這樣的代碼:

func SendMessage[T A | B](msg *T) {
	fmt.Println(msg.Encode())
}

遺憾的是這樣的代碼會(huì)收獲編譯錯(cuò)誤:msg.Encode undefined (type *T is pointer to type parameter, not type parameter)。這是個(gè)常見錯(cuò)誤了,直接取泛型變量的指針大多數(shù)時(shí)候都會(huì)報(bào)這種錯(cuò),我以前的博客里有解釋過原因,這里不再贅述。

你也許會(huì)靈機(jī)一動(dòng),直接讓T本身是指針類型不就行了嗎:

- func SendMessage[T A | B](msg *T) {
+ func SendMessage[T *A | *B](msg T) {
    fmt.Println(msg.Encode())
}

這回確實(shí)有變化,只不過是報(bào)錯(cuò)信息變了:msg.Encode undefined (type T has no field or method Encode)

這是因?yàn)間olang規(guī)定如果泛型的類型約束是具體的類型,那么允許在泛型對象上執(zhí)行的只有內(nèi)置的那些加減乘除以及==、a[123]這樣的操作,并且多個(gè)類型之間允許的操作會(huì)取交集。很遺憾,方法調(diào)用并不在允許的范圍內(nèi)。對于寫慣了其他語言中泛型代碼的開發(fā)者來說,go的這類限制多少有點(diǎn)自廢武功的意味。

好消息是稍微繞一條路,我們也可以達(dá)成相同的效果:

type SendAble[T A | B] interface {
	*T
	Encoder
}

func SendMessage[T A | B, PT SendAble[T]](msg *T) {
	ptrMsg := PT(msg)
	fmt.Println(ptrMsg.Encode())
}

通過引入新的類型約束SendAble,我們可以限制參數(shù)的類型了。SendAble中的*T表示被約束的類型只能是T的指針,而我們限制了T只能是A或者B;第二行則包含了Encoder的方法,這要求這個(gè)指針類型也必須實(shí)現(xiàn)了這些方法。新代碼中的ptrMsg := PT(msg)則把指針類型轉(zhuǎn)換成了另一個(gè)類型參數(shù)PT,PT擁有Encode方法因此可以正常調(diào)用,而且編譯器在類型推導(dǎo)中不會(huì)把*T當(dāng)成類型參數(shù)的指針,而是實(shí)際的類型T的指針,這也避免了最初一版代碼的報(bào)錯(cuò)。

這個(gè)模式雖然有些繞,但形式相當(dāng)固定,因此很容易掌握,你可以當(dāng)成一些golang的慣用法來看待?,F(xiàn)在如果我們傳遞了C類型的變量到函數(shù)中,編譯器會(huì)報(bào)錯(cuò):C does not satisfy A | B (C missing in main.A | main.B)。錯(cuò)誤描述還是多少有點(diǎn)不盡人意,但總比運(yùn)行時(shí)出問題要好得多。

除了代碼稍微復(fù)雜了一些,這段代碼本質(zhì)上是調(diào)用了泛型的接口,雖然編譯器做了很多優(yōu)化,但難免還是會(huì)因?yàn)間olang選擇的泛型實(shí)現(xiàn)方式導(dǎo)致一點(diǎn)點(diǎn)的性能下降。不過比起類型斷言來說,這點(diǎn)下降影響往往沒有前者那么大。

這只是使用泛型保護(hù)類型安全的一個(gè)比較常見也比較簡單的例子,充分利用泛型特性可以在保證代碼簡潔的同時(shí)讓代碼更安全。

保證常量安全

golang中的常量很簡單,類型只能是整數(shù)、浮點(diǎn)、字符串或者以這些為底層類型的自定義類型。

對于1、2、3、4、5這樣的數(shù)字常量,golang默認(rèn)都是int類型。大多數(shù)時(shí)候這都是我們希望的,然而有時(shí)候也會(huì)帶來煩惱:

func handleOdd(n int)
func handleEven(n int)

假設(shè)我們有兩個(gè)分別處理奇偶數(shù)的函數(shù)handleOddhandleEven,函數(shù)參數(shù)類型自然只能是int,但int的取值實(shí)在是太寬泛了,對于我們的函數(shù)來說里面有接近二分之一的值是不可接受的。

然而除了運(yùn)行時(shí)檢查參數(shù)之外,我們并沒有其他的手段避免錯(cuò)誤的值被傳入函數(shù),盡管這些值很可能是常量,人工檢查一眼就能發(fā)現(xiàn)錯(cuò)誤的那種。

這和上一節(jié)提到的interface意外接受錯(cuò)誤的類型一樣,屬于如何從某個(gè)大集合中獲取滿足特定條件的元素的子集,只不過討論的對象從變量變成了常量。

在前泛型時(shí)代我們只能靠運(yùn)行時(shí)檢查解決問題,當(dāng)然在泛型時(shí)代因?yàn)間olang的限制我們也沒法解決上面的奇偶數(shù)檢查問題,但對于更具體的實(shí)際場景來說,泛型剛好能派上用場。

例子同樣選自生產(chǎn)環(huán)境中的系統(tǒng),這個(gè)系統(tǒng)里有一個(gè)請求發(fā)送組件,它接受特定格式的數(shù)據(jù)對象和一個(gè)url,通過http請求把數(shù)據(jù)發(fā)送至url,然后再把返回結(jié)果存進(jìn)特定格式的對象里,偽代碼如下:

type ARequest struct{}
type AResponse struct{}
func (a *ARequest) RequestData() string { return "A Request" }
func (a *AResponse) ResponseData() string { return "A Response" }

type BRequest struct{}
type BResponse struct{}
func (a *BRequest) RequestData() string { return "B Request" }
func (a *BResponse) ResponseData() string { return "B Response" }

type Requester interface {
    RequestData() string
}

type Responser interface {
    ResponseData() string
}

type Endpoint string

const (
    AURL Endpoint = "https://a/api"
    BURL Endpoint = "https://b/api"
)

func SendRequest(url Endpoint, req Requester) Resopnser {
    //...
}

是的,發(fā)送邏輯是單一且固定的,所以我們又使用接口來刪除冗余代碼,只保留一個(gè)泛用的SendRequest函數(shù)。但問題在于,url、request和reponse是嚴(yán)格配對的,而我們的函數(shù)可以接受他們的任意組合,比如我們只允許SendRequest(AURL, &ARequest{}),但即使寫了SendRequest(AURL, &BRequest{})代碼也能正常通過編譯,這會(huì)導(dǎo)致系統(tǒng)在運(yùn)行時(shí)崩潰或者更遭的遇到一些難以排查的臟數(shù)據(jù)問題。這就是常量安全問題最常見的一種體現(xiàn)。

當(dāng)然你還是可以在運(yùn)行時(shí)通過字符串比較和類型斷言來做校驗(yàn),但代碼會(huì)很復(fù)雜而且有不低的性能開銷。但所有參數(shù)我們其實(shí)在編譯時(shí)就知道了,url都是常量,參數(shù)類型和返回值類型也是已知的,只不過編譯器不知道他們之間的配對關(guān)系。換句話說,只要我們把常量和類型之間的配對關(guān)系以某種方式告訴編譯器,那么就有機(jī)會(huì)把這些參數(shù)校驗(yàn)放在編譯時(shí)完成,根本不需要付出運(yùn)行時(shí)代價(jià),也不會(huì)讓代碼變得過于復(fù)雜。

正好泛型編程中的Phantom Type可以解決這種區(qū)分常量以及類型配對的問題。

所謂Phantom Type,其實(shí)就是把一些簡單的類型泛型化加上類型參數(shù),但這些類型參數(shù)只是簡單占位和該泛型類型的值無關(guān)也不參與實(shí)際的計(jì)算和處理:

type PhantomString[T any] string
type PhantomInt[T, U any] int

PhantomStringPhantomInt仍然可以當(dāng)做字符串和整形來使用,但因?yàn)榧由狭祟愋蛥?shù),所以即使他們底層的值相同,也會(huì)因?yàn)轭愋筒煌灰暈椴煌某A浚?/p>

const (
    AP PhantomString[int] = "hello"
    BP PhantomString[int] = "hello"
)
// AP和BP因?yàn)橛型耆煌念愋?,所有即使值相同,他們也是不同?

const (
    A PhantomInt[int, uint] = 1
    B PhantomInt[int, float64] = 1
    C PhantomInt[[]rune, string] = 1
)
// 同理ABC也是完全不同的

可以看到類型參數(shù)本身和類型的值沒有任何關(guān)聯(lián),就像幻影一樣,所以得名Phantom Type

熟悉Haskell或者c++模板元編程的開發(fā)者應(yīng)該知道這種技巧,通過賦予常量不同的類型,我們可以靠類型系統(tǒng)來區(qū)分這些常量。而且泛型允許的類型參數(shù)可以有多個(gè),所以我們還能把類型之間的組合關(guān)系綁定到這些類型化的常量上。

因此上面的例子可以使用Phantom Type改寫:

// 將URL常量和請求/應(yīng)答類型進(jìn)行綁定
type Endpoint[Req Requester, Resp Responser] string

const (
    AURL Endpoint[*ARequest, *AResponse] = "https://a/api"
    BURL Endpoint[*BRequest, *BResponse] = "https://b/api"
)

func SendMessage[Req Requester, Resp Responser](url Endpoint[Req, Resp], req Req) Resp {
    // 可以直接把url轉(zhuǎn)回string
    fmt.Printf("send request to: %s\n", string(url))
    var ret Resp
    return ret
}

func main() {
	ret := SendMessage(AURL, &ARequest{})
	fmt.Println(ret.ResponseData())
    // 編譯報(bào)錯(cuò)
    // SendMessage(AURL, &BRequest{})
}

現(xiàn)在我們?yōu)槌A拷壎苏埱蠛蛻?yīng)答的類型,常量傳入函數(shù)后編譯器會(huì)自動(dòng)推導(dǎo)出請求參數(shù)和返回值必須與常量綁定的類型一致,任何不匹配都會(huì)報(bào)錯(cuò)。比如注釋中的表達(dá)式:in call to SendMessage, type *BRequest of &BRequest{} does not match inferred type *ARequest for Req。這次的報(bào)錯(cuò)信息也相當(dāng)清晰。

利用Phantom Type的代碼整體上也遠(yuǎn)比運(yùn)行時(shí)檢查清晰簡潔,而且這次我們不會(huì)付出任何運(yùn)行時(shí)的性能代價(jià),所有檢查都在編譯代碼時(shí)就完成了。

不過有一點(diǎn)需要注意,golang不會(huì)自動(dòng)推導(dǎo)函數(shù)返回值的類型,這里我們通過Endpoint綁定請求/應(yīng)答類型,能夠讓編譯器推導(dǎo)出所有類型參數(shù),但其他場景下得注意這個(gè)限制,有時(shí)候需要明確給出所有類型參數(shù)才行,這時(shí)候代碼可能就沒那么簡潔了。

總結(jié)

本文只是簡單介紹了兩種最常見的泛型增強(qiáng)代碼安全性的用法,實(shí)際上還有很多實(shí)用技巧等待大家去發(fā)現(xiàn)。

核心思想很簡單:利用泛型的類型參數(shù)來綁定類型之間的關(guān)系,并通過不同的類型來區(qū)分不同種類的值。上一節(jié)里把這兩種思想綜合運(yùn)用之后可以得到既安全又簡潔的代碼。

在即將發(fā)布的 Go 1.26 版本中,泛型的實(shí)用性將進(jìn)一步增強(qiáng)。盡管限制仍然很多,但利用好泛型不僅可以少寫代碼,還可以讓你的代碼安全性更上一層樓。

以上就是淺析Golang如何利用泛型編寫更安全的代碼的詳細(xì)內(nèi)容,更多關(guān)于Golang泛型的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 解決Go gorm踩過的坑

    解決Go gorm踩過的坑

    這篇文章主要介紹了解決Go gorm踩過的坑,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Go語言實(shí)現(xiàn)二維數(shù)組的2種遍歷方式以及案例詳解

    Go語言實(shí)現(xiàn)二維數(shù)組的2種遍歷方式以及案例詳解

    這篇文章主要介紹了Go語言實(shí)現(xiàn)二維數(shù)組的2種遍歷方式以及案例詳解,圖文代碼聲情并茂,有感興趣的可以學(xué)習(xí)下
    2021-03-03
  • Golang: 內(nèi)建容器的用法

    Golang: 內(nèi)建容器的用法

    這篇文章主要介紹了Golang: 內(nèi)建容器的用法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • Go語言Web編程實(shí)現(xiàn)Get和Post請求發(fā)送與解析的方法詳解

    Go語言Web編程實(shí)現(xiàn)Get和Post請求發(fā)送與解析的方法詳解

    這篇文章主要介紹了Go語言Web編程實(shí)現(xiàn)Get和Post請求發(fā)送與解析的方法,結(jié)合實(shí)例形式分析了Go語言客戶端、服務(wù)器端結(jié)合實(shí)現(xiàn)web數(shù)據(jù)get、post發(fā)送與接收數(shù)據(jù)的相關(guān)操作技巧,需要的朋友可以參考下
    2017-06-06
  • Go json反序列化“null“的問題解決

    Go json反序列化“null“的問題解決

    本文主要介紹了Go json反序列化“null“的問題解決,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • ubuntu下搭建Go語言(golang)環(huán)境

    ubuntu下搭建Go語言(golang)環(huán)境

    這篇文章主要介紹了ubuntu下搭建Go語言(golang)環(huán)境,需要的朋友可以參考下
    2015-01-01
  • golang如何實(shí)現(xiàn)proxy代理簡單方法

    golang如何實(shí)現(xiàn)proxy代理簡單方法

    這篇文章主要給大家介紹了關(guān)于golang如何實(shí)現(xiàn)proxy代理簡單方法的相關(guān)資料,Proxy是golang實(shí)現(xiàn)的高性能http,https,websocket,tcp,udp,socks5,ss代理服務(wù)器,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-10-10
  • golang基礎(chǔ)之Interface接口的使用

    golang基礎(chǔ)之Interface接口的使用

    這篇文章主要介紹了golang基礎(chǔ)之Interface接口的使用,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-07-07
  • Go語言中反射的正確使用

    Go語言中反射的正確使用

    Go本身不支持模板,因此在以往需要使用模板的場景下往往就需要使用反射(reflect). 反射使用多了以后會(huì)容易上癮,有些人甚至?xí)纬梢环N莫名其妙的鄙視鏈。下面這篇文章就給大家介紹了如何正確使用Go語言中的反射以及在使用前的注意,有需要的朋友們下面來一起看看吧。
    2016-12-12
  • 詳解Go語言如何實(shí)現(xiàn)二叉樹遍歷

    詳解Go語言如何實(shí)現(xiàn)二叉樹遍歷

    這篇文章主要為大家詳解介紹了Go語言中如何實(shí)現(xiàn)二叉樹遍歷,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Go語言有一定幫助,需要的可以參考一下
    2022-04-04

最新評(píng)論

徐闻县| 江津市| 镇沅| 义马市| 静海县| 桦甸市| 盘山县| 罗平县| 耒阳市| 务川| 商水县| 寿光市| 安宁市| 六安市| 米脂县| 宜昌市| 论坛| 米易县| 安国市| 涡阳县| 齐齐哈尔市| 梁河县| 香河县| 万盛区| 全州县| 徐闻县| 通化县| 兰州市| 甘洛县| 湘潭市| 汝州市| 吴桥县| 图木舒克市| 汽车| 休宁县| 濉溪县| 台州市| 老河口市| 普格县| 新兴县| 汨罗市|