The "past and present" of golang generics

I am participating in the "Nuggets · Sailing Program"

The "Past Life" of Generics

Q: Why do you need generics, don't you already have interface{}?

A: Children's shoes who have been exposed to static languages ​​such as C++ and Java have a certain understanding of generics. Generics play a great role in improving code reuse. Generics provide special data types in static languages ​​for accepting or returning parameters, avoiding the need to write multiple functions (methods) of different types for the same business code, resulting in a lot of repetitive code in the project.

A: Before the introduction of Go1.18, interface{} was usually used instead. Write an example below:

// Sum calculate the value of two value with the same data type.
//    	the data type can be int, float64, string
// a the first value
// b the second value
func Sum(a,b interface{}) interface{} {
	if reflect.TypeOf(a).Kind() != reflect.TypeOf(b).Kind() {
        return nil
    }
    switch reflect.TypeOf(a).Kind() {
	case reflect.Int:
		return reflect.ValueOf(a).Int() + reflect.ValueOf(b).Int()
	case reflect.Float64:
		return reflect.ValueOf(a).Float() + reflect.ValueOf(b).Float()
	case reflect.String:
		return reflect.ValueOf(a).String() + reflect.ValueOf(b).String()
	default:
		return nil
	}
}

func main() {
	a :=Sum(1,2)
	b := Sum(1.1,2.1)
	c := Sum("1","2")
	fmt.Printf("a:%v\t b:%v c:%v\n", a, b, c)
}
复制代码

Why do you want to refer to generics? I believe that the friends who have read the above examples can already understand; although inteface{} solves the acceptance and return of parameters of different data types, it needs to reflect the corresponding data types for classification processing, and frequent Type conversion, which results in code complexity and performance inefficiencies. Below we mainly give a further overview based on the content of the go generic proposal. Children's shoes with good English reading ability can directly read this document. go.googlesource.com/proposal/+/…

Q: What new features does the introduction of generics bring to go?

A: The Go 1.18.0 standard library officially introduced generic support, and its new features are as follows:

  1. Type parameters
  2. Constraints
  3. List of parameter types
  4. Generic types
  5. generic function
  6. Generic receiver (receiver)

golang generics "this life"

Several new features of Golang generics:

1. Generics syntax

Structure syntax:

type container[T any] struct{
    elem T
}
复制代码

Function syntax:

// 第一种
func Functionname[T any](p T) {
	...
}
// 第二种
func func1[T string | int | float64 ] (a T){

}

type Float interface {
	~float32 | ~float64
}

type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 
}

type Unsigned interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 
}

type Integer interface {
	Signed | Unsigned
}

type Ordered interface {
	Integer | Float | ~string
}
// 第三种
func func1[T Ordered] (a T){
	fmt.Printf("current value is : %v\n", reflect.TypeOf(a))
}
复制代码

2. Type constraints

golang introduces the identifier any, which can be used to replace interface{}, and is often used to represent any type in the use of generics;

func Filter[T any](s []T)  {
	for _, v := range s {
		_, _ = fmt.Printf("%v\t", v)
	}
	_, _ = fmt.Print("\n")
}
func main() {
    assembleSlice([]int{1,2,3,4})
	assembleSlice([]string{"chengdu", "sichuan"})
}
复制代码

slice:

func ForEach[T any](s []T, f func(ele T, i int, s []T)) {
	for i, ele := range s {
		f(ele, i, s)
	}
}

func main() {
	s := []int{1, 2, 3, 4, 5}
	ForEach(s, func(ele int, i int, s []int) {
		fmt.Printf("ele at %d is %d\n", i, ele)
	})

}
复制代码

map:

// keys return the key of a map
// here m is generic using K and V
// V is contraint using any
// K is restrained using comparable i.e any type that supports != and == operation
func keys[K comparable, V any](m map[K]V) []K {
// creating a slice of type K with length of map
    key := make([]K, len(m))
    i := 0
    for k, _ := range m {
        key[i] = k
        i++
    }
    return key
}
复制代码

3.类型参数(Type parameters) image.png 在这里我们以上图为例,简要阐述下几个概念:

类型形参 (Type parameter): T

类型约束 (Type constraint): any 也可以使用 int | float32 | float64 等具体确定的类型表示;

类型形参列表(type parameter list) : T int | float32 | float64

泛型类型(Generic type): 类型定义中带 类型形参类型

类型实参(Type argument): 具体的类型的参数

实例化(Instantiations): 传入类型实参确定具体类型的操作

