内置范型

Go语言目前(Go 1.12)不支持自定义范型,只支持一等公民组合类型的内值范型。 我们可以用各种一等公民组合类型来组合出无穷个类型。

本文将展示一些自定义组合类型的例子并解释如何解读这些自定义类型。

一些自定义组合类型的例子

Go中的组合类型字面表示设计得非常直观和易于解读。 即使对于一些非常复杂的类型,我们也几乎不可能在解读它们的字面形式中迷失。 下面将从简单到复杂列出一些自定义组合类型的例子并进行解读。

先看一个简单的例子:
[3][4]int

当解读一个类型的字面形式时,我们应该从左到右进行解读。 左边开头的[3]表示着这个类型为一个数组类型,它右边的整个部分为它的元素类型。 对于这个例子,它的元素类型为另外一个数组类型[4]int。 此另外一个数组类型的元素类型为为内值类型int。 第一个数组类型可以被看作是一个二维数组类型。

一个使用此数组类型的例子:
package main

import (
	"fmt"
)

func main() {
	matrix := [3][4]int{
		{1, 0, 0, 1},
		{0, 1, 0, 1},
		{0, 0, 1, 1},
	}

	matrix[1][1] = 3
	a := matrix[1] // 变量a的类型为[4]int
	fmt.Println(a) // [0 3 0 1]
}

类似的,

下面是另一个自定义组合类型:
chan *[16]byte

最左边的chan关键字表明此类型是一个数据通道类型。 chan关键字右边的整个部分*[16]byte表示此数据通道类型的元素类型,此元素类型是一个指针类型。 此指针类型的基类型为*右边的整个部分:[16]byte,此基类型为一个数组类型。 此数组类型的元素类型为内置类型byte

一个使用此数据通道类型的例子:
package main

import (
	"fmt"
	"time"
	"crypto/rand"
)

func main() {
	c := make(chan *[16]byte)

	go func() {
		// 使用两个数组以避免数据竞争。
		var dataA, dataB = new([16]byte), new([16]byte)
		for {
			_, err := rand.Read(dataA[:])
			if err != nil {
				close(c)
			} else {
				c <- dataA
				dataA, dataB = dataB, dataA
			}
		}
	}()

	for data := range c {
		fmt.Println((*data)[:])
		time.Sleep(time.Second / 2)
	}
}

类似的,类型map[string][]func(int) int为一个映射类型。 此映射类型的键值类型为内值类型string,右边剩余的部分为此映射类型的元素类型。 []表明此映射的元素类型为一个切片类型,此切片类型的元素类型为一个函数类型func(int) int

一个使用了此映射类型的例子:
package main

import "fmt"

func main() {
	addone := func(x int) int {return x + 1}
	square := func(x int) int {return x * x}
	double := func(x int) int {return x + x}

	transforms := map[string][]func(int) int {
		"inc,inc,inc": {addone, addone, addone},
		"sqr,inc,dbl": {square, addone, double},
		"dbl,sqr,sqr": {double, double, square},
	}

	for _, n := range []int{2, 3, 5, 7} {
		fmt.Println(">>>", n)
		for name, transfers := range transforms {
			result := n
			for _, xfer := range transfers {
				result = xfer(result)
			}
			fmt.Printf(" %v: %v \n", name, result)
		}
	}
}

下面是一个看上去有些复杂的类型:
[]map[struct {
	a int
	b struct {
		x string
		y bool
	}
}]interface {
	Build([]byte, struct {x string; y bool}) error
	Update(dt float64)
	Destroy()
}

让我们从左到右解读此类型。 最左边开始的[]表明这是一个切片类型,紧跟着的map关键字表明此切片类型的元素为一个映射类型。 map关键字后紧跟的一对方括号[]中的结构体类型字面形式表明此映射的键值类型为一个结构体类型。 此中括号右边的整个部分表明此映射的元素类型为一个接口类型。此接口类型指定了三个方法。 此映射的键值结构体类型有两个字段,第一个字段的名称和类型为a和内置类型int; 第二个字段的名称为b,它的类型为另外一个结构体类型struct {x string; y bool}。 此另外一个结构体类型也有两个字段:内置string类型的字段x和内置bool类型的字段y

请注意第二个结构体类型也被用做刚提及的接口类型所指定的其中一个方法中的其中一个参数类型。

我们经常将复杂类型的各个组成部分单独提前声明为一个类型名,从而获得更高的可读性。 下面的代码中的类型别名T和上面刚解读的类型表示同一个类型。
type B = struct {
	x string
	y bool
}

type K = struct {
	a int
	b B
}

type E = interface {
	Build([]byte, B) error
	Update(dt float64)
	Destroy()
}

type T = []map[K]E

当前Go中的内置范型现状

Go中当前的内置范型除了上述类型组合,还有一些支持范型的内置函数。 比如,内置函数len可以用来获取各种容器值的长度。 unsafe标准库包中的函数也可以被看作是支持范型的内置函数。

Go目前不支持自定义范型这一事实有时候确实造成了一些不方便。 比如,math标准库包中的函数的参数类型和结果类型都为内值类型float64。 当调用这些函数的时候,我们必须将非float64类型的实参值转换为float64,然后还需把结果从float64类型转回来。 这不仅不方便,而且代码执行效率不高。

幸运的是,很多Go项目对自定义范型并没有很强的需求。 并且,缺少自定义范型的不足可以通过(在运行时刻)使用反射机制和(在编译时刻)代码生成来部分地弥补。

未来的Go自定义范型

Go语言核心设计和开发团队并不排斥在Go中加入自定义范型, 他们只是还没找到一个可以保持Go的简洁性的自定义范型方案。 未来的Go版本中还是很可能会支持自定义范性的。 目前已经有了一个Go自定义范型草案。 此草案仍旧处于早期朦胧阶段,最终的设计可能会和此草案差别很大。

Go语言101项目目前同时托管在GithubGitlab上。 欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