這篇文章帶給大家的內容是介紹深入理解golang中的泛型?泛型怎麼使用?有一定的參考價值,有需要的朋友可以參考一下,希望對你們有幫助。
#泛型(Generic)是一種程式設計技術。在強型別語言中, 允許編寫程式碼時使用以後才指定的型別, 在實例化時指定對應的型別。
在泛型中,可以使用型別參數來取代特定的資料型別。這些類型參數可以在類別、方法或介面中聲明,並且可以在這些聲明中使用。使用泛型的程式碼可以在運行時指定實際的類型參數, 這使得程式碼可以適用於多種不同類型的資料。
泛型可以提高程式碼的可讀性、可維護性和可重複使用性。它可以減少程式碼的冗餘程度, 並且可以提供更好的類型安全性和編譯時類型檢查。
我們透過一個具體的例子來解釋為什麼泛型可以減少程式碼的冗餘:
提供一個函數, 傳回a, b 的最小值, 我們需要每一種特定的資料類型「int, float...」寫一個函數; 或使用interface{}「需要對參數進行類型斷言, 對運行性能有影響, 且無法約束傳入的參數」
func minInt(a, b int) int { if a > b { return b } return a } func minFloat(a, b float64) float64 { if a > b { return b } return a } func minItf(a, b interface{}) interface{} { switch a.(type) { case int: switch b.(type) { case int: if a.(int) > b.(int) { return b } return a } case float64: switch b.(type) { case float64: if a.(float64) > b.(float64) { return b } return a } } return nil }
從上面的方法我們可以看出, minInt 和minFloat 除了參數與傳回結果的型別不同之外, 其餘程式碼皆相同。那有沒有一種方式可以不指定特定的型別, 在函數呼叫的時候再確定傳入的型別?這裡就引入一個概念叫泛型, 可以簡單理解為寬泛的類型或未指定具體型別。透過引入泛型, 我們就不需要再指定具體的資料型別, min 函數就可以使用下面的方式:
// T 为类型参数, 在调用时确定参数的具体值, 可以为 int, 也可以为 float64;它与 a, b 一样也是参数, 需要调用时传入具体的值;不同的是,T 为类型参数,值为具体的类型, a,b 为函数参数,值为具体类型对应的值 func minIntAndFloat64[T int | float64](a, b T) T { if a < b { return a } return b } minIntAndFloat64[int](1, 2) // 实例化/调用时指定具体的类型
add 函數。
add 為函數名稱,
x, y 為形參,
(x,y int)為參數清單。發生函數呼叫時,
add(2, 3) 2, 3 為實參。
類比到泛型, 我們需要一個型別參數, 當發生函數呼叫時傳入對應的型別實參, 帶有型別參數的函數叫做泛型函數。 [T int | int64] 為型別參數清單,
T 為型別參數,
int | int64 為型別集合/型別約束。當發生函數呼叫時
add[int](2,3),int 即為型別實參, 這一呼叫我們也叫做實例化, 即確定型別實參。
MyStruct[T] 是一個泛型結構體, 可以為泛型結構體定義方法。
int | string就是類型的集合。那麼如何對類型的集合進行複用呢?這裡就使用了接口來定義。下面就是一個型別集合的定義。因此, 我們可以定義一個泛型函數
add[T Signed](x, y T) T
MyInt 類型實作了 Add 方法, 因此可以轉換為
MyInterface。
type MyInterface interface { Add(x, y int) int } type MyInt int func (mi myInt) Add(x, y int) int { return x + y } func main() { var mi MyInterface = myInt(1) fmt.Println(mi.Add(1, 2)) }如果我們換個角度來思考一下,
MyInterface 可以看作一個型別集合, 即包含了所有實作
add 方法的型別。那麼, MyInterface 就可以當作型別集合使用。例如, 我們可以定義如下泛型函數。
func I[T MyInterface](x, y int, i T) int { return i.Add(x, y) }
在泛型中, 我们的类型集合不仅仅是实现接口中定义方法的类型, 还需要包含基础的类型。因此, 我们可以对接口的定义进行延伸, 使其支持基础类型。为了保证向前兼容, 我们需要对接口类型进行分类:
只包含方法的集合, 既可以当作类型集合, 又可以作为数据类型进行声明。如下面的 MyInterface
。还有一个特殊的接口类型 interface{}, 它可以用来表示任意类型, 即所有的类型都实现了它的空方法。在 1.8 之后可以使用 any 进行声明。
type any = interface{} type MyInterface interface { Add(x, y int) int String() string String() string // 非法: String 不能重复声明 _(x int) // 非法: 必须要有一个非空的名字 }
可以通过接口组合的形式声明新的接口, 从而尽可能的复用接口。从下面的例子可以看出, ReadWriter
是 Reader
和 Write
的类型集合的交集。
type Reader interface { Read(p []byte) (n int, err error) Close() error } type Writer interface { Write(p []byte) (n int, err error) Close() error } // ReadWriter's methods are Read, Write, and Close. type ReadWriter interface { Reader // includes methods of Reader in ReadWriter's method set Writer // includes methods of Writer in ReadWriter's method set }
上面说的接口都必须要实现具体的方法, 但是类型集合中无法包含基础的数据类型。如: int, float, string...。通过下面的定义, 可以用来表示包含基础数据类型的类型集合。在 golang.org/x/exp/constraints
中定义了基础数据类型的集合。我们可以看到 ~
符号, 它表示包含潜在类型为 int | int8 | int16 | int32 | int64 的类型, |
表示取并集。Singed
就表示所有类型为 int 的类型集合。
// Signed is a constraint that permits any signed integer type. // If future releases of Go add new predeclared signed integer types, // this constraint will be modified to include them. type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } type myInt int // 潜在类型为 int func add[T constraints.Integer](x, y T) T { return x + y } func main() { var x, y myInt = 1, 2 fmt.Println(add[myInt](x, y)) }
下面来看一些特殊的定义
// 潜在类型为 int, 并且实现了 String 方法的类型 type E interface { ~int String() string } type mInt int // 属于 E 的类型集合 func (m mInt) String() string { return fmt.Sprintf("%v", m) } // 潜在类型必须是自己真实的类型 type F interface { ~int // ~mInt invalid use of ~ (underlying type of mInt is int) // ~error illegal: error is an interface } // 基础接口可以作为形参和类型参数类型, 通用类型只能作为类型参数类型, E 只能出现在类型参数中 [T E] var x E // illegal: cannot use type E outside a type constraint: interface contains type constraints var x interface{} = E(nil) // illegal: cannot use interface E in conversion (contains specific type constraints or is comparable)
由于泛型使用了类型参数, 因此在实例化泛型时我们需要指定类型实参。 看下面的 case, 我们在调用函数的时候并没有指定类型实参, 这里是编译器进行了类型推导, 推导出类型实参, 不需要显性的传入。
func add[T constraints.Integer](x, y T) T { return x + y } func main() { fmt.Println(add(1, 1)) // add[int](1,1) }
有时候, 编译器无法推导出具体类型。则需要指定类型, 或者更换写法, 也许可以推断出具体类型。
// 将切片中的值扩大 func Scale[E constraints.Integer](s []E, c E) []E { r := make([]E, len(s)) for i, v := range s { r[i] = v * c } return r } func ScaleAndPrint(p Point) { r := Scale(p, 2) r.string() // 非法, Scale 返回的是 []int32 } type Point []int32 func (p Point) string() { fmt.Println(p) } // 方法更新,这样传入的是 Point 返回的也是 Point func Scale[T ~[]E, E constraints.Integer](s T, c E) T { r := make([]E, len(s)) for i, v := range s { r[i] = v * c } return r }
go 是在 1.8 版本中开始引入泛型的。下面主要介绍一下什么时候使用泛型:
在 go 中, 提供以下容器类型:map, slice, channel。当我们用到容器类型时, 且逻辑与容器具体的类型无关, 这个时候可以考虑泛型。这样我们可以在调用时指定具体的类型实参, 从而避免了类型断言。例如,下面的例子, 返回 map 中的 key。
// comparable 是一个内置类型, 只能用于对类型参数的约束。在 map 中, key 必须是可比较类型。 func GetKeys[K comparable, V any](m map[K]V) []K { res := make([]K, 0, len(m)) for k := range m { res = append(res, k) } return res }
对于一些通用的结构体, 我们应该使用泛型。例如, 栈、队列、树结构。这些都是比较通用的结构体, 且逻辑都与具体的类型无关, 因此需要使用泛型。下面是一个栈的例子:
type Stack[T any] []T func (s *Stack[T]) Push(item T) { *s = append(*s, item) } func (s *Stack[T]) Pop() T { if len(*s) == 0 { panic("can not pop item in emply stack") } lastIndex := len(*s) - 1 item := (*s)[lastIndex] *s = (*s)[:lastIndex] return item } func main() { var s Stack[int] s.Push(9) fmt.Println(s.Pop()) s.Push(9) s.Push(8) fmt.Println(s.Pop(), s.Pop()) }
有些类型会实现相同的方法, 但是对于这些类型的处理逻辑又与具体类型的实现无关。例如: 两个数比大小, 只要实现 Ordered 接口即可进行大小比较:
func Min[T constraints.Ordered](x, y T) T { if x < y { return x } return y } func main() { fmt.Println(Min(5, 6)) fmt.Println(Min(6.6, 9.9)) }
go 在引入泛型算是一次较大的改动。我们只有弄清楚类型参数、类型约束、类型集合、基础接口、通用接口、泛型函数、泛型类型、泛型接口等概念, 才能不会困惑。核心改动点还是引入了类型参数, 使用接口来定义类型集合。
当然,也不能为了使用泛型而使用泛型。还是要具体的 case 具体来分析。 简单的指导原则就是, 当你发现你的代码除了类型不同外, 其余代码逻辑都相同; 或者你写了许多重复代码, 仅仅是为了支持不同类型; 那么你可以考虑使用泛型。
推荐学习:Golang教程
以上是深入了解golang中的泛型(Generic)的詳細內容。更多資訊請關注PHP中文網其他相關文章!