admin管理员组

文章数量:1031308

Go 1 正式发布时相比 r60.3 有哪些值得注意的改动?

本系列旨在梳理 Go 的 release notes 与发展史,来更加深入地理解 Go 语言设计的思路。

Go 1 值得关注的改动:

  1. 初始化时 goroutine 的启动时机:Go 1 允许在初始化过程中创建并运行 goroutine,不再限制其启动时机,增强了 init 构造的实用性。
  2. rune 类型的引入:Go 1 引入 rune 类型(int32 的别名)表示 Unicode 码点,为将来 int 扩展到 64 位做准备。
  3. error 类型和 errors:Go 1 将 error 定义为内置接口,并引入 errors 包,取代 os.Error,使错误处理更通用。
  4. 不允许开发者对 map 的迭代顺序有依赖:Go 1 明确规定 map 的迭代顺序不可预测,禁止依赖特定顺序。
  5. 多重赋值的细化:Go 1 明确多重赋值时先从左到右求值所有表达式,再按顺序赋值,确保行为一致。
  6. 复制包含未导出字段的结构体:Go 1 允许复制包含其他包未导出字段的结构体,提升了 API 设计的灵活性。
  7. 相等性定义的调整:Go 1 为结构体和数组定义了相等性,可用作 map 键,同时移除函数和 map 的相等性定义(nil 比较除外)。

下面是一些值得展开的讨论:

rune 类型的引入

简要概括

Go 1 引入 rune 类型(int32 的别名)表示 Unicode 码点,为 int 扩展到 64 位做准备,同时优化字符存储。

详细内容

在 Go 1 之前,int 在所有平台上均为 32 位,这在 64 位平台上限制了其表示能力,尤其是在索引大数组时。若直接将 int 扩展为 64 位,会因 Unicode 码点的存储浪费空间。为此,Go 1 引入 rune 类型,作为 int32 的别名,专门表示 Unicode 码点。字符字面量如 'a''語''\u0345' 的默认类型变为 rune,类似于 1.0 默认类型为 float64。未显式指定类型的字符常量变量将具有 rune 类型。标准库中的函数,如 unicode.ToLower,也更新为接受和返回 rune 类型。

以下是一个示例:

代码语言:go复制
delta := 'δ' // delta 的类型为 rune
var DELTA rune
DELTA = unicode.ToUpper(delta)
epsilon := unicode.ToLower(DELTA + 1)
if epsilon != 'δ'+1 {
    log.Fatal("inconsistent casing for Greek")
}

更新影响:由于类型推断(type inference)会自动引入 rune,大多数代码不受影响。少数代码可能需添加简单的类型转换以解决类型错误。

error 类型和 errors

简要概括

Go 1 将 error 定义为内置接口,并引入 errors 包,取代 os.Erroros.NewError,使错误处理更通用和灵活。

详细内容

在 Go 1 之前,os.Error 位于 os 包中,这源于历史原因,但其局限性逐渐显现:错误处理比操作系统更基础,且在 syscall 等包中使用 os.Error 会引入不必要的依赖。Go 1 通过内置 error 接口解决此问题:

代码语言:go复制
type error interface {
    Error() string
}

同时引入 errors 包,提供 New 函数创建错误:

代码语言:go复制
func New(text string) error

fmt 包会自动调用 Error 方法打印错误值。开发者可自定义错误类型,只需实现 Error 方法:

代码语言:go复制
type SyntaxError struct {
    File    string
    Line    int
    Message string
}

func (se *SyntaxError) Error() string {
    return fmt.Sprintf("%s:%d: %s", se.File, se.Line, se.Message)
}

此外,syscall 包现在返回 error 类型。在 Unix 平台上,系统调用错误由 syscall.Errno 实现,满足 error 接口。

更新影响:运行 go fix 可更新大部分代码。若错误类型定义了 String 方法,需手动改为 Error 方法。建议使用 os 包而非 syscall 以减少影响。

map 迭代顺序不可预测

