Go 1.1 值得关注的改动:
- 字符串和 rune 字面量的定义被细化,明确排除了 Unicode 代理对(surrogate halves)作为有效的 Unicode 码点。这确保了 Go 在处理 Unicode 字符时更加符合标准。
- Go 1.1 实现了 方法值(method values),这是一种绑定到特定接收器实例的函数。它与 方法表达式(method expressions)不同,后者是基于类型生成函数。这个改动是向后兼容的。
- 在 64 位平台上,int 和 uint 类型的大小从 32 位调整为 64 位。这使得在 64 位系统上可以分配超过 20 亿个元素的切片,但也可能影响依赖 int 为 32 位假设的代码行为。
- 在 64 位架构上,最大堆内存(heap)大小显著增加,从之前的几 GB 提升到了数十 GB,具体取决于系统。32 位架构的堆大小保持不变。
- 引入了一个重要工具:竞态检测器(race detector)。它可以帮助发现并发访问同一变量(且至少一个是写操作)导致的 bug,通过 go test -race 等命令启用。
- go 命令进行了一些改进以优化新用户体验,包括在找不到包时提供更详细的错误信息(包含搜索路径),以及 go get 命令强制要求设置有效的 $GOPATH。
- go test 命令在启用性能分析时不再删除测试二进制文件,方便进行后续分析。同时,新增了阻塞分析(blocking profile)功能 (-blockprofile),用于报告 goroutine 的阻塞点。
- bufio 包新增了 Scanner 类型,提供了一种更简单、更常用的方式来读取文本输入,例如按行或按空格分隔的单词读取。对于简单场景,它比之前的 ReadBytes, ReadString 等函数更方便。
下面是一些值得展开的讨论:
Unicode 代理对的处理调整
Go 1.1 对 Unicode 字符的处理进行了更严格的约束,特别是禁止了 代理对(surrogate halves)作为独立的 rune 值。代理对是 Unicode 标准中为了在 UTF-16 编码里表示超过 65535 的码点而设计的特殊范围,它们本身不代表任何字符,只能成对出现用于组合。
在 Go 1.1 中,编译器、库和运行时都强制执行了这一约束:一个独立的代理对码点被视为非法的 rune 值,无论是在 UTF-8 编码中,还是在单独的 UTF-16 编码中。当遇到这类非法值时(例如,从 rune 转换为 UTF-8),它会被视为编码错误,并产生 Unicode 替换字符 utf8.RuneError (U+FFFD)。
例如,以下代码在 Go 1.0 和 Go 1.1 中的行为不同:
在 Go 1.0 中,它会打印 "\ud800"。但在 Go 1.1 中,由于 0xD800 是一个非法的独立 rune 值,它会被替换字符替代,打印 "\ufffd"。
相应地,包含代理对 Unicode 值的 rune 和 string 常量现在也是非法的,例如 '\ud800' 或 "\ud800" 会导致编译错误。
不过,如果开发者显式地使用其 UTF-8 字节序列来创建字符串,例如 "\xed\xa0\x80"(这是 U+D800 的 UTF-8 编码),这种字符串仍然可以被创建。但是,当尝试将这种包含非法 UTF-8 序列的字符串解码为 rune 序列时(例如在 range 循环中),只会得到 utf8.RuneError。
输出:
此外,Go 1.1 允许 Go 源文件的起始处出现 Unicode 字节顺序标记(BOM)U+FEFF(其 UTF-8 编码为 EF BB BF)。虽然 UTF-8 编码本身不需要 BOM,但有些编辑器会添加它作为文件编码的标识,此更改提升了兼容性。
总的来说,这些关于 Unicode 的改动提高了 Go 对 Unicode 标准的遵从度,使得字符处理更加健壮,对大多数程序没有影响,但依赖旧行为处理代理对的程序需要修改。
方法值(Method Values)
Go 1.1 引入了一个新特性:方法值(method values)。方法值是一个已绑定到特定接收器值的函数。与之相对的是已有的 方法表达式(method expressions),它会从一个类型的方法生成一个函数。
方法值 (Method Value)
当你拥有一个具体的值(接收器),并访问它的某个方法但不立即调用它(即不加括号 ()),你就得到了一个方法值。这个方法值是一个函数,它内部“记住”了那个具体的接收器。调用这个方法值就等同于在原始接收器上调用该方法。
我们来看一个例子:
在上面的例子中,g.Greet 就是一个方法值。它是一个 func() 类型的函数,调用它时,Greet 方法会在 g 这个实例上执行。
方法表达式 (Method Expression)
方法表达式则是基于 类型 而不是 实例 来获取一个方法对应的函数。它的形式是 TypeName.MethodName 或 (*TypeName).MethodName。这个生成的函数会将接收器作为它的 第一个 参数。
继续使用上面的 Greeter 类型:
方法表达式 Greeter.Greet 生成了一个类型为 func(Greeter) 的函数。调用它时,需要显式地传入一个 Greeter 类型的实例作为第一个参数,这个实例就充当了该次调用的接收器。同理,Greeter.GreetPerson 生成了一个 func(Greeter, string, int) 类型的函数。
总结对比
- **方法值 (instance.Method)**:绑定到 特定实例,函数签名不包含接收器。
- **方法表达式 (Type.Method)**:基于 类型,生成的函数签名将接收器作为 第一个参数。
这个特性是完全向后兼容的,不会影响任何现有代码的编译和运行。它为 Go 语言在函数式编程风格和接口使用方面提供了更大的灵活性。
64 位平台 int 大小的变化
Go 语言规范允许 int 和 uint 类型的大小根据目标平台是 32 位还是 64 位来决定。在 Go 1.1 之前,所有的 Go 实现都将 int 和 uint 定义为 32 位。
从 Go 1.1 开始,gc(标准编译器)和 gccgo 实现在 64 位平台(如 AMD64/x86-64)上将 int 和 uint 定义为 64 位。这一变化的主要好处之一是,它使得在 64 位系统上可以创建和操作元素数量超过 2^31(大约 20 亿)的切片或执行需要更大整数范围的计算。
由于 Go 强制要求显式类型转换,不允许在不同的数字类型之间进行隐式转换,因此这个改变不会导致任何现有程序编译失败。
但是,如果程序中包含了 int 始终是 32 位的 隐式假设,那么其行为可能会在 64 位平台上发生改变。一个典型的例子涉及到从一个无符号 32 位整数转换为 int:
在 32 位系统上,int 和 uint32 大小相同,将 0xffffffff 转换为 int 时,其位模式保持不变,根据 int(有符号类型)的解释,这个位模式代表 -1。
但在 Go 1.1 及以后版本的 64 位系统上,int 是 64 位。当将 32 位的 x (值为 0xffffffff) 转换为 64 位的 int 时,会进行零扩展(因为源类型是无符号的),结果是 64 位的 0x00000000ffffffff,其十进制值是 4294967295。
如果你的代码需要确保无论在哪种平台,都执行 32 位的符号扩展(即将 0xffffffff 转换为 -1),应该先显式地将值转换为 int32,再转换为 int:
通过 int32(x),我们将 0xffffffff 强制解释为 32 位有符号整数,得到 -1。然后 int(-1) 在任何位数的 int 上结果都是 -1。
这个改动对大部分程序是透明的,但开发者应检查代码中是否存在对 int 位宽的隐藏假设,尤其是在进行位运算或与底层系统、特定数据格式交互时。
go 命令的改进
Go 1.1 对 go 命令行工具进行了一些旨在改善新用户体验和规范项目管理的改动。
1. 更详细的包未找到错误信息
当使用 go build, go test 或 go run 等命令时,如果无法定位所需的包,go 命令现在会提供更详细的错误输出。错误信息会包含它尝试搜索包的具体路径列表,这些路径基于 $GOROOT 和 $GOPATH 环境变量。
例如,如果尝试构建一个不存在的包 foo/quxx:
这个改进让开发者能更快地诊断出是 $GOPATH 设置不正确、包未下载还是路径拼写错误等问题。
2. go get 强制要求 $GOPATH
go get 命令用于下载和安装包及其依赖。在 Go 1.1 中,go get 不再允许在没有设置有效 $GOPATH 的情况下运行,并且不再隐式地将 $GOROOT 作为下载目标。现在,必须设置一个有效的 $GOPATH 环境变量,go get 会将下载的源码放在 $GOPATH/src 目录下。
如果 $GOPATH 未设置:
3. go get 禁止 $GOPATH 与 $GOROOT 相同
作为上一条规则的延伸,Go 1.1 还禁止将 $GOPATH 设置为与 $GOROOT 相同的值。$GOROOT 指向 Go 的安装目录,包含标准库源码,而 $GOPATH 指向用户的工作区,包含第三方库和用户自己的项目代码。将两者混用会导致管理混乱。
如果 $GOPATH 被设置为 $GOROOT:
这些对 go 命令的改动,特别是围绕 $GOPATH 的调整,旨在引导开发者从一开始就采用标准的 Go 工作区布局,这对于管理依赖和项目结构至关重要,尤其是在 Go Modules 出现之前。