跳到主要内容
  1. Skills/
  2. 后端编程/

go 1.17 发布,即将正式支持泛型

··字数 3701·8 分钟
go

golang 开发者福音: 随着 go 1.17发布,期待已久的 泛型 ,也就是 类型参数,即将被正式支持了。目前已发现泛型相关的代码已被合并到 go 的代码库中。而且可以通过 go1.17 编译运行简单的例子。

对于很多 java/c++ 开发者来说,这一功能似乎是习以为常,而且是现代编程语言必须具备的,但在 golang 生态中,针对是否在 go 中添加这一功能,语言设计者与社区可是经过是一个非常长期的讨论周期。 随着官方在语言级别的泛型支持,标志着长期有关于泛型设计的讨论告一段落,以后将出现的是,官方针对泛型实现的优化与社区各种泛型的使用讨论。

什么是泛型 #

本节仅是简单讲解泛型,方便 go 初学者快速了解泛型的作用。若你已具有相当的 java/c++ 开发经验,这一节的内容对你来说并无太大价值。

泛型是具有 强类型系统的编程语言 中的一种范式,它允许开发者使用一种泛化式的标记,来指定类型,在编译或者运行时实例化才明确数据类型,以此来减少数据类型不同但流程相同的重复代码。从功能上看,即可以是类似于宏或者代码生成工具那样帮助开发者填充模板代码,又可以类似于面向对象中,使用基础类来表示可以接收派生类实例的范围(这里仅仅是类比,而不是泛型跟面向对象之间真的有关系,泛型本身影响的或者实际用途,是在于声明数据类型)。有些c开发者会联想到宏,有些 rust 开发者会联想到 trait bound。不同的视角会有不同的看法。

泛型是数据类型的通用化表达,可以用于函数参数、函数的返回值类型、数组元素的数据类型、面向对象中类的属性、方法参数与返回值等等的数据类型。在不同的程序设计语言中,泛型可能有多种不同的叫法,如 java 叫泛型(java 1.5就出现了),而 haskell 叫参数多态,在C++中就是众所周之的模板。而在一些书籍或者博客中,有人会把它称为parameterized type 或 type parameter,意为参数类型、参数化类型、类型参数,指用于描述数据类型的参数的类型。呵呵,好拗口。可在静态类型语言(通过编译器或者宏系统)中用来辅助生成代码,可在动态类型语言中,用来约束数据类型。

// 以下是一段伪代码
数组: [T] {
   append(新元素: T) 
}
T 为某数据类型,如整数 int 或 字符串 string

在 C++ 中称泛型为模板,也许是最直白理解。是的,不管叫什么名字,从代码复用或者模板代码的角度,是为开发者提供了一种避免由于仅数据类型不同但逻辑过程相同的代码精简手段。比如上面的伪代码,对于数组这个数据结构来说,不管数组中的元素是什么类型,在一个数组的尾巴上,追加一个符合该数组元素类型定义的数据的过程,应该是相同的吧。如果没有这种泛型或者模板化技术,开发者是不是要针对每一种不同的数据类型的相同逻辑过程都分别编写一遍代码呢!😖☹️😣😫🥺😤😠

泛型在各语言中的书写形式上,不像上面那段伪代码一样,并没有统一的格式。有些语言在泛型理念出现前就已经存在,或者在语言设计时没打算一开始就支持泛型(如 go,有兴趣的可以看看 为什么 Go 语言没有泛型 ,有悟不想陷入口水战),你可能会看到非常奇怪别扭的书写形式。不过,如果你见到类似于如下形式的代码时,大概是发现了带有泛型定义的代码了:

函数名1<T> (参数名1: T, 参数名2参数类型): 返回值类型 {
     return ...
}
函数名2<T> (参数名1: T, 参数名2参数类型): T {
     return ...
}
类1<T> {
    属性1 T;
    属性2 string
    方法1(参数: T): 返回值类型 { return ... }
    方法2(参数): T { return ...}
}

