Golang中g(shù)eneric泛型的實戰(zhàn)指南
簡介
generic 通常翻譯成“泛型”。
Go 從 1.18 開始支持泛型。
泛型解決的核心問題很直接:同一套邏輯,適配多種類型,同時保留編譯期類型檢查。
比如求和。
沒有泛型時,int 要寫一份:
func SumInts(nums []int) int {
var total int
for _, n := range nums {
total += n
}
return total
}
float64 又要寫一份:
func SumFloat64s(nums []float64) float64 {
var total float64
for _, n := range nums {
total += n
}
return total
}
邏輯完全一樣,只是類型不同。
使用泛型后,可以合并成一個函數(shù):
func Sum[T int | float64](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
一句話概括:
泛型讓代碼把“類型”也當(dāng)成參數(shù)傳進(jìn)去。
第一個泛型函數(shù)
先看一個最簡單的泛型函數(shù):
package main
import "fmt"
func Identity[T any](value T) T {
return value
}
func main() {
fmt.Println(Identity(100))
fmt.Println(Identity("hello"))
fmt.Println(Identity(true))
}
輸出:
100
hello
true
這里的核心是:
func Identity[T any](value T) T
拆開看:
| 部分 | 含義 |
|---|---|
T | 類型參數(shù)名稱 |
any | 類型約束,表示任意類型 |
value T | 參數(shù)類型是 T |
返回值 T | 返回值類型也是 T |
Identity(100) 調(diào)用時,T 會被推斷成 int。
Identity("hello") 調(diào)用時,T 會被推斷成 string。
類型參數(shù)是什么
普通函數(shù)的參數(shù)是“值參數(shù)”。
func Add(a int, b int) int {
return a + b
}
a、b 是值參數(shù)。
泛型函數(shù)多了一類參數(shù),叫“類型參數(shù)”。
func Add[T int | int64](a T, b T) T {
return a + b
}
T 是類型參數(shù)。
它不是某個具體值,而是某個具體類型的占位符。
調(diào)用時:
Add(1, 2)
編譯器推斷出:
T = int
調(diào)用:
Add(int64(1), int64(2))
編譯器推斷出:
T = int64
any 約束
any 是 interface{} 的別名。
type any = interface{}
它表示任意類型。
package main
import "fmt"
func Print[T any](value T) {
fmt.Printf("type=%T value=%v\n", value, value)
}
func main() {
Print(100)
Print("go")
Print([]int{1, 2, 3})
}
輸出:
type=int value=100
type=string value=go
type=[]int value=[1 2 3]
但 any 也有限制。
它只表示“任意類型”,不代表這個類型支持所有操作。
下面這種代碼不能通過編譯:
func Add[T any](a T, b T) T {
return a + b
}
原因是:
any 不保證 T 支持 + 運算。
如果要使用 +,必須給類型參數(shù)加更具體的約束。
類型約束
泛型里的約束用來限制類型參數(shù)能是什么類型。
例如:
func Add[T int | int64 | float64](a T, b T) T {
return a + b
}
這里的:
T int | int64 | float64
表示:
T 只能是 int、int64、float64 這幾種類型之一。
因為這些類型都支持 +,所以函數(shù)體里可以寫:
return a + b
完整示例:
package main
import "fmt"
func Add[T int | int64 | float64](a T, b T) T {
return a + b
}
func main() {
fmt.Println(Add(1, 2))
fmt.Println(Add(int64(10), int64(20)))
fmt.Println(Add(1.5, 2.5))
}
輸出:
3
30
4
自定義約束
如果很多函數(shù)都要使用同一組數(shù)值類型,可以把約束抽出來。
type Number interface {
int | int64 | float64
}
然后復(fù)用:
func Add[T Number](a T, b T) T {
return a + b
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
完整示例:
package main
import "fmt"
type Number interface {
int | int64 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(Sum([]int{1, 2, 3}))
fmt.Println(Sum([]int64{10, 20, 30}))
fmt.Println(Sum([]float64{1.1, 2.2, 3.3}))
}
輸出:
6
60
6.6
~ 是什么
~ 用來匹配底層類型。
先看一個自定義類型:
type UserID int64
UserID 是一個新類型,它的底層類型是 int64。
如果約束寫成:
type Integer interface {
int64
}
那么 UserID 不滿足這個約束。
因為 UserID 不是 int64,只是底層類型是 int64。
如果希望支持這種自定義類型,要寫:
type Integer interface {
~int64
}
完整示例:
package main
import "fmt"
type UserID int64
type Integer interface {
~int | ~int64
}
func Add[T Integer](a T, b T) T {
return a + b
}
func main() {
var a UserID = 100
var b UserID = 200
fmt.Println(Add(a, b))
}
輸出:
300
可以這樣記:
- int64 只匹配 int64。
- ~int64 匹配 int64,以及底層類型是 int64 的自定義類型。
comparable 約束
comparable 是 Go 內(nèi)置約束。
它表示類型可以使用 == 和 != 比較。
常見可比較類型:
- 數(shù)字
- 字符串
- 布爾
- 指針
- channel
- 字段都可比較的結(jié)構(gòu)體
slice、map、func 不能比較。
泛型里經(jīng)常用 comparable 寫查找函數(shù):
package main
import "fmt"
func Contains[T comparable](items []T, target T) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func main() {
fmt.Println(Contains([]int{1, 2, 3}, 2))
fmt.Println(Contains([]string{"go", "java"}, "php"))
}
輸出:
true
false
comparable 也常用于 map key。
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
map 的 key 必須可比較,所以這里的 K 要寫成 comparable。
類型推斷
調(diào)用泛型函數(shù)時,很多時候不用手動寫類型。
fmt.Println(Contains([]int{1, 2, 3}, 2))
編譯器可以根據(jù)參數(shù)推斷:
T = int
也可以顯式指定:
fmt.Println(Contains[int]([]int{1, 2, 3}, 2))
多數(shù)業(yè)務(wù)代碼不需要顯式指定。
但如果類型參數(shù)只出現(xiàn)在返回值里,編譯器通常推斷不出來。
func Zero[T any]() T {
var zero T
return zero
}
調(diào)用時需要寫類型:
v := Zero[int]()
泛型切片工具:Map
Map 用來把一個切片轉(zhuǎn)換成另一個切片。
例如把 []int 轉(zhuǎn)成 []string。
package main
import "fmt"
func Map[T any, R any](items []T, mapper func(T) R) []R {
result := make([]R, 0, len(items))
for _, item := range items {
result = append(result, mapper(item))
}
return result
}
func main() {
nums := []int{1, 2, 3}
texts := Map(nums, func(n int) string {
return fmt.Sprintf("num=%d", n)
})
fmt.Println(texts)
}
輸出:
[num=1 num=2 num=3]
這個函數(shù)里有兩個類型參數(shù):
func Map[T any, R any](items []T, mapper func(T) R) []R
含義:
T是輸入元素類型R是輸出元素類型
泛型切片工具:Filter
Filter 用來過濾切片。
package main
import "fmt"
func Filter[T any](items []T, predicate func(T) bool) []T {
result := make([]T, 0, len(items))
for _, item := range items {
if predicate(item) {
result = append(result, item)
}
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
evens := Filter(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens)
}
輸出:
[2 4]
同一個 Filter 也能過濾結(jié)構(gòu)體切片:
type User struct {
ID int
Name string
Active bool
}
users := []User{
{ID: 1, Name: "張三", Active: true},
{ID: 2, Name: "李四", Active: false},
}
activeUsers := Filter(users, func(u User) bool {
return u.Active
})
泛型切片工具:Find
Find 用來查找第一個滿足條件的元素。
找到了返回元素和 true。
沒找到返回零值和 false。
package main
import "fmt"
func Find[T any](items []T, predicate func(T) bool) (T, bool) {
var zero T
for _, item := range items {
if predicate(item) {
return item, true
}
}
return zero, false
}
type User struct {
ID int
Name string
}
func main() {
users := []User{
{ID: 1, Name: "張三"},
{ID: 2, Name: "李四"},
}
user, ok := Find(users, func(u User) bool {
return u.ID == 2
})
fmt.Println(user, ok)
}
輸出:
{2 李四} true
泛型切片工具:Reduce
Reduce 用來把一組數(shù)據(jù)歸約成一個結(jié)果。
例如求總和、拼接字符串、統(tǒng)計數(shù)量。
package main
import "fmt"
func Reduce[T any, R any](items []T, initial R, reducer func(R, T) R) R {
result := initial
for _, item := range items {
result = reducer(result, item)
}
return result
}
func main() {
nums := []int{1, 2, 3, 4}
total := Reduce(nums, 0, func(sum int, n int) int {
return sum + n
})
fmt.Println(total)
}
輸出:
10
泛型結(jié)構(gòu)體
結(jié)構(gòu)體也可以帶類型參數(shù)。
最簡單的是 Box:
package main
import "fmt"
type Box[T any] struct {
Value T
}
func (b Box[T]) Get() T {
return b.Value
}
func (b *Box[T]) Set(value T) {
b.Value = value
}
func main() {
intBox := Box[int]{Value: 100}
fmt.Println(intBox.Get())
stringBox := Box[string]{Value: "hello"}
stringBox.Set("go")
fmt.Println(stringBox.Get())
}
輸出:
100
go
定義泛型類型時:
type Box[T any] struct
給泛型類型定義方法時,接收者也要帶上類型參數(shù):
func (b Box[T]) Get() T func (b *Box[T]) Set(value T)
實戰(zhàn) Demo:泛型 Stack
棧是典型的泛型數(shù)據(jù)結(jié)構(gòu)。
int 棧、string 棧、User 棧,邏輯都一樣。
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{
items: make([]T, 0),
}
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
lastIndex := len(s.items) - 1
item := s.items[lastIndex]
s.items = s.items[:lastIndex]
return item, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func main() {
stack := NewStack[int]()
stack.Push(10)
stack.Push(20)
v, ok := stack.Pop()
fmt.Println(v, ok)
fmt.Println(stack.Len())
}
輸出:
20 true
1
實戰(zhàn) Demo:泛型 Set
Go 沒有內(nèi)置 Set,可以用 map[T]struct{} 封裝。
因為 map key 必須可比較,所以 T 需要 comparable 約束。
package main
import "fmt"
type Set[T comparable] struct {
data map[T]struct{}
}
func NewSet[T comparable](items ...T) *Set[T] {
set := &Set[T]{
data: make(map[T]struct{}, len(items)),
}
for _, item := range items {
set.Add(item)
}
return set
}
func (s *Set[T]) Add(item T) {
s.data[item] = struct{}{}
}
func (s *Set[T]) Remove(item T) {
delete(s.data, item)
}
func (s *Set[T]) Contains(item T) bool {
_, ok := s.data[item]
return ok
}
func (s *Set[T]) Values() []T {
values := make([]T, 0, len(s.data))
for item := range s.data {
values = append(values, item)
}
return values
}
func main() {
ids := NewSet(1, 2, 2, 3)
fmt.Println(ids.Contains(2))
fmt.Println(ids.Values())
names := NewSet("go", "java", "go")
fmt.Println(names.Contains("php"))
fmt.Println(names.Values())
}
輸出類似:
true
[1 2 3]
false
[go java]
map 遍歷順序不固定,所以 Values() 輸出順序可能變化。
實戰(zhàn) Demo:通用 API 響應(yīng)
Web 項目里經(jīng)常有統(tǒng)一響應(yīng)結(jié)構(gòu)。
沒有泛型時,Data 可能寫成 any:
type ApiResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
這種寫法靈活,但丟失了 Data 的具體類型。
使用泛型可以保留類型:
type ApiResponse[T any] struct {
Code int `json:"code"`
Message string `json:"message"`
Data T `json:"data"`
}
完整示例:
package main
import (
"encoding/json"
"fmt"
)
type ApiResponse[T any] struct {
Code int `json:"code"`
Message string `json:"message"`
Data T `json:"data"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func Success[T any](data T) ApiResponse[T] {
return ApiResponse[T]{
Code: 200,
Message: "success",
Data: data,
}
}
func main() {
resp := Success(User{ID: 1, Name: "張三"})
data, err := json.Marshal(resp)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
輸出:
{"code":200,"message":"success","data":{"id":1,"name":"張三"}}
實戰(zhàn) Demo:分頁結(jié)果
分頁結(jié)果也很適合泛型。
用戶分頁、商品分頁、訂單分頁,結(jié)構(gòu)基本一樣,只是列表元素類型不同。
package main
import (
"encoding/json"
"fmt"
)
type PageResult[T any] struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
Total int64 `json:"total"`
Items []T `json:"items"`
}
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
}
func NewPageResult[T any](page int, pageSize int, total int64, items []T) PageResult[T] {
return PageResult[T]{
Page: page,
PageSize: pageSize,
Total: total,
Items: items,
}
}
func main() {
result := NewPageResult(1, 10, 100, []Product{
{ID: 1, Name: "鍵盤", Price: 199},
{ID: 2, Name: "鼠標(biāo)", Price: 99},
})
data, err := json.Marshal(result)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
輸出:
{"page":1,"pageSize":10,"total":100,"items":[{"id":1,"name":"鍵盤","price":199},{"id":2,"name":"鼠標(biāo)","price":99}]}
實戰(zhàn) Demo:通用內(nèi)存 Repository
泛型也可以用來封裝一些通用倉儲邏輯。
先定義一個實體約束:
type Entity interface {
GetID() int64
}
然后 Repository 只接收實現(xiàn)了 GetID 的類型。
完整示例:
package main
import "fmt"
type Entity interface {
GetID() int64
}
type Repository[T Entity] struct {
data map[int64]T
}
func NewRepository[T Entity]() *Repository[T] {
return &Repository[T]{
data: make(map[int64]T),
}
}
func (r *Repository[T]) Save(entity T) {
r.data[entity.GetID()] = entity
}
func (r *Repository[T]) FindByID(id int64) (T, bool) {
entity, ok := r.data[id]
return entity, ok
}
func (r *Repository[T]) List() []T {
result := make([]T, 0, len(r.data))
for _, entity := range r.data {
result = append(result, entity)
}
return result
}
type User struct {
ID int64
Name string
}
func (u User) GetID() int64 {
return u.ID
}
type Product struct {
ID int64
Name string
Price int
}
func (p Product) GetID() int64 {
return p.ID
}
func main() {
userRepo := NewRepository[User]()
userRepo.Save(User{ID: 1, Name: "張三"})
user, ok := userRepo.FindByID(1)
fmt.Println(user, ok)
productRepo := NewRepository[Product]()
productRepo.Save(Product{ID: 100, Name: "鍵盤", Price: 199})
product, ok := productRepo.FindByID(100)
fmt.Println(product, ok)
}
輸出:
{1 張三} true
{100 鍵盤 199} true
這種寫法適合演示通用邏輯。
真實業(yè)務(wù)里的 Repository 往往會有復(fù)雜查詢、事務(wù)、權(quán)限、緩存等差異,不一定都適合硬抽成泛型。
泛型接口
接口也可以帶類型參數(shù)。
例如定義一個通用 Store:
type Store[T any] interface {
Save(value T) error
Find() (T, bool)
}
完整示例:
package main
import "fmt"
type Store[T any] interface {
Save(value T) error
Find() (T, bool)
}
type MemoryStore[T any] struct {
value T
ok bool
}
func (s *MemoryStore[T]) Save(value T) error {
s.value = value
s.ok = true
return nil
}
func (s *MemoryStore[T]) Find() (T, bool) {
return s.value, s.ok
}
func main() {
var store Store[string] = &MemoryStore[string]{}
_ = store.Save("hello")
value, ok := store.Find()
fmt.Println(value, ok)
}
輸出:
hello true
泛型方法的注意點
Go 支持給泛型類型定義方法。
type Box[T any] struct {
Value T
}
func (b Box[T]) Get() T {
return b.Value
}
但方法不能單獨聲明一組新的類型參數(shù)。
下面這種寫法不能編譯:
func (b Box[T]) Map[R any](mapper func(T) R) R {
return mapper(b.Value)
}
這類需求可以改成普通泛型函數(shù):
func MapBox[T any, R any](b Box[T], mapper func(T) R) R {
return mapper(b.Value)
}
泛型 vs interface
interface 適合表達(dá)行為。
type Writer interface {
Write(p []byte) (int, error)
}
重點是:對象能做什么。
泛型適合保留具體類型并復(fù)用算法。
func First[T any](items []T) (T, bool) {
if len(items) == 0 {
var zero T
return zero, false
}
return items[0], true
}
重點是:同一套代碼處理不同類型,同時返回具體類型。
對比:
func FirstAny(items []any) (any, bool) {
if len(items) == 0 {
return nil, false
}
return items[0], true
}
FirstAny 返回的是 any,調(diào)用方還要類型斷言。
泛型版本返回的是準(zhǔn)確類型:
value, ok := First([]int{1, 2, 3})
這里的 value 是 int。
標(biāo)準(zhǔn)庫里的泛型工具
Go 標(biāo)準(zhǔn)庫里已經(jīng)有一些泛型工具包,例如 slices、maps。
示例:
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{3, 1, 2}
slices.Sort(nums)
fmt.Println(nums)
fmt.Println(slices.Contains(nums, 2))
}
輸出:
[1 2 3]
true
這些工具能直接處理不同元素類型的切片。
日常項目里,能使用標(biāo)準(zhǔn)庫工具時,沒必要重復(fù)封裝一套。
什么時候適合用泛型
適合使用泛型的場景:
- 多種類型共享同一套邏輯
- 返回值需要保留具體類型
- 集合工具函數(shù),例如
Map、Filter、Contains - 通用數(shù)據(jù)結(jié)構(gòu),例如
Set、Stack、Queue - 通用返回結(jié)構(gòu),例如
ApiResponse[T]、PageResult[T] - 類型安全比
any+ 類型斷言更重要
示例:
func Contains[T comparable](items []T, target T) bool type Set[T comparable] struct type PageResult[T any] struct
什么時候不適合用泛型
不適合泛型化的場景:
- 只有一個類型會使用
- 各類型之間業(yè)務(wù)差異很大
- 類型約束寫得特別復(fù)雜
- 泛型讓調(diào)用方更難讀懂
- 為了少寫幾行代碼而犧牲清晰度
比如兩個業(yè)務(wù)流程只是表面類似,但校驗、查詢、事務(wù)、日志完全不同,強行抽成泛型會讓代碼變繞。
這種情況下,普通函數(shù)或接口可能更合適。
常見問題
any 和泛型是一回事嗎
不是。
any 是一個約束,也可以作為普通接口類型使用。
泛型是“帶類型參數(shù)的代碼”。
例如:
func Print[T any](value T)
這里使用了泛型,any 只是 T 的約束。
而下面這個函數(shù)不是泛型:
func Print(value any)
它只是接收一個空接口值。
comparable 可以表示所有能排序的類型嗎
不能。
comparable 只表示可以用 ==、!= 比較。
它不代表可以用 <、> 排序。
例如布爾值可以比較相等,但不能排序。
如果需要 <、>,需要使用自定義約束:
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
泛型類型可以自動推斷類型參數(shù)嗎
泛型函數(shù)通常可以通過參數(shù)推斷。
Contains([]int{1, 2, 3}, 2)
泛型類型初始化時通常需要寫出類型參數(shù):
stack := Stack[int]{}
如果通過構(gòu)造函數(shù),函數(shù)可以幫助推斷一部分類型。
set := NewSet(1, 2, 3)
這里 NewSet 是泛型函數(shù),編譯器可以根據(jù)傳入?yún)?shù)推斷 T。
泛型能完全替代 interface 嗎
不能。
泛型和接口解決的問題不同。
接口更適合描述行為和解耦實現(xiàn)。
泛型更適合復(fù)用類型安全的算法和數(shù)據(jù)結(jié)構(gòu)。
例如 io.Reader 這種“能讀取”的行為抽象,用接口更自然。
例如 Set[T] 這種“相同邏輯適配不同元素類型”的容器,用泛型更自然。
總結(jié)
generic 的核心可以壓縮成幾句話:
- 泛型讓類型也能成為參數(shù)。
- 類型參數(shù)寫在 [] 里。
- 約束限制類型參數(shù)能做什么。
- any 表示任意類型。
- comparable 表示支持 == 和 !=。
- ~T 表示包含底層類型是 T 的自定義類型。
- 泛型適合通用算法、通用容器、通用響應(yīng)結(jié)構(gòu)。
日常選擇可以按下面這張表判斷:
| 場景 | 常見寫法 |
|---|---|
| 任意類型參數(shù) | [T any] |
| 可比較類型 | [T comparable] |
| 數(shù)值計算 | 自定義 Number 約束 |
| 支持自定義底層類型 | ~int、~string |
| 多類型聯(lián)合 | `int |
| 通用切片轉(zhuǎn)換 | Map[T, R] |
| 通用切片過濾 | Filter[T] |
| 通用集合 | Set[T comparable] |
| 通用響應(yīng) | ApiResponse[T] |
| 通用分頁 | PageResult[T] |
| 行為抽象 | interface 更合適 |
泛型不是越多越好。它最適合消除真正重復(fù)的類型邏輯,并且讓返回值保持具體類型。代碼只有一個類型會用、業(yè)務(wù)規(guī)則差異很大、約束寫得很繞時,直接寫普通代碼通常更清楚。
以上就是Golang中g(shù)eneric泛型的實戰(zhàn)指南的詳細(xì)內(nèi)容,更多關(guān)于Golang generic泛型的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang bad file descriptor問題的解決方法
這篇文章主要給大家介紹了golang bad file descriptor問題的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
VSCode1.4 搭建Golang的開發(fā)調(diào)試環(huán)境(遇到很多問題)
這篇文章主要介紹了VSCode1.4 搭建Golang的開發(fā)調(diào)試環(huán)境(遇到很多問題),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04
詳解golang中?work與?module?的區(qū)別與聯(lián)系
Go?模塊通常由一個項目或庫組成,并包含一組隨后一起發(fā)布的?Go?包,Go?模塊通過允許用戶將項目代碼放在他們選擇的目錄中并為每個模塊指定依賴項的版本,解決了原始系統(tǒng)的許多問題,本文將給大家介紹一下golang中?work與?module?的區(qū)別與聯(lián)系,需要的朋友可以參考下2023-09-09
golang定時器Timer的用法和實現(xiàn)原理解析
這篇文章主要介紹了golang定時器Ticker,本文主要來看一下Timer的用法和實現(xiàn)原理,需要的朋友可以參考以下內(nèi)容2023-04-04