“纸上谈来终觉浅,绝知此事要躬行指”,下面就通过一个例子来具象化上面的概念

type Slice[T int | float32 | float64 | string] []T

function main() {
    var a Slice[int] = []int{1, 2, 3}
	fmt.Printf("slice: %v\n", a) // console slice: [1,2,3]
    var b Slice[string] = []string{"thank", "you", "very", "much"}
    fmt.Printf("slice: %v\n", b)
    // var c Slice[T] = []int{1.1, 2.1, 3.1}
}
复制代码

这里 Slice[T] 泛型被类型实参 int 实例化为具体的类型 Slice[int]T类型形参int | float32 | float64 | stringT类型约束Slice[T int | float32 | float64 | string]泛型类型;变量 aint类型实参

4. 其他的泛型类型

type Float interface {
	~float32 | ~float64
}

type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 
}

type Unsigned interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 
}

type Integer interface {
	Signed | Unsigned
}
// 泛型接口
type Ordered interface {
    Integer | Float |~string
}
// 泛型结构体
type CustStruct[T int | float | string] struct {
    Code   int
    Message string
    Data   T
}
//  泛型通道
type CustChan[T int | string] chan T

复制代码

在这里讲一个小知识点, 代表了指定底层类型的所有类型。Go新增该标识符就会为解决自定义类型无法实例化的问题,例如:

type Int interface {
    int | int8 | int16 | int32 | int64
}

type Slice[T Int] []T

var s Slice[int]   // ✔

type CusInt int

var s1 Slice[CusInt] // ✘ 编译错误 缺失int类型约束,建议用~int
复制代码

当然~也有限制使用,后面的语法错误内会详细讲解。

当然本次1.18 还引入标准库的泛型改造,即 constraints标准包。以及内置泛型限制,匹配所有允许相等比较的类型comparable的新特性带给标准库的影响,这里我不再过多的展开讨论,具体可以参考

taoshu.in/go/no-chang…

go.dev/blog/intro-…

Golang泛型实现原理:

执行命令 go tool compile -S -N -l sliceGeneric.go 或者 go build -gcflags="-l -S" sliceGeneric.go > sliceGeneric.s 2>&1 编译成汇编代码,如下

FUNCDATA	$2, main.main.stkobj(SB)
LEAQ	main..dict.func1[string](SB), AX
LEAQ	go.string."a"(SB), BX
MOVL	$1, CX
PCDATA	$1, $0
CALL	main.func1[go.shape.string_0](SB)
复制代码

通过汇编代码, 我们知道GO泛型是基于编译器实现,泛型的函数/方法使用了字典来存放,这样避免了泛型函数/方法每一次调用创建不同的函数实例,附带不同类型的参数,该字典提供了关于类型参数的相关信息,允许单个函数实例对许多不同的类型参数正确运行。为了减少性能的损耗,提出了GC Shape Stenciling 方案。gcshape是类型的集合,当被指定为类型参数之一时,这些类型可以在泛型的实现中共享通用函数/方法的相同实例。对于具有单一类型参数的泛型类型的方法,只需对gcshape相同的类型参数进行一次实例化,最终转化为对应具体的类型。

Golang泛型使用指南:

泛型切片(Generic slice)

type Slice [T int|float64|string] []T
复制代码

泛型map:

type GenericMap[K int | string, V float32 | float64] map[K]V

var m GenericMap[string, float32] = map[string]float32 {
    "golang": 1.19,
    "java": 1.18
}
复制代码

泛型struct:

type Response [T string | int | float64 | bool] {
    Code int
    Msg string
    Data T
}
复制代码

注意:匿名结构体是不支持泛型的,例如:

resp := struct[T int|string] {
    Code int
    Msg  string
    Data T
}[int] {
    "error",
    2,
    3,
}
fmt.Println("response:", resp) // ✘ 编译不通过,语法错误
复制代码

泛型channel:

type CusChan[T any] chan T

func main() {
    // 声明string类型带缓冲的channel
    ch := make(CusChan[string], 5)
	ch <- "hello gopher"
	x := <- ch
	close(ch)
	fmt.Printf("%v\n",x)
}
复制代码

泛型函数:(generic function)

func Sum[T int|float64](a,b T) T {
  return a + b
}
复制代码

自定义约束类型

type cusInteger interface {
    int | int8 | int16 | int32 | int64
}

type cusNumb interface {
    int | float64
}

type cusFloat interface {
    float32 | float64
}

type DefinedNumb interface {
    cusInteger
    cusNumb
}