名称 后面使用类似于 <T>(小括号已经被用于函数签名的参数列表) 来标记该名称所定义函数或者类中使用到了泛型,应用在具体的 类属性类方法参数返回值。这个字母不一定是 T,你可以使用其它喜欢的字母,只要符合具体的编程语言要求即可。出于对泛型类型的约束,有些编程语言可以对这个 T 所要表示的数据类型进行限定,将其约束在某个许可的范围内,而不是 any 类型。如在 java 中,经常看到 <E extends 某个基类> 这个形式来约定泛型仅代指来自某个基类或其派生子类。(已经学习了 go interface 的人,是否已经联系起来了呢, interface{} 零接口就是一个超级泛型 – any类型,可以表现任意数据类型)

各种编程语言的本身设计特点与泛型实现差异,其底层实现会分为编译时确定、运行时确定等。因为泛型本身并不是实际的数据类型,需要运行之前实例化为具体数据类型,这个实例化过程可以在运行时根据实例值来确定,但能否优化,还要看编译器和语言本身有没有能力在生成机器码之前就推断出具体数据类型。这个差异会对性能表现造成影响,若非编写高性能的程序或者非常繁忙的程序模块,这种差异不需要过多关注。不过,这是一个让语言设计者头痛的问题,需要在编程效率、编译速度、运行速度之间权衡,作为开发者的你需要了解的是,你使用的那个开发语言采用的是哪种方式,具体支持哪些场景的泛型,这些内容大概就几张 A4 纸的篇幅就可以说明清楚。

当前支持泛型的编程语言很多,有悟无法列出完整清单,在常见的 C++、 java、C#、swift、scala 这些被大规模使用的编程语言中都有支持。

在 go 1.17 之前的类泛型表示 #

em….

copy / paste,你没看错。

利用 sublime text 或者 vscode 这些支持 multi selected(同时多选)的牛叉现代编辑器。

代码生成器。

其它任何可以让你减少敲击键盘的动态方式可者静态代码生成器方式。要注意动态方式是否能够满足你的程序的性能要求。

go的官方团队 2020年6月发布文章 The Next Step for Generics ,告诉大家 go 中的泛型设计方案。

go 1.17 之后的泛型表示 #

尘埃落定,经历多年的讨论甚至由此所引发的口水战、人身攻击终于划上句号。

上周(8月16日) golang 1.17 正式发布,有开发者在 go 的代码库中发现,泛型支持的相关代码已经被合并。但在 go 1.17 的发行说明 Go 1.17 is released 中没有提及泛型。

没关系,尝鲜还是可以的。 下面的示例代码,来自 go 团队为泛型所建的试验运行环境,随意找个目录新建个 helloworld.go 测试下。

// https://go2goplay.golang.org/
package main

import (
    "fmt"
)

// The playground now uses square brackets for type parameters. Otherwise,
// the syntax of type parameter lists matches the one of regular parameter
// lists except that all type parameters must have a name, and the type
// parameter list cannot be empty. The predeclared identifier "any" may be
// used in the position of a type parameter constraint (and only there);
// it indicates that there are no constraints.

func Print[T any](s []T) {
    for _, v := range s {
        fmt.Print(v)
    }
}

func main() {
    Print([]string{"Hello, ", "playground\n"})
}

有悟兴高采烈的直接运行 go run,得到的结果长这样:

(在编写本文时8月23日,不知道什么原因,brew 迟迟不更新 go 到1.17)

d:\projects\go\examples\generic
λ go version
go version go1.17 windows/amd64