简要概括

Go 1 明确规定 map 的迭代顺序不可预测,禁止开发者依赖特定顺序,以提升 map 实现的灵活性。

详细内容

在 Go 1 之前,map 的迭代顺序未在语言规范中定义,且因硬件平台不同而异,导致测试不稳定。Go 1 使用 for range 迭代 map 时,元素访问顺序被定义为不可预测,即使同一 map 多次运行也如此。代码不应假设元素按特定顺序访问:

代码语言:go复制
m := map[string]int{"Sunday": 0, "Monday": 1}
for name, value := range m {
    // 此循环不应假设 Sunday 先被访问
    f(name, value)
}

此更改确保依赖顺序的代码尽早失败并修复,同时允许 map 实现优化性能和平衡性。

更新影响:工具无法自动修复,需手动检查所有 map 的 range 语句,确保不依赖迭代顺序。标准库中相关代码已修复。之前依赖未定义顺序的代码本就错误,此更改仅明确了不可预测性。

多重赋值的细化

简要概括

Go 1 明确多重赋值时先从左到右求值所有表达式,再按顺序赋值,确保行为可预测。

详细内容

语言规范长期保证赋值时先求值右侧表达式,再赋值给左侧。Go 1 进一步细化:若左侧包含需求值的表达式(如函数调用或数组索引),这些表达式按从左到右顺序求值,然后再赋值:

代码语言:go复制
sa := []int{1, 2, 3}
i := 0
i, sa[i] = 1, 2 // 设置 i = 1, sa[0] = 2

sb := []int{1, 2, 3}
j := 0
sb[j], j = 2, 1 // 设置 sb[0] = 2, j = 1

sc := []int{1, 2, 3}
sc[0], sc[0] = 1, 2 // 先设置 sc[0] = 1,再设置为 2

更新影响:工具无法自动修复,但影响甚微。标准库代码未受影响,依赖之前未定义行为的代码本就错误。

复制包含未导出字段的结构体

简要概括

Go 1 允许复制包含其他包未导出字段的结构体,增强了 API 设计的灵活性。

详细内容

在 Go 1 之前,语言禁止复制包含其他包未导出字段的结构体,但方法接收者和 copyappend 函数未受此限。Go 1 放宽限制,允许复制此类结构体,使包可返回不透明值而无需指针或接口,如 time.Timereflect.Value

代码语言:go复制
// 包 p 的定义
type Struct struct {
    Public int
    secret int
}

func NewStruct(a int) Struct { // 返回值类型为 Struct 而非 *Struct
    return Struct{a, f(a)}
}

func (s Struct) String() string {
    return fmt.Sprintf("{%d (secret %d)}", s.Public, s.secret)
}

// 导入 p 包的代码
import "p"

myStruct := p.NewStruct(23)
copyOfMyStruct := myStruct
fmt.Println(myStruct, copyOfMyStruct)

更新影响:此为新特性,现有代码无需更改。

相等性定义的调整

简要概括

Go 1 为结构体和数组定义了相等性,可用作 map 键,同时移除函数和 map 的相等性(nil 比较除外)。

详细内容

在 Go 1 之前,结构体和数组的相等性未定义,无法用作 map 键,而函数和 map 的相等性定义存在问题。Go 1 引入结构体和数组的相等性(==!=),采用逐元素比较:

代码语言:go复制
type Day struct {
    long  string
    short string
}

Christmas := Day{"Christmas", "XMas"}
Thanksgiving := Day{"Thanksgiving", "Turkey"}
holiday := map[Day]bool{
    Christmas:    true,
    Thanksgiving: true,
}
fmt.Printf("Christmas is a holiday: %t\n", holiday[Christmas])

函数相等性(nil 比较除外)和 map 相等性被移除,因其定义复杂且不符合用户期望。slice 的相等性仍未定义。

更新影响:结构体和数组相等性为新特性,现有代码无需更改。依赖函数或 map 相等性的代码需手动修复。

Go 1 正式发布时相比 r60.3 有哪些值得注意的改动?

本系列旨在梳理 Go 的 release notes 与发展史,来更加深入地理解 Go 语言设计的思路。

Go 1 值得关注的改动:

  1. 初始化时 goroutine 的启动时机:Go 1 允许在初始化过程中创建并运行 goroutine,不再限制其启动时机,增强了 init 构造的实用性。
  2. rune 类型的引入:Go 1 引入 rune 类型(int32 的别名)表示 Unicode 码点,为将来 int 扩展到 64 位做准备。
  3. error 类型和 errors:Go 1 将 error 定义为内置接口,并引入 errors 包,取代 os.Error,使错误处理更通用。
  4. 不允许开发者对 map 的迭代顺序有依赖:Go 1 明确规定 map 的迭代顺序不可预测,禁止依赖特定顺序。
  5. 多重赋值的细化:Go 1 明确多重赋值时先从左到右求值所有表达式,再按顺序赋值,确保行为一致。
  6. 复制包含未导出字段的结构体:Go 1 允许复制包含其他包未导出字段的结构体,提升了 API 设计的灵活性。
  7. 相等性定义的调整:Go 1 为结构体和数组定义了相等性,可用作 map 键,同时移除函数和 map 的相等性定义(nil 比较除外)。

下面是一些值得展开的讨论:

rune 类型的引入

简要概括

Go 1 引入 rune 类型(int32 的别名)表示 Unicode 码点,为 int 扩展到 64 位做准备,同时优化字符存储。

详细内容

在 Go 1 之前,int 在所有平台上均为 32 位,这在 64 位平台上限制了其表示能力,尤其是在索引大数组时。若直接将 int 扩展为 64 位,会因 Unicode 码点的存储浪费空间。为此,Go 1 引入 rune 类型,作为 int32 的别名,专门表示 Unicode 码点。字符字面量如 'a''語''\u0345' 的默认类型变为 rune,类似于 1.0 默认类型为 float64。未显式指定类型的字符常量变量将具有 rune 类型。标准库中的函数,如 unicode.ToLower,也更新为接受和返回 rune 类型。

以下是一个示例:

代码语言:go复制
delta := 'δ' // delta 的类型为 rune
var DELTA rune
DELTA = unicode.ToUpper(delta)
epsilon := unicode.ToLower(DELTA + 1)
if epsilon != 'δ'+1 {
    log.Fatal("inconsistent casing for Greek")
}

更新影响:由于类型推断(type inference)会自动引入 rune,大多数代码不受影响。少数代码可能需添加简单的类型转换以解决类型错误。

error 类型和 errors

简要概括

Go 1 将 error 定义为内置接口,并引入 errors 包,取代 os.Erroros.NewError,使错误处理更通用和灵活。

详细内容

在 Go 1 之前,os.Error 位于 os 包中,这源于历史原因,但其局限性逐渐显现:错误处理比操作系统更基础,且在 syscall 等包中使用 os.Error 会引入不必要的依赖。Go 1 通过内置 error 接口解决此问题:

代码语言:go复制
type error interface {
    Error() string
}

同时引入 errors 包,提供 New 函数创建错误:

代码语言:go复制
func New(text string) error

fmt 包会自动调用 Error 方法打印错误值。开发者可自定义错误类型,只需实现 Error 方法:

代码语言:go复制
type SyntaxError struct {
    File    string
    Line    int
    Message string
}

func (se *SyntaxError) Error() string {
    return fmt.Sprintf("%s:%d: %s", se.File, se.Line, se.Message)
}

此外,syscall 包现在返回 error 类型。在 Unix 平台上,系统调用错误由 syscall.Errno 实现,满足 error 接口。

更新影响:运行 go fix 可更新大部分代码。若错误类型定义了 String 方法,需手动改为 Error 方法。建议使用 os 包而非 syscall 以减少影响。

map 迭代顺序不可预测

简要概括

Go 1 明确规定 map 的迭代顺序不可预测,禁止开发者依赖特定顺序,以提升 map 实现的灵活性。

详细内容

在 Go 1 之前,map 的迭代顺序未在语言规范中定义,且因硬件平台不同而异,导致测试不稳定。Go 1 使用 for range 迭代 map 时,元素访问顺序被定义为不可预测,即使同一 map 多次运行也如此。代码不应假设元素按特定顺序访问:

代码语言:go复制
m := map[string]int{"Sunday": 0, "Monday": 1}
for name, value := range m {
    // 此循环不应假设 Sunday 先被访问
    f(name, value)
}

此更改确保依赖顺序的代码尽早失败并修复,同时允许 map 实现优化性能和平衡性。

更新影响:工具无法自动修复,需手动检查所有 map 的 range 语句,确保不依赖迭代顺序。标准库中相关代码已修复。之前依赖未定义顺序的代码本就错误,此更改仅明确了不可预测性。

多重赋值的细化

简要概括

Go 1 明确多重赋值时先从左到右求值所有表达式,再按顺序赋值,确保行为可预测。

详细内容

语言规范长期保证赋值时先求值右侧表达式,再赋值给左侧。Go 1 进一步细化:若左侧包含需求值的表达式(如函数调用或数组索引),这些表达式按从左到右顺序求值,然后再赋值:

代码语言:go复制
sa := []int{1, 2, 3}
i := 0
i, sa[i] = 1, 2 // 设置 i = 1, sa[0] = 2

sb := []int{1, 2, 3}
j := 0
sb[j], j = 2, 1 // 设置 sb[0] = 2, j = 1

sc := []int{1, 2, 3}
sc[0], sc[0] = 1, 2 // 先设置 sc[0] = 1,再设置为 2

更新影响:工具无法自动修复,但影响甚微。标准库代码未受影响,依赖之前未定义行为的代码本就错误。

复制包含未导出字段的结构体

简要概括

Go 1 允许复制包含其他包未导出字段的结构体,增强了 API 设计的灵活性。

详细内容

在 Go 1 之前,语言禁止复制包含其他包未导出字段的结构体,但方法接收者和 copyappend 函数未受此限。Go 1 放宽限制,允许复制此类结构体,使包可返回不透明值而无需指针或接口,如 time.Timereflect.Value

代码语言:go复制
// 包 p 的定义
type Struct struct {
    Public int
    secret int
}

func NewStruct(a int) Struct { // 返回值类型为 Struct 而非 *Struct
    return Struct{a, f(a)}
}

func (s Struct) String() string {
    return fmt.Sprintf("{%d (secret %d)}", s.Public, s.secret)
}

// 导入 p 包的代码
import "p"

myStruct := p.NewStruct(23)
copyOfMyStruct := myStruct
fmt.Println(myStruct, copyOfMyStruct)

更新影响:此为新特性,现有代码无需更改。

相等性定义的调整

简要概括

Go 1 为结构体和数组定义了相等性,可用作 map 键,同时移除函数和 map 的相等性(nil 比较除外)。

详细内容

在 Go 1 之前,结构体和数组的相等性未定义,无法用作 map 键,而函数和 map 的相等性定义存在问题。Go 1 引入结构体和数组的相等性(==!=),采用逐元素比较:

代码语言:go复制
type Day struct {
    long  string
    short string
}

Christmas := Day{"Christmas", "XMas"}
Thanksgiving := Day{"Thanksgiving", "Turkey"}
holiday := map[Day]bool{
    Christmas:    true,
    Thanksgiving: true,
}
fmt.Printf("Christmas is a holiday: %t\n", holiday[Christmas])

函数相等性(nil 比较除外)和 map 相等性被移除,因其定义复杂且不符合用户期望。slice 的相等性仍未定义。

更新影响:结构体和数组相等性为新特性,现有代码无需更改。依赖函数或 map 相等性的代码需手动修复。

本文标签: Go 1 正式发布时相比 r603 有哪些值得注意的改动