type DefinedNumb1 interface {
    cusInteger
    cusFloat
}

func ForEach[T DefinedNumb1](s []T) {
    for i,v := range s {
        fmt.Printf("the index: %v\t value: %v\n", i, v)
    }
}
复制代码

注意:自定义约束类型的交集不能为空

泛型方法:(receiver type)

在这里方法是指接收者(receiver)类型变量的函数。因此我们还是用例子说明这些概念:

type cusStr string

funcs cusStraddString(p string) string {
    s.append(p)
    fmt.Println(s)
}
复制代码

接收器泛型 + 参数泛型

type Number interface {
	~int | ~float32 | ~float64 | ~string
}

type CusSlice[T Number] []T

// 接收器泛型
func (c CusSlice[T]) add() T {
	var sum T 
	for _, v := range c {
		sum += v
	}
	return sum
}

type CusSlice1 []int

// func (c CusSlice1) add[T int](a T) []T { //✘ 编译不通过,方法不支持泛型
	
// }


// 接收器泛型 + 参数类型
func (s CusSlice[T]) add(a T) []T{
	s = append(s, a)
	return s
}


func main() {
    s := CusSlice[int] {1, 3, 4, 2, 1}
    sum := s.add()
	s = s.add(5);
	fmt.Printf("%v\n",s) // [1 3 4 2 1 5]
    fmt.Printf("sum:%v\n", sum) // sum: 11
}
复制代码

泛型接口:

在Go1.18之前,常常将接口定义为一个方法集。

An interface type specifies a method set called its interface

在1.18之后,接口由方法集变为了 类型集,类型集即类型的集合。

1.基本接口 (即只有方法)

type error interface {
	Error() string
}

// 基本泛型接口
type Cus[T int| string] interface {
    Error() T
}

func Error() string {
    return "error"
}
复制代码

2.一般接口(既有方法,又有类型)

type Reader interface {
    ~int | ~string | ~float32 | ~float64
	Read(p []byte) (n int, err error)
}

// 一般泛型接口
type CusInterface [T int| string] interface {
    int | string
    SetName(d T) T
    GetName() T
}
// 实例化(必须实现Read和底层类型的类型)
func SetName(d string) string {
    return d
}
func GetName() string {}

// var c CusInterface[string] = {} 	// ✘ 编译不通过,一般泛型接口只能作为一个类型约束
//cannot use type CusInterface[string] outside a type constraint: interface contains type 
复制代码

注意:一般泛型接口,只能被当做类型参数来使用,无法被实例化

语法使用错误集锦:

~限制使用:

type CusInt int

type CusType interface {
    ~CusInt  // ✘ 只能为基本类型
    ~error   // ✘ 不能为接口
}
复制代码

接口泛型限制

// 使用 | (union)连接多个类型时不能使用包含类型有交集的类型
type CusInt int

type CusInteger interface {
    ~int | CusInt      // ✘ 
    ~int | interface{CusInt} // ✔ 接口包含的类型除外
    ~interface{int} | CusInt // ✔ 
}

// 接口中包含的类型中出现空集, 使得该代码无任何意义
type Number interface {
    int
    float32    // 虽然不会报错,但是这里已经是空集,没有任何意义,尽量不要使用该写法
}

// 一般泛型接口当包换约束类型时,只能作类型参数使用【借用之前的例子】
type CusInterface [T int| string] interface {
    int | string
    SetName(d T) T
    GetName() T
}

func SetName(d string) string {
    return d
}

func main() {
    var c CusInterface[string] = {} 	// ✘ 编译不通过
}
复制代码

语法歧义导致错误

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type CusRead[T *Reader] []T   // ✘ 编译不通过,编译器识别为表达式,而非指针

type CusRead[T interface{*Reader}] []T // ✔ 推荐写法

type CusIo[T *Reader | *Writer] []T   // ✘ 编译不通过

type CusIo[T interface{*Reader | *Writer}] []T // ✔ 推荐写法
复制代码

针对语法歧义的情景,通常我们建议使用 interface{}

匿名形式不支持泛型

// 匿名函数
sum := func[T int|float32|float64|string] (a, b T) T {   // ✘ 编译不通过
    return a +b;
}
fmt.Println(sum(1, 2))
// 匿名结构体
resp := struct [T int|string|bool]{  // ✘ 编译不通过
		code int
		msg string
		data T
	}[bool]{
		code: 1,
		msg: "success",
		data: true,
	}
复制代码

Guess you like

Origin juejin.im/post/7147567560911749150