Golang从1.8版本开始支持泛型,相信很多人跟我一样,都是观望的状态。如今Golang都已经发布到1.19版本了,我觉得可以尝试一下。

两数相加例子

在之前对Golang引入泛型的呼声中,一个最常见的例子,就是类似将两数相加的场景。 以支持整数和浮点数相加为例,在支持泛型之前,我们需要实现两个函数,并分别调用。 代码类似:

package main

func AddInt(v1, v2 int) int             { return v1 + v2 }
func AddFloat64(v1, v2 float64) float64 { return v1 + v2 }

func main() {
	AddInt(1, 2)
	AddFloat64(0.1, 0.2)
}

在支持泛型后,我们只需要定义一个函数,并直接调用:

package main

func Add[T int | float64](v1, v2 T) T { return v1 + v2 }

func main() {
	Add[int](1, 2)
	Add[float64](0.1, 0.2)
}

甚至还可以进一步简化为:

package main

func Add[T int | float64](v1, v2 T) T { return v1 + v2 }

func main() {
	Add(1, 2)
	Add(0.1, 0.2)
}

当然,后面这种最简化的版本能行,是因为可以通过函数参数 v1v2的类型,反推出 类型参数 T。如果没有参数的,或者函数参数无法推导出类型是,不能简化。

HTTP API封装的例子

虽然两数相加的例子很典型,不过我平时也几乎没有在意过这个问题。毕竟在静态语言中,大多数时候已经确定数据的类型,并不需要真的同一个地方调用三种函数。

但是在我封装HTTP API封装的时候,每次都要纠结一下,如果支持泛型,是否能让我的代码更合理一些。

具体说起来,对于常见的HTTP API,返回值大多数是一个JSON结构的文本。 这个结构虽然每个API都不一样,但是他们又通常都有部分相同的信息,比如错误码、错误消息。例如:


type CommonResponse struct{
	ErrCode int
	ErrMsg string
}

func (resp CommonResponse) CheckError() error {
	if resp.ErrCode == 0 && resp.ErrMsg == "" {
		return nil
	}

	return fmt.Errorf("[%d]%s", resp.ErrCode,resp.ErrMsg)
}

type Responser interface {
	CheckError() error
}

type LoginResponse struct{
	CommonResponse
	Result struct{
		Token  string
		Expire int
	}
}

type HomeResponse struct{
	CommonResponse
	Result struct{
		Username   string
		LoginCount int
	}
}

作为API的封装者,我通常希望能够在API内部,方便的、统一的实现下面两个功能:

  • 统一的JSON结构解析
  • 统一的错误处理

不使用泛型

在没有泛型的年代,我们的处理方式是,先初始化API的响应数据结构。然后将其指针作为函数的参数传递(常见的出参用法)。这种方式能工作的前提,是因为json.Encoder.Decode()本身就支持any类型的指针。当然,也存在一定的风险。比如理论上调用方可以存在传nil之类不合理的使用,导致代码panic。

同时,为了做统一的错误处理,我们在公共结构CommonResponse上实现了一个CheckError的方法,并将该方法定义为一个接口。用于约束传入的参数。这种方式好处就是对入参做了个最最最基本的类型检查。但是由于Golang的接口和类继承不一样,任何实现了该方法、但并不是CommonResponse或者CommonResponse组合出来的结构,都可以在这里使用。存在一定的风险。

func request(url string, resp Responser) error {
	res, err := http.Get(url)
	if err!=nil {
		return err
	}
	defer res.Body.Close()

	err := json.NewDecoder(res.Body).Decode(resp)
	if err!=nil {
		return err
	}

	if err:=resp.CheckError(); err!=nil {
		return err
	}

	return nil
}

func APILogin() (LoginResponse,error) {
	var resp LoginResponse
	err := json.Unmarshal(data, &resp)
	return resp,err
}

func APIHome() (HomeResponse,error) {
	var resp HomeResponse
	err := json.Unmarshal(data, &resp)
	return resp,err
}

使用泛型

在有了泛型的支持后,我们就可以把响应的数据接口,作为类型参数声明起来。然后执行调用的时候,明确指定类型参数即可。

这种方式在底层函数中,传递给json库的仍然是一个any类型的指针。但是这个指针是在这个函数中,使用明确的类型进行初始化的,因此不存在调用不合理调用的风险。

同时,理论上讲,泛型类型T的类型约束,可以直接细化到所有的API响应结构列表。避免任何满足CommonResponse.CheckError函数签名的接口,都可以作为参数传入的问题。不过这种问题影响不大,最多就是解析不到合理的数据,不会panic。

func request[T Responser](url string) (T, error) {
	res, err := http.Get(url)
	if err!=nil {
		return err
	}
	defer res.Body.Close()

	var respInfo T
	err := json.NewDecoder(res.Body).Decode(&respInfo)
	if err!=nil {
		return err
	}

	if err:=resp.CheckError(); err!=nil {
		return err
	}

	return nil
}

func APILogin() (LoginResponse,error) {
	return request[LoginResponse](data)
}

func APIHome() (HomeResponse,error) {
	return request[HomeResponse](data)
}

两项对比

单纯从代码上来看,在HTTP API封装的场景下,是否使用泛型对代码量的影响基本没有。但是从代码质量上来看,无疑泛型相对来讲更完善一些。