Go 遵循每 6 个月发布一个大版本的规律,最新版本是 Go1.20发布于 2023/01/01 Go 的每个版本围绕 “语言特性”,“工具链”,“Runtime”,“Compiler”, “Linker”, “Library” 这几个方面进行大量的迭代。 由于内容很多,本文打算总结研发过程中可能会关注到语言特性的改进,并使用一些case 对新的语言特性进行解释。
本文根据Go官网各版本发布手册整理,对于一些特别重要的特性,版本文档中也只会做简要描述,要深入了解则需要看特性的专文。
代码示例:
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)
这个迭代可以简单说是实现了方法的重写(C++/Java中的说法)能力。通过代码示例能更加直观一些。
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的版本便支持了这种写法。
为什么需要支持这个组合接口特性?在什么场景下会碰到这种用法?下面用官网给的一个示例说明
假设我们有一个用于保存人员数据的数据库API,我们需要定义一个通用的Person接口包含一系列人员属性和动作。
type Person interface {
Name() string
Age() int
...
}
后来我们需要记录一下员工数据,需要定义一个Employee,Employee可以在Person定义的基础信息外拓展其他属性。
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
Go 1.15 包含一个新包 time/tzdata,它允许将时区数据库嵌入到程序中。 导入此包(如 import _ "time/tzdata")允许程序查找时区信息,即使时区数据库在本地系统上不可用。 您还可以通过使用 -tags timetzdata 构建来嵌入时区数据库。
//go:generate go run generate_zipdata.go
`go run generate_zipdata.go` 命令用于生成 `time/zoneinfo.zip` 文件,该文件包含了所有时区的数据。如果这个包在程序的任何地方被导入,那么如果时间包在系统上找不到tzdata文件,它将使用这个嵌入的信息。导入这个包会增加程序的大小450 KB。
除了需要额外导入zoneinfo库外其他在使用中不需要额外做什么。下面是一个使用示例。
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
go 命令现在支持使用新的 //go:embed 指令将静态文件和文件树作为最终可执行文件的一部分。它可以将文件嵌入 Go 代码中,从而方便地在程序中访问这些文件。
代码演示:
1. 嵌入文件内容到string
import (
_ "embed"
)
//下面这句表示嵌入hello.txt文件并将文件内容赋值到s上
//go:embed hello.txt
var s string
func main() {
print(s)
}
2. 嵌入文件到字节切片中
import _ "embed"
//go:embed hello.txt
var b []byte
print(string(b))
3. 嵌入多个文件到文件系统
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
从切片到数组指针的转换:[]T 类型的表达式 s 现在可以转换为数组指针类型 *[N]T, 假设 a 是转换的结果,如果 len(s) 小于 N。则在范围内的相应索引会引用到相同的底层元素:for 0 <= i < N &a[i] == &s[i] 。如果切片的长度小于数组的长度,就会发生运行时panic。
代码演示:
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的数组呢?
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?
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版本语言特性除了以上所述,还有其他几点:
本版本扩展 Go 语言以将可选类型参数添加到类型和函数声明中,话不多说我们先看官网上总结的语言方面的改动。
看到这里是否一些懵,在以下部分中,将会详细地介绍这些语言更改中的每一个,并通过示例来解释。
代码示例:
1. 基本用法
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与下面代码实现结果是一样的
func Print(s interface{}) {
fmt.Println(s)
}
这里就引申到使用泛型T any和interface{}的区别:interface{} 可以接受任何类型的参数, 使用灵活但是不进行类型检查,在类型转换中容易出现错误。泛型可以在编译时进行类型检查,避免了运行时类型错误的风险,使用泛型可以使代码更加通用和灵活,减少了代码重复的情况。
总的来说,如果需要在函数内部对参数进行类型转换或类型检查,建议使用泛型;如果只是需要接受任意类型的参数并进行简单的操作,可以使用空接口。下面代码用来对比使用interface{}和泛型。
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就是接口类型。
那如何定义一个我们自己可以使用的类型约束呢,下面通过代码示例:
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 中首次使用 ~ 作为标记。
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。 并集元素的类型集是序列中每个元素的类型集的并集。 联合中列出的元素必须全部不同:
type PredeclaredSignedInteger interface {
int | int8 | int16 | int32 | int64
}
type SignedInteger interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
5. 泛型在channel中的使用
// 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
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 位系统上也是如此。
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上有很大的优化,这个后面我们再探索。
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 (例如普通接口)现在可以满足可比较的约束,即使类型参数不是严格可比较的(比较可能会在运行时崩溃)。 这使得实例化受可比较约束的类型参数(例如,用户定义的通用映射键的类型参数)与非严格可比较类型参数(例如接口类型或包含接口类型的复合类型)成为可能。
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的新版本。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。