Go 1.17 泛型嚐鮮
今天,Go 的 1.17 版本終於正式釋出,除了帶來各種優化和新功能外,1.17 正式在程式中提供了嚐鮮的泛型支援,這一功能也是為 1.18 版本泛型正式實裝做鋪墊。意味著在 6 個月後,我們就可以正式使用泛型開發了。那在 Go 1.18 正式實裝之前,我們在 1.17 版本中先嚐鮮一下泛型的支援吧。
泛型有什麼作用?
在使用 Go 沒有泛型之前我們怎麼實現針對多型別的邏輯實現的呢?有很多方法,比如說使用interface{}
作為變數型別引數,在內部通過型別判斷進入對應的處理邏輯;將型別轉化為特定表現的鴨子型別,通過介面定義的方法實現邏輯整合;還有人專門編寫了 Go 的函式程式碼生成工具,通過批量生成不同型別的相同實現函式代替手工實現等等。這些方法多多少少存在一些問題:使用了interface{}
作為引數意味著放棄了編譯時檢查,作為強型別語言的一個優勢就被抹掉了。同樣,無論使用程式碼生成還是手工書寫,一旦出現問題,意味著這些方法都需要重複生成或者進行批量修改,工作量反而變得更多了。
在 Go 中引入泛型會給程式開發帶來很多好處:通過泛型,可以針對多種型別編寫一次程式碼,大大節省了編碼時間。你可以充分應用編譯器的編譯檢查,保證程式變數型別的可靠性。藉助泛型,你可以減少程式碼的重複度,也不會出現一處出現問題需要修改多處地方的尷尬問題。這也讓很多測試工作變得更簡單,藉助型別安全,你甚至可以少考慮很多的邊緣情況。
Go 語言官方有詳細的泛型提案文件可以在這裡和這裡檢視詳情。
如何使用泛型
前面理論我們僅僅只做介紹,這次嚐鮮還是以實踐為主。讓我們先從一個小例子開始。
從簡單的例子開始
讓我們先從一個最簡單的例子開始:
package main
import (
"fmt"
)
type Addable interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64, complex64, complex128,
string
}
func add[T Addable](a, b T) T {
return a + b
}
func main() {
fmt.Println(add(1,2))
fmt.Println(add("1", "2"))
}
這個函式可以實現任何需要使用+
符號進行運算的型別,我們通過定義Addable
型別,列舉了所有可能可以使用add
方法的所有的型別。比如我們在main
函式中就使用了int
和string
兩種不同型別。
但是如果這時我們使用簡單的go run
命令執行,會發現提示語法錯誤:
$ go version
go version go1.17 darwin/arm64
$ go run ~/main.go
# command-line-arguments
../main.go:8:2: syntax error: unexpected type, expecting method or interface name
../main.go:15:6: missing function body
../main.go:15:9: syntax error: unexpected [, expecting (
因為在 Go 1.17 中,泛型並未預設開啟,你需要定義gcflags
方式啟用泛型:
$ go run -gcflags=-G=3 ~/main.go
3
12
如果你覺得這種方式太過於複雜,每次都需要新增,也可以通過定義環境變數形式讓每次都帶此引數(不推薦,尤其是多版本環境時低版本 Go 中會報錯):
$ export GOFLAGS="-gcflags=-G=3"
$ go run ~/main.go
3
12
在 Go 中,泛型可以做什麼更多更復雜的事情嗎?當然可以。除了最基礎的演算法實現以外,我們可以通過後面的幾個場景看一下泛型可用的場景。
實現型別安全的 Map
在現實開發過程中,我們往往需要對 slice 中資料的每個值進行單獨的處理,比如說需要對其中數值轉換為平方值,在泛型中,我們可以抽取部分重複邏輯作為 map 函式:
package main
import (
"fmt"
)
func mapFunc[T any, M any](a []T, f func(T) M) []M {
n := make([]M, len(a), cap(a))
for i, e := range a {
n[i] = f(e)
}
return n
}
func main() {
vi := []int{1,2,3,4,5,6}
vs := mapFunc(vi, func(v int) int {
return v*v
})
fmt.Println(vs)
}
$ go run -gcflags=-G=3 main.go
[1 4 9 16 25 36]
在這個例子中,我們定義了一個 M 型別,因此除了進行同樣型別的轉換外,也可以做不同型別的轉換:
- vs := mapFunc(vi, func(v int) int {
- return v*v
+ vs := mapFunc(vi, func(v int) string {
+ return "<"+fmt.Sprint(v)+">"
$ go run -gcflags=-G=3 main.go
[<1> <2> <3> <4> <5> <6>]
實現型別安全的 Map/Filter
除了運算元據以外,我們通常還需要對資料進行篩選。在前面的例子上,我們可以通過實現filterFunc
實現更好的通用邏輯:
package main
import (
"crypto/rand"
"fmt"
"math/big"
"strings"
)
func mapFunc[T any, M any](a []T, f func(T) M) []M {
n := make([]M, len(a), cap(a))
for i, e := range a {
n[i] = f(e)
}
return n
}
func filterFunc[T any](a []T, f func(T) bool) []T {
var n []T
for _, e := range a {
if f(e) {
n = append(n, e)
}
}
return n
}
func main() {
vi := filterFunc(
mapFunc([]int{1,2,3,4,5,6},
func(v int) int {
return v*v
},
),
func(v int) bool {
return v < 40
})
fmt.Println(vi)
vs := filterFunc(
mapFunc([]string{"a", "b", "c", "d", "e"},
func(v string) string {
// 需要使用crypto/rand增加隨機性
n, _ :=rand.Int(rand.Reader, big.NewInt(5))
i := int(n.Int64())+1
return strings.Repeat(v, i)
},
),
func(v string) bool {
return len(v)>3
})
fmt.Println(vs)
}
$ go run -gcflags=-G=3 main.go
[1 4 9 16 25 36]
[aaaa dddd eeeee]
實現型別可靠的 Worker Pool
除了上面這個例子,我們還可以通過泛型實現一個型別可靠的通用批量型別轉換函式:
package main
import (
"fmt"
"strconv"
"sync"
)
type T1 interface{}
type T2 interface{}
func ParallelMap(parallelism int, in []T1, f func(T1) (T2, error)) ([]T2, error) {
var wg sync.WaitGroup
defer wg.Wait()
inc, outc, errc := make(chan T1), make(chan T2), make(chan error)
donec := make(chan struct{})
defer close(donec)
wg.Add(parallelism)
for i := 0; i < parallelism; i++ {
go func() {
defer wg.Done()
for x := range inc {
y, err := f(x)
if err != nil {
select {
case errc <- err:
case <-donec:
}
return
}
select {
case outc <- y:
case <-donec:
return
}
}
select {
case errc <- nil:
case <-donec:
}
}()
}
go func() {
for _, x := range in {
inc <- x
}
close(inc)
}()
out := make([]T2, 0, len(in))
for rem := parallelism; rem > 0; {
select {
case err := <-errc:
if err != nil {
return nil, err
}
rem--
case y := <-outc:
out = append(out, y)
}
}
return out, nil
}
func main() {
in := []T1{"1", "2", "3", "4", "5"}
out, err := ParallelMap(4, in, func(x T1) (T2, error) {
return strconv.Atoi(x.(string))
})
if err != nil {
fmt.Println("error: ", err)
return
}
fmt.Println(out)
in2 := []T1{1, 2, 3, 4, 5}
out2, err := ParallelMap(4, in2, func(x T1) (T2, error) {
return fmt.Sprintf("<%d>", x), nil
})
if err != nil {
fmt.Println("error: ", err)
return
}
fmt.Println(out2)
}
$ go run -gcflags=-G=3 main.go
[3 5 2 4 1]
[<1> <4> <5> <3> <2>]
其他應用
我們可以預見在 Go 1.18 版本中,多個標準庫會被新增或者擴充套件,包括:型別定義庫constraints
,通用 slice 操作庫slices
,通用型別安全 mapmaps
等等。因為這些會進入標準庫,大家可以先自行實現試用,真正線上使用建議等待標準庫新增內容即可。
Go 泛型的實現原理
我們迴歸到最原始的例子快速看一下 Go 中是如何實現泛型的。為了方便分析,我們在所有func
上新增go:noinline
防止內聯,然後編譯程式進行分析。這裡可能 Go 1.17 實現問題未能支援如go tool
或go build -gcflags=all=-S
之類的命令傳遞-G=3
引數,因此這裡我們選擇第三方的反彙編工具看一下具體的實現:
可以看到目前 Go 會根據型別將泛型展開成對應型別函式,這樣也會小小的增加編譯時間和編譯後檔案大小。因為我測試使用 Apple Silicon 平臺,考慮大家可能不熟悉相關彙編,具體執行邏輯不再具體展示。
其他注意事項
目前 Go 的泛型仍在開發過程中,即便在 1.17beta 到正式版過程中,很多泛型的 corner case 也正在完善過程中,比如在之前測試中我發現某些程式碼在 beta 版本無法正確編譯,但是在 RC 中已可以正確編譯。目前的泛型實現未必代表 1.18 版本中是相同的實現細節,甚至可能在 1.18 中提供更多的功能。同時,目前 1.17 泛型型別是無法在 package 中匯出的,這導致在 1.17 版本中它的應用場景大大的受限。如果你仍有計劃在某些場景中使用,我仍舊建議單元測試覆蓋你使用的場景情況,防止出現版本迭代可能導致的問題。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Julia 嚐鮮
- Go 泛型Go泛型
- Go 泛型之泛型約束Go泛型
- React Loops 嚐鮮ReactOOP
- React Suspense 嚐鮮React
- go泛型教程Go泛型
- 泛型最佳實踐:Go泛型設計者教你如何用泛型泛型Go
- .Net8 Blazor 嚐鮮Blazor
- 利用Conda嚐鮮Python 3.10Python
- Windows 10 週年版嚐鮮Windows
- 鴻蒙系統嚐鮮鴻蒙
- Vue.js 2.6嚐鮮Vue.js
- Go 官方出品泛型教程:如何開始使用泛型Go泛型
- Java & Go 泛型對比JavaGo泛型
- Go泛型基礎使用Go泛型
- TiDB 4.0 新特性嚐鮮指南TiDB
- go 1.18 泛型初體驗Go泛型
- go需要泛型的場景Go泛型
- Go 需要泛型的場景Go泛型
- Flutter新版本 Web App 嚐鮮FlutterWebAPP
- Oracle 19c 安裝嚐鮮Oracle
- HTML5中dialog元素嚐鮮HTML
- Spring Cloud Gateway 閘道器嚐鮮SpringCloudGateway
- Go 1.18泛型的侷限性初探Go泛型
- Go Internals: Go 反射 vs Java 泛型 vs cpp 模板Go反射Java泛型
- Go 1.18 泛型全面講解:一篇講清泛型的全部Go泛型
- ent orm筆記1---快速嚐鮮ORM筆記
- 【轉】Kinect嚐鮮(1)——第一個程式
- Webpack5.0 新特性嚐鮮實戰 ??Web
- Go中泛型和反射比較指南Go泛型反射
- Go泛型草案設計簡明指南Go泛型
- 泛型類、泛型方法及泛型應用泛型
- 嚐鮮:Gradle構建SpringBoot(2.3.1最新版)GradleSpring Boot
- 【Flutter桌面篇】Flutter&Windows應用嚐鮮FlutterWindows
- TiDB at 豐巢:嚐鮮分散式資料庫TiDB分散式資料庫
- 【java】【泛型】泛型geneticJava泛型
- 預計在 Go 1.18 中內建泛型Go泛型
- go1.18泛型的簡單嘗試Go泛型