golang泛型简述
简述Generics
Generics
是一种基本所有现代高级强类型编程语言中都会提供的特性,各个语言的实现模式不一样,例如:
- java基于类型擦除(Type Erasure)实现,简单来说就是被在运行时被实际转换为声明的类型(如果没有声明就是object)。这意味着在运行时我们已经丢失实际的类型。
cpp
基于Template
来实现泛型,模版在编译时进行实例化。golang
基于 类型参数 和 约束 来实现泛型。
总的来说优缺点如下表所示:
特性/语言 | Java 泛型 | C++ 模板 | Go 泛型 |
---|---|---|---|
实现方式 | 类型擦除(Type Erasure) | 编译时实例化 | 类型参数和约束 |
类型安全 | 有 | 有 | 有 |
编译时类型检查 | 有 | 有 | 有 |
灵活性 | 差 | 极高,支持模板化和模板元变成 | 一般 |
复杂性 | 简单 | 极复杂 | 简单 |
运行时开销 | 有 | 无 | 有 |
缺点 | - 运行时无法获取泛型类型信息 - 不能创建泛型数组 - 不能使用基本类型作为泛型参数 - 不能在静态上下文中使用泛型类型参数 - 不能直接实例化泛型类型参数 |
- 编译速度慢:模板实例化可能导致编译时间增加 - 代码膨胀:大量模板实例化可能导致生成的二进制代码体积增大 - 错误信息复杂:模板错误信息可能难以理解和调试 |
- 相对较新的特性,生态系统和社区支持可能还不够成熟 - 可能引入一些性能开销,具体需要根据实际使用场景评估 |
Type Erasure 的运行时类型
下面的代码中,结果是 true,这是因为 String 和 Integer 在运行时都被擦除为 Object 类型。
1 | public class TypeErasureExample { |
Type Erasure 的运行时安全保证
在下面的代码中,只是会有一些 WARNING
信息,然而实际代码是可以执行的,只是在运行时会抛出异常
java.lang.ClassCastException
,这是因为由于我们使用了
<Integer>
作为限制,类型会被擦除为
<Integer>
而不是上面例子中的
Object
。然而,运行时会对类型进行检查,并转换 String 到
Integer。
1 | public class MyList<T> { |
1 | public class IntegerList extends MyList<Integer> { |
CPP的模版
CPP的模板是在编译时,对模版代码根据用户调用进行实例化,也就是说,模版对应于java里的class,而最终会生成一个真正的函数,对应于java里的实例(instance)。灵活性非常之高, 但是也极度复杂,写多了掉头发。
PS:在我多年工作经验中,几乎没有碰见过常用cpp模版的人。
1 | // 定义一个模板函数,用于比较两个值并返回较大的那个 |
Golang Generics
golang generics 的定义
golang 的 generics 主要包含三个特性:
- Type parameters for functions and types
- Type sets defined by interfaces
- Type inference
其中:
- 声明了函数,struct等golang常用特性应该如何声明,使用 generics;
- 声明了如何在
interface
中定义 generics;- 声明了编译器如何对generics的类型进行推导;
我们会在后面对这几个主要特性进行探讨。
几个简单的例子
type parameter for functions and types
下面的例子说明了如何在function中使用泛型。
1 | // Sum sums the values of map m. |
Type sets defined by interfaces
下面的例子中说明了如何在interface中使用泛型
1 | // Number declare Number interface type to use as a type constraint |
type infer
下面的例子中说明了如何手动的指定泛型类型以及如何使用编译器的类型推导(type infer)来实现函数调用。
1 | func sumInt64() { |
struct with generics
下面的例子定义了一个泛型接口,但是泛型接口的实例是非泛型的
1 | type Id[T interface{}] interface { |
如果我们想要泛型实例本身也是泛型的,那么他们本身也必须是泛型的,对于上面的例子中的
PageView
,他并不是一个泛型的结构体,所以他无法实现泛型的接口。如果我们需要实现一个泛型的接口,我们可以这样处理:
1 | type IntNumber interface { |
使用接口作为泛型的返回值/类型断言
注意,这里必须要是使用类型断言将接口类型转换为T并返回。
1 | type Gender interface { |
对底层类型基于 ~
进行约束
~
是一种类型约束符号,用于表示一组类型的集合。具体来说,~int
可以用于表示所有底层类型为int的类型。下面是一个简单的例子:
1 | type MyInt int |
泛型可能涉及到的断言
参考 Cannot use type assertion on type parameter value,我们在使用泛型的过程中,可能会遇到一些类型无法使用类型断言的情况,可以使用如下方法绕过限制。
x
's typeT
is a type parameter, not an interface. It is only constrained by an interface. The Go (revised1.18
) language spec explicitly states type parameters are not allowed in a type assertion:For an expression
x
of interface type, but not a type parameter, and a typeT
... the notationx.(T)
is called a type assertion.
1 | func GenericIsInt[T interface{}](x T) bool { |
一些常见的错误
golang目前不支持方法泛型
1 | type Person struct{} |
Cannot use a type parameter as RHS in type declaration
Go 语言不允许在类型声明中直接使用类型参数作为右侧的类型,因为在部分情况下可能会产生歧义。
1 | // Cannot use a type parameter as RHS in type declaration |
Invalid recursive type
Go不允许直接嵌套自身类型的实例,这是为了防止无限递归导致的编译器和内存问题,我们可以通过指针来避免这个问题。
1 | // ERROR : Invalid recursive type 'Tree' |
generics EBNF
现在我们差不多了解了golang generics的一些常用用法了,我们可以再回到go spec的一些EBNF来查看泛型的使用。
Prerequisite
这里,必须先解释一些重要的概念:
type arguments
类型参数是用于定义泛型类型或泛型函数的占位符,通常在定义时指定。type parameter
类型实参是用于替换类型参数的具体类型,当使用泛型类或者泛型函数时,需要提供具体的类型作为类型实参。
例如下面的这个例子:
1 | // 定义一个泛型函数,T 是 type arguments |
Types
A type may be denoted by a type name, if it has one, which must be followed by type arguments if the type is generic.
1 | Type = TypeName [ TypeArgs ] | TypeLit | "(" Type ")" . |
Type parameter declarations
A type parameter list declares the type parameters of a generic function or type declaration. The type parameter list looks like an ordinary function parameter list except that the type parameter names must all be present and the list is enclosed in square brackets rather than parentheses [Go 1.18].
1 | TypeParameters = "[" TypeParamList [ "," ] "]" . |
Instantiations
A generic function or type is instantiated by substituting
type arguments
for the type parameters
[Go 1.18]. Instantiation
proceeds in two steps:
- Each type argument is substituted for its corresponding type parameter in the generic declaration. This substitution happens across the entire function or type declaration, including the type parameter list itself and any types in that list.
- After substitution, each type argument must satisfy the constraint (instantiated, if necessary) of the corresponding type parameter. Otherwise instantiation fails.
Instantiating a type results in a new non-generic named type; instantiating a function produces a new non-generic function.
When using a generic function, type arguments may be provided explicitly, or they may be partially or completely inferred from the context in which the function is used. Provided that they can be inferred, type argument lists may be omitted entirely if the function is:
- called with ordinary arguments,
- assigned to a variable with a known type
- passed as an argument to another function, or
- returned as a result.
here is a example below:
1 | // sum returns the sum (concatenation, for strings) of its arguments. |
A partial type argument list cannot be empty; at least the first argument must be present. The list is a prefix of the full list of type arguments, leaving the remaining arguments to be inferred. Loosely speaking, type arguments may be omitted from "right to left".
1 | func apply[S ~[]E, E any](s S, f func(E) E) S { … } |