d:\projects\go\examples\generic
λ go run hello.go
# command-line-arguments
.\hello.go:14:6: missing function body
.\hello.go:14:11: syntax error: unexpected [, expecting (

哟吼,上面的错误提示是编译无法泛型语法[T any],上网查了下,需要加个编译参数 -G=3,又得到这个错误。

d:\projects\go\examples\generic
λ go run --gcflags=-G=3 hello.go
# command-line-arguments
.\hello.go:14:6: internal compiler error: Cannot export a generic function (yet): Print

Please file a bug report including a short program that triggers the error.
https://golang.org/issue/new

没问题,只是提示 『Cannot export』(不能导出,现在都流行叫导出,用接触过 java 的开发者熟悉的话说,就是 public,函数名开头大写表示 public,在模块外可引用)。那把 Print 改为 print 试试:

d:\projects\go\examples\generic
λ go run --gcflags=-G=3 hello.go
Hello, playground

可以了,离正式支持泛型很接近了。估计在稍后的新版本就会公开说明。针对这个新的编译器参数-G,有人发现代码库里也已经调整过来了,默认为 -G=3cmd/compile: enable -G=3 by default ,以后的新版本就编译器会自动启用泛型类型检查支持。

在官方的版本发行公告中有正式说明之前,请勿用于生产环境。

通过上面示例,确定了 go 中采用 [T] 这种方式来表示泛型,不是其它语言(如 java, typescript)中的 <T>,更不是 (T)

在之前的草案中,就出现了 func Add(type T)(a, b T) T { return a + b } 这样的备选,这让别人怎么阅读代码嘛,函数形参与类型参数,傻傻分不清楚。

延伸 – 其他编程语言中的泛型 #

本节展示在几种主流编程语言中使用泛型,纯粹仅是满足一下好奇心,直接要学习该语言的泛型功能的,都要去详细阅读对应语言的完整手册。

在随意目录下新建一个文件 Hello.java,注意,文件名要与类名相同。

// Hello.java
public class Hello
{
   // 泛型方法 print                         
   public static < E > void print( E[] input )
   {
      // 输出数组元素            
         for ( E e : input ){        
            System.out.printf( "%s ", e );
         }
         System.out.println();
    }
 
    public static void main( String args[] )
    {
        System.out.println( "java 泛型示例: " );

        Integer[] intArray = { 1, 2, 3, 4, 5 };
        Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
 
        System.out.println( "整型: " );
        print( intArray  ); // 传递一个整型数组
 
        System.out.println( "\n字符型: " );
        print( charArray ); // 传递一个字符型数组
    } 
}

编译并运行:


~/Projects/go/examples/generic
➜  javac Hello.java && java Hello
java 泛型示例:
整型:
1 2 3 4 5

字符型:
H E L L O
// https://www.runoob.com/cplusplus/cpp-templates.html
// hello.cpp
#include <iostream>
#include <string>
 
using namespace std;
 
template <typename T>
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 
int main ()
{
    cout << "C++ 泛型示例" << endl; 
 
    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl; 
 
    double f1 = 13.5; 
    double f2 = 20.7; 
    cout << "Max(f1, f2): " << Max(f1, f2) << endl; 
 
    string s1 = "Hello"; 
    string s2 = "World"; 
    cout << "Max(s1, s2): " << Max(s1, s2) << endl; 
 
   return 0;
}

编译并运行 c++ 程序:

~/Projects/go/examples/generic
➜  g++ -o hello_cpp hello.cpp && ./hello_cpp
C++ 泛型示例
Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World

关于 rust 中的泛型,请看官方说明文档, Generic Data Types

fn main() {

    println!("rust 泛型示例");
    
    let i = 39;
    let j = 20;
    println!("max(i,j): {}", max(i,j));

    let s1 = "Hello"; 
    let s2 = "World"; 
    println!("max(s1,s2): {}", max(s1,s2));
}
// https://doc.rust-lang.org/book/ch10-02-traits.html#using-trait-bounds-to-conditionally-implement-methods
fn max<T: std::cmp::PartialOrd>(a: T, b :T) -> T {
    if a < b {
        return b
    } else {
        return a
    }
}

rust 的类型声明系统非常强大,连比较整数与字符串这个简单的要求,都要加 std::cmp::PartialOrd 这个 trait bound 来限定。 编译并运行:

~/Projects/go/examples/generic
➜  rustc hello.rs -o hello_rs && ./hello_rs
rust 泛型示例
max(i,j): 39
max(s1,s2): World

javascript 是弱类型,可以使用 typescript 来为 javascript 添加强大的类型检查。现在流行的js 库都开始或者已经添加了类型检查。

function Max<T>(a: T, b :T): T {
    return a < b ? b:a
}

function main() {
    let i = 39;
    let j = 20;
    console.log("max(i,j): ", Max(i,j))

    let s1 = "Hello"; 
    let s2 = "World"; 
    console.log("max(s1,s2): ", Max(s1,s2))
}

main()

编译并运行(使用 deno run hello.tstsc hello.ts && node hello.js 运行):

~/Projects/go/examples/generic
➜  deno run hello.ts
Check file:///Users/youwu/Projects/go/examples/generic/hello.ts
max(i,j):  39
max(s1,s2):  World

~/Projects/go/examples/generic
➜  tsc hello.ts && node hello.js
max(i,j):  39
max(s1,s2):  World

备注说明 #

编写本文时,参考了一些文章,如: