前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go1.13-1.20语言特性

Go1.13-1.20语言特性

原创
作者头像
COY_fenfei
修改2023-06-13 11:26:56
9140
修改2023-06-13 11:26:56
举报
文章被收录于专栏:COYCOY

前言

Go 遵循每 6 个月发布一个大版本的规律,最新版本是 Go1.20发布于 2023/01/01 Go 的每个版本围绕 “语言特性”,“工具链”,“Runtime”,“Compiler”, “Linker”, “Library” 这几个方面进行大量的迭代。 由于内容很多,本文打算总结研发过程中可能会关注到语言特性的改进,并使用一些case 对新的语言特性进行解释。

为什么需要了解版本迭代?

  1. 版本更新会引入新的语言特性和改进,可以让开发者更加高效地编写代码和解决问题。
  2. 版本更新可能会更改标准库、包和API的行为,需要开发者进行适应和更新。
  3. 了解版本更新可以帮助开发者更好地利用Go语言的性能和功能,提高代码的质量和效率。
  4. 版本更新通常会修复漏洞和错误,提高代码的安全性和可靠性。
  5. Go语言社区和生态系统的发展也与版本更新密切相关,了解版本更新可以帮助开发者更好地了解Go语言的生态环境和未来发展方向。

本文根据Go官网各版本发布手册整理,对于一些特别重要的特性,版本文档中也只会做简要描述,要深入了解则需要看特性的专文。

Go1.13  数字表示法

  • 二进制整数:前缀 0b 或 0B 表示二进制整数文字,例如 0b1011。
  • 八进制整数:前缀 0o 或 0O 表示八进制整数文字,例如 0o660。 由前导 0 后跟八进制数字表示的现有八进制表示法仍然有效。
  • 十六进制浮点值:前缀 0x 或 0X 现在可用于表示十六进制格式的浮点数的尾数,例如 0x1.0p-1021。 十六进制浮点数必须始终有一个指数,写为字母 p 或 P 后跟一个十进制指数。
  • 虚数文字:虚数后缀 i 现在可以与任何(二进制、十进制、十六进制)整数或浮点数文字一起使用。
  • 数字分隔符:现在可以使用下划线分隔(分组)任何数字文字的数字,例如 1_000_000、0b_1010_0110 或 3.1415_9265。 下划线可以出现在任何两个数字之间或文字前缀和第一个数字之间。
  • Go 1.13 移除了计数必须是无符号的限制。 此更改消除了许多在使用<< 和 >> 运算符时需要额外转换成uint的必要。

代码示例:

代码语言:javascript
复制
	fmt.Println(0b1010)
	fmt.Println(012)
	fmt.Println(0o12, 0o12)
	fmt.Println(0x2.1p3)
	fmt.Println(0b101i, 0o12i, 0xaei, 1i)
	fmt.Println(10_00_00, 0b10_10,3.14_15_926)

Go1.14  允许嵌入具有重叠方法集的接口

这个迭代可以简单说是实现了方法的重写(C++/Java中的说法)能力。通过代码示例能更加直观一些。

代码语言:javascript
复制
type E1 interface{ 
	m(x int) bool 
}
type E2 interface{ 
	m(x int) bool 
}
type I interface {
        m(x int) bool
        E1 
        E2 
} // invalid since E1.m and E2.m have the same name

上面的代码在Go13及更早的版本中是无法编译通过的,报错“Duplicate method 'm'”。 因为这样的定义会被判定为在一个接口中定义多个相同方法,在GO中接口中的方法应该是唯一名称的。

在Go14的版本便支持了这种写法。

Go1.14
Go1.14
Go1.13
Go1.13

为什么需要支持这个组合接口特性?在什么场景下会碰到这种用法?下面用官网给的一个示例说明

假设我们有一个用于保存人员数据的数据库API,我们需要定义一个通用的Person接口包含一系列人员属性和动作。

代码语言:javascript
复制
type Person interface {
	Name() string
	Age() int
	...
}

后来我们需要记录一下员工数据,需要定义一个Employee,Employee可以在Person定义的基础信息外拓展其他属性。

代码语言:javascript
复制
type Employee interface {
	Person
	Level() int
	…
	String() string
}

Employee定义了一个String()方法用于简化信息的格式化。开始这样使用是没有任何问题的,突然有一天Person维护研发觉得同样也需要增加一个String()方法时,问题就出现了。Person增加了 String()后Employee就必须移除它定义的 String(),否则无法通过编译,但是Person的String是无法处理Empolyee特有属性的,此时就产生了矛盾。

详细了解 -> Proposal: Permit embedding of interfaces with overlapping method sets

Go1.15  增加包time/tzdata

Go 1.15 包含一个新包 time/tzdata,它允许将时区数据库嵌入到程序中。 导入此包(如 import _ "time/tzdata")允许程序查找时区信息,即使时区数据库在本地系统上不可用。 您还可以通过使用 -tags timetzdata 构建来嵌入时区数据库。 

代码语言:javascript
复制
//go:generate go run generate_zipdata.go

`go run generate_zipdata.go` 命令用于生成 `time/zoneinfo.zip` 文件,该文件包含了所有时区的数据。如果这个包在程序的任何地方被导入,那么如果时间包在系统上找不到tzdata文件,它将使用这个嵌入的信息。导入这个包会增加程序的大小450 KB。

除了需要额外导入zoneinfo库外其他在使用中不需要额外做什么。下面是一个使用示例。

代码语言:javascript
复制
import (
	"fmt"
	"time"
)
func main() {
	// 加载Asia/Tokyo时区的数据
	tz, err := time.LoadLocation("Asia/Tokyo")
	if err != nil {
		panic(err)
	}
	// 用时区转换器解析时间
	loc := time.Date(2022, 06, 10, 10, 30, 0, 0, tz)
	fmt.Println(loc)
	// 格式化时间
	fmt.Println(loc.Format("2006-01-02 15:04:05 -0700"))
}

可以看到我们只是使用LoadLocation去加载东京的时区,没有直接使用tzdata,但是LoadLocation加载时会使用到tzdata

Go1.16  支持二进制嵌入文件

go 命令现在支持使用新的 //go:embed 指令将静态文件和文件树作为最终可执行文件的一部分。它可以将文件嵌入 Go 代码中,从而方便地在程序中访问这些文件。

代码演示:

1. 嵌入文件内容到string

代码语言:javascript
复制
import (
	_ "embed"
)
//下面这句表示嵌入hello.txt文件并将文件内容赋值到s上
//go:embed hello.txt
var s string
func main() {
	print(s)
}

 2. 嵌入文件到字节切片中

代码语言:javascript
复制
import _ "embed"
//go:embed hello.txt
var b []byte
print(string(b))

3. 嵌入多个文件到文件系统

代码语言:javascript
复制
import (
   "embed"
)
//go:embed hello.txt hello2.txt
var f embed.FS
func main() {
	data, _ := f.ReadFile("hello.txt")
	print(string(data))
        data2, _ := f.ReadFile("hello2.txt")
	print(string(data2)) }

go:embed使用注意事项也有很多,在使用中需要多加注意。//go:embed 指令使用一个或多个 path.Match 模式指定要嵌入的文件。该指令必须紧接在包含单个变量声明的行之前。 指令和声明之间只允许空行和‘//’行注释。变量的类型必须是字符串类型,或者字节类型的切片,或者FS(或者FS的别名)。//go:embed后面的文件路径分隔符是正斜杠,即使在 Windows 系统上也是如此。 模式不得包含“.”或“..”或空路径元素,也不得以斜杠开头或结尾。 要匹配当前目录中的所有内容,请使用“*”而不是“.” 如果一个模式命名一个目录,则以该目录为根的子树中的所有文件都被嵌入(递归),除了名称以“.”或“_”开头的文件被排除在外.如果模式以前缀“all:”开头,则遍历目录的规则将更改为包括那些以“.”或“_”开头的文件。对于嵌入单个文件,字符串或 []byte 类型的变量通常是最好的。 FS 类型允许嵌入文件树,例如静态 Web 服务器内容的目录。

go:embed 的使用场景包括: 1. 嵌入配置文件:将配置文件嵌入到可执行文件中,避免了读取配置文件的繁琐操作。 2. 嵌入静态资源:将静态资源文件(如 HTML、CSS、JavaScript 等)嵌入到可执行文件中,方便在程序中访问和使用。 3. 嵌入模板文件:将模板文件嵌入到可执行文件中,方便在程序中渲染模板。

使用 go:embed 的优势包括: 1. 简化代码结构:不需要手动管理文件读取和路径解析等操作,可以直接通过嵌入的方式访问文件。 2. 优化性能:嵌入的文件可以在编译时就加载到内存中,避免了运行时的文件读取操作,从而提高了程序的性能。 3. 避免上传到服务器上用于程序读取的配置文件等被篡改。

如果想更详细了解go:embed的用法可以去 Standard library embed

Go1.17 允许切片转为数组指针

从切片到数组指针的转换:[]T 类型的表达式 s 现在可以转换为数组指针类型 *[N]T, 假设 a 是转换的结果,如果 len(s) 小于 N。则在范围内的相应索引会引用到相同的底层元素:for 0 <= i < N &a[i] == &s[i] 。如果切片的长度小于数组的长度,就会发生运行时panic。

代码演示:

代码语言:javascript
复制
s := make([]byte, 2, 4)
s[0], s[1] = 'a', 'b'
fmt.Println(s)
s0 := (*[2]byte)(s)
fmt.Println(*s0)
s[0] = 'c'
s0[1] = 'd'
println(&s[0],&s[1])
println(&s0[0],&s0[1])
//输出结果为:
[97 98]
[97 98]
0xc00001806c 0xc00001806d
0xc00001806c 0xc00001806d

从上面的示例我们看到,在切片s转换为数组指针s0后,s切片地址与s0数组元素地址是一样的。如果把长度为2的切片转化为长度为3的数组呢?

代码语言:javascript
复制
s := make([]byte, 2, 4)
s0 := (*[3]byte)(s)
fmt.Println(*s0)
//输出结果:
panic: runtime error: cannot convert slice with length 2 to pointer to array with length 3

 那如果在同等长度转换后,切片元素增加是否会产生数组越界panic?

代码语言:javascript
复制
s := make([]byte, 2, 4)
s[0], s[1] = 'a', 'b'
fmt.Println(s)
s0 := (*[2]byte)(s)
fmt.Println(*s0)
s = append(s, 'c')
fmt.Println(s)
fmt.Println(*s0)
//输出结果
[97 98]
[97 98]
[97 98 99]
[97 98]

事实证明在切片元素增加后,对转换后的元素无影响。

在开发中,如果我们是切片类型数据,在调用函数需要使用固定长度的数组或数组指针,可以使用这个特性进行转换,以避免在转换过程中发生数据复制,从而提高了程序的性能。此外,将切片转换为数组或数组指针还可以使代码更加简洁和易于理解。

Go1.17版本语言特性除了以上所述,还有其他几点:

  1. unsafe.Add:unsafe.Add(ptr, len) 将 len 添加到 ptr 并返回更新后的指针 unsafe.Pointer(uintptr(ptr) + uintptr(len))。
  2. unsafe.Slice:对于 *T 类型的表达式 ptr,unsafe.Slice(ptr, len) 返回一个 []T 类型的切片,其底层数组从 ptr 开始,长度和容量为 len。

Go1.18 泛型

本版本扩展 Go 语言以将可选类型参数添加到类型和函数声明中,话不多说我们先看官网上总结的语言方面的改动。

  1. 函数可以额外有一个使用方括号的类型参数列表,其他方面普通的参数列表没有区别:func F[T any](p T) { ... }。
  2. 这些类型参数可以被常规参数和函数体使用,type结构也可以有一个类型参数列表:type M[T any] []T。
  3. 每个类型参数都有一个类型约束,就像每个普通参数都有一个类型一样:func F[T Constraint](p T) { ... },类型约束是接口类型, 新的预声明名称 any 是允许任何类型的类型约束。
  4. 用作类型约束的接口类型可以嵌入额外的元素来限制满足约束的类型参数集:
  • a. 任意类型 T 限制为该类型
  • b. 近似元素 ~T 限制为基础类型为 T 的所有类型
  • c. 联合元素 T1 | T2 | ... 限制为任何列出的元素
  • d. 通用函数只能使用约束允许的所有类型支持的操作。
  • e. 使用泛型函数或类型需要传递类型参数。
  • f.  类型推断允许在常见情况下省略函数调用的类型参数。

看到这里是否一些懵,在以下部分中,将会详细地介绍这些语言更改中的每一个,并通过示例来解释。

代码示例:

1. 基本用法

代码语言:javascript
复制
func main() {
	intV := 1
	Print(intV)
	strV := "hello world"
	Print(strV)
	boolV := true
	Print(boolV)
}
func Print[T any](s T) { 
	fmt.Println(s)
}

 Print在这里的[T any]写法是否有点像interface{}类型的参数呢?确实可以说是的。上面Print与下面代码实现结果是一样的

代码语言:javascript
复制
func Print(s interface{}) {
	fmt.Println(s)
}

这里就引申到使用泛型T any和interface{}的区别:interface{} 可以接受任何类型的参数, 使用灵活但是不进行类型检查,在类型转换中容易出现错误。泛型可以在编译时进行类型检查,避免了运行时类型错误的风险,使用泛型可以使代码更加通用和灵活,减少了代码重复的情况。

总的来说,如果需要在函数内部对参数进行类型转换或类型检查,建议使用泛型;如果只是需要接受任意类型的参数并进行简单的操作,可以使用空接口。下面代码用来对比使用interface{}和泛型。

代码语言:javascript
复制
func main() {
	minInt := minGeneric(1, 2, LessInt)
	fmt.Println(minInt)
	minFloat := minGeneric(1.5, 2.7, LessFloat64)
	fmt.Println(minFloat)
	minStr := minGeneric("ssc", "sse", LessString)
	fmt.Println(minStr)
	minIFInt := minInterface(1, 2)
	fmt.Println(minIFInt)
	minIFFloat := minInterface(1.5, 2.7)
	fmt.Println(minIFFloat)
	minIFStr := minInterface("ssc", "sse")
	fmt.Println(minIFStr)
}
func minInterface(x, y interface{}) interface{} {
	switch x.(type) {
	case int:
		if x.(int) < y.(int) {
			return x
		}
		return y
	case float64:
		if x.(float64) < y.(float64) {
			return x
		}
		return y
	case string:
		if x.(string) < y.(string) {
			return x
		}
		return y
	default:
		return nil
	}
}
func minGeneric[T comparable](x, y T, compare func(T, T) bool) T {
	if compare(x, y) {
		return x
	}
	return y
}
func LessInt(a, b int) bool         { return a < b }
func LessFloat64(a, b float64) bool { return a < b }
func LessString(a, b string) bool   { return a < b }

2. 类型约束定义

Go 已经有一个接近于我们需要的所谓的类型约束的数据结构:接口类型。 接口类型是一组方法的集合。前面也提到过T的约束是一个接口类型。使用类型参数调用泛型函数类似于分配给接口类型的变量:类型参数必须实现类型参数的约束。在这个设计中,约束只是接口类型。 满足约束意味着实现接口类型。其实在源码中我们看前面所用到的any,comparable就是接口类型。

那如何定义一个我们自己可以使用的类型约束呢,下面通过代码示例:

代码语言:javascript
复制
type Numeric interface {
	// 定义一个泛型方法约束
        Add(other Numeric) Numeric
}
// 定义一个泛型类型,它实现了 Numeric 接口
type MyInt int
func (i MyInt) Add(other Numeric) Numeric {
	return i + other.(MyInt)
}
// 定义一个泛型函数,它接受两个 Numeric 类型的参数
func Sum[T Numeric](a, b T) T {
	return a.Add(b).(T)
}
func main() {
	x := MyInt(1)
	y := MyInt(2)
	fmt.Println(Sum(x, y)) // 输出 3
} 

3. 近似约束元素

在约束中允许的第二个新元素是一个新的句法结构:一个近似元素,写为 ~T。 ~T 的类型集是基础类型为 T 的所有类型的集合。例如:输入 AnyString interface{ ~string }。 ~string 的类型集,以及 AnyString 的类型集,是基础类型为字符串的所有类型的集合。这个新的 ~T 语法将是 Go 中首次使用 ~ 作为标记。

代码语言:javascript
复制
func main() {
	myStr := MyString("123")
	PrintStr(myStr)
	youStr := YourString("123")
	PrintStr(youStr)
	str := "456"
	PrintStr(str)
}
// AnyString matches any type whose underlying type is string.
// This includes, among others, the type string itself, and
// the type MyString.
type AnyString interface {
	~string
}
type MyString string
type YourString string
func PrintStr[T AnyString](s T) {
	fmt.Println(s)
}

 如果我们把AnyString中的~string去掉~,则由string衍生的MyString和YourString不再可以实现AnyString

4. 组合约束元素

约束中允许的第三种新元素类型也是一个新的语法结构:联合元素,是一系列由竖线 (|) 分隔的约束元素。 例如:整数 | float32 或 ~int8 | 〜int16 | 〜int32 | ~int64。 并集元素的类型集是序列中每个元素的类型集的并集。 联合中列出的元素必须全部不同:

代码语言:javascript
复制
type PredeclaredSignedInteger interface {
	int | int8 | int16 | int32 | int64
}
type SignedInteger interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

5. 泛型在channel中的使用

代码语言:javascript
复制
// Drain drains any elements remaining on the channel.
func Drain[T any](c <-chan T) {
	for range c {
	}
}
// A Sender is used to send values to a Receiver.
type Sender[T any] struct {
	values chan<- T
	done   <-chan bool
}

// Send sends a value to the receiver. It reports whether any more
// values may be sent; if it returns false the value was not sent.
func (s *Sender[T]) Send(v T) bool {
	select {
	case s.values <- v:
		return true
	case <-s.done:
		// The receiver has stopped listening.
		return false
	}
}
func (s *Sender[T]) Close() {
	close(s.values)
}

// A Receiver receives values from a Sender.
type Receiver[T any] struct {
	values <-chan T
	done   chan<- bool
}

// Next returns the next value from the channel. The bool result
// reports whether the value is valid. If the value is not valid, the
// Sender has been closed and no more values will be received.
func (r *Receiver[T]) Next() (T, bool) {
	v, ok := <-r.values
	return v, ok
}
// finalize is a finalizer for the receiver.
// It tells the sender that the receiver has stopped listening.
func (r *Receiver[T]) finalize() {
	close(r.done)
}

 GO18的泛型是12-20版本中语言特性变更最大的一点,内容非常多本文介绍的只是冰山一角。如果感兴趣可以详细了解->Type Parameters Proposal

Go1.19 内存模型和atomic 包

Go 的内存模型现在明确定义了 sync/atomic 包的行为。 happens-before 关系的正式定义已经过修改,以与 C、C++、Java、JavaScript、Rust 和 Swift 使用的内存模型保持一致。 现有程序不受影响。 随着内存模型的更新,sync/atomic 包中有新的类型,例如 atomic.Int64 和 atomic.Pointer[T],可以更轻松地使用原子值。

 新的atomic类型

sync/atomic 包定义了新的原子类型 Bool、Int32、Int64、Uint32、Uint64、Uintptr 和 Pointer。 这些类型隐藏了底层值,因此所有访问都被迫使用原子 API。 Pointer 还避免了在调用站点转换为 unsafe.Pointer 的需要。 Int64 和 Uint64 自动对齐到结构和分配数据中的 64 位边界,即使在 32 位系统上也是如此。

代码语言:javascript
复制
func atomicUse() {
	var val uint32 = 42
	go func() {
		for {
			if atomic.CompareAndSwapUint32(&val, 42, 43) {
				break
			}
		}
	}()
	for {
		if atomic.LoadUint32(&val) == 43 {
			fmt.Println("val is 43")
			break
		}
	}
}

 Go1.19版本在语言特性上变更比较少,更多的是在内存模型和GC上,GO19在内存模型和GC上有很大的优化,这个后面我们再探索。

Go1.20 切片转换数组和约束类型

  1. Go 1.17 添加了从切片到数组指针的转换。 Go 1.20 扩展了它以允许从切片到数组的转换:给定切片 x,现在可以写入 [4]byte(x) 而不是 (*[4]byte)(x)。
代码语言:javascript
复制
func main() {
	s := make([]byte, 2, 4)
	s[0], s[1] = 'a', 'b'
	a0 := [1]byte(s)
	a1 := [1]byte(s[1:]) // a1[0] == s[1]
	a2 := [2]byte(s)     // a2[0] == s[0]
	fmt.Println(a0)
	fmt.Println(a1)
	fmt.Println(a2)
}

2. unsafe 包定义了三个新函数 SliceData、String 和 StringData。 与 Go 1.17 的 Slice 一起,这些函数现在提供了构建和解构切片和字符串值的完整能力,而不依赖于它们的确切表示。

3. Comparable types (例如普通接口)现在可以满足可比较的约束,即使类型参数不是严格可比较的(比较可能会在运行时崩溃)。 这使得实例化受可比较约束的类型参数(例如,用户定义的通用映射键的类型参数)与非严格可比较类型参数(例如接口类型或包含接口类型的复合类型)成为可能。

代码语言:javascript
复制
package main
import (
    "fmt"
)
type Key interface {
    Id() int
}
type User struct {
    id int
    name string
}
func (u User) Id() int {
    return u.id
}
type GenericMap[T comparable] map[T]string
func main() {
    userMap := make(GenericMap[Key])
    user := User{id: 1, name: "John"}
    userMap[user] = "user1"
    fmt.Println(userMap)
}

 我们定义了一个Key接口和一个User结构体。User结构体实现了Key接口的Id方法。然后我们定义了一个泛型类型GenericMap,它的类型参数T需要满足可比较约束。 在main函数中,我们使用User结构体作为键来创建一个userMap实例。由于User结构体实现了Key接口的Id方法,因此它满足了可比较约束。这意味着我们可以将User结构体作为键传递给泛型类型的map,即使User结构体本身不是一个严格可比较的类型。 在Go 1.20之前,这种情况是不可能的,因为只有严格可比较的类型才能传递给泛型类型的map的键。现在,我们可以使用任何满足可比较约束的类型作为键,即使它们不是严格可比较的。

 Go1.20版本在语言特性上变更也比较少,更多的是在编译器和链接器的优化。Go 1.18 和 1.19 的构建速度有所下降,这主要是由于增加了对泛型的支持和后续工作。 Go 1.20 将构建速度提高了 10%,使其与 Go 1.17 保持一致。 相对于 Go 1.19,生成的代码性能也普遍略有提升。

按照GO版本发布周期,1.21应该也快发布了,让我们在熟悉历史版本的特性中期待Go的新版本。

参考文献

  1. Go 1.13 Release Notes
  2. Go 1.14 Release Notes
  3. Go 1.15 Release Notes
  4. Go 1.16 Release Notes
  5. Go 1.17 Release Notes
  6. Go 1.18 Release Notes
  7. Go 1.19 Release Notes
  8. Go 1.20 Release Notes
  9. Proposal: Permit embedding of interfaces with overlapping method sets
  10. Standard library embed
  11. Type Parameters Proposal
  12. The Go Programming Language Specification

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 为什么需要了解版本迭代?
  • Go1.13  数字表示法
  • Go1.14  允许嵌入具有重叠方法集的接口
  • Go1.15  增加包time/tzdata
  • Go1.16  支持二进制嵌入文件
  • Go1.17 允许切片转为数组指针
  • Go1.18 泛型
  • Go1.19 内存模型和atomic 包
  • Go1.20 切片转换数组和约束类型
  • 参考文献
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档