版本升级

这节会学到接口的定义和泛型的相关知识,而泛型是在Go 1.18进行的支持,所以学习前,我们先将Go的版本升为当前最新的稳定版,升级方式和安装一样,只需要下载不同的版本即可。

# 卸载
$ sudo apt remove golang
$ sudo apt autoremove
$ sudo rm -rf /usr/local/go
# 安装
$ sudo tar -C /usr/local -xzf go1.18.2.linux-amd64.tar.gz

进阶语法

结构

Go中没有其他语言较为明确的类写法,而是使用结构体+特殊定义的语法来实现其他语言中的类功能。声明结构需要使用到struct的关键词,将一组字段进行组合表达。

// 结构声明
type Person struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}
// 嵌套结构
type Employee struct {
    Information Person
    ManagerID   int
}

结构体标签也是Go里面比较有意思的一个功能,在我们使用时进行关系对应,例如我们在使用JSON时想更改一些显示命名。Go中的标签功能是使用reflect.StructTag进行表示的,而reflect.StructTag实际对应一个string。

type StructTag string

我们根据reflect.StructTag结构中提供的Get方法就可以进行标签的获取,除Go中提供的一些通用的struct tags外,我们也可以定制一些自己的tag。

type Person struct {
    ID        int    
    FirstName string `json:"name"`
    LastName  string
}

p := Person{FirstName: "John"}
data, _ := json.Marshal(p)
// {"ID":0,"name":"John","LastName":""}
fmt.Printf("%s", data)

定义完基础的结构属性,我们还可以给结构增加一些方法,Go不像其他的语言一样有private/public这类的修饰词来控制访问权限,而是直接使用了一种强制的命名规则来进行控制,结构/属性/方法等定义中开头字母为大写则为public可以在其他包中访问,否则只能在当前包中访问。

type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}
// 首字母非大写则只能在当前包中访问
func (s Square) perimeter() float64 {
    return s.size * 4
}

结构体方法的调用方式和其他语言类似。

s := Square{3}
fmt.Println("Area: ", s.Area())

make 和 new

我们在Go中初始化结构时,会看到make和new两个关键词功能相似而感到疑惑,让我们来看下他们的规则。

func new(Type) *Type : 分配内存的函数,为传入的类型分配一块零值内存空间,并返回一个指向这块内存空间的指针。

// {0}
var s1 Square
// {0}
s2 := Square{}
// &{0}
s3 := new(Square)
// &{0}
s4 := &Square{}

根据上面的声明方式,我们在使用时可以用其他对等的方式来声明结构,能尽量避免新手的疑惑。

func make(t Type, size …IntegerType) Type : 只适用于内置的slices, maps, channels,返回类型已初始化的非零值,而不是指针。

// &[],*i1 == nil
// 基本不会用到
i1 := new([]int)
// [0]
i2 := make([]int, 1)

接口

Go中的接口也比较有意思,是隐式实现的,不像其他语言一样必须要显示的声明出来。声明接口使用interface关键词。

type Shape interface {
    Perimeter() float64
    Area() float64
}

由于接口是隐式的,我们在实现时不会看到任何和接口相关的声明,只需要在使用时实现接口的所有方法即可。

// 实现1
type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}

func (s Square) Perimeter() float64 {
    return s.size * 4
}
// 实现2
type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

定义后Go在使用时判断是否符合接口。

// 方法接收接口类型的参数,调用时就可以传入任何实现了该接口的结构
func printInformation(s Shape) {
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}
// 调用
var s Shape = Square{3}
printInformation(s)

c := Circle{6}
printInformation(c)

defer

在Go中,defer语句会推迟函数的运行,常用于关闭文件描述符、数据库连接、解锁资源等操作。多个defer存在时,会逆序运行,先运行最后一个,最后运行第一个。这种操作可以让我们在打开资源时就增加defer关闭函数,比写在最后清晰且不易忘记。

// 逆序释放
// 1 2 3 4 -4 -3 -2 -1
for i := 1; i <= 4; i++ {
    defer fmt.Println("deferred", -i)
    fmt.Println("regular", i)
}
// 关闭文件,打开资源后就可以直接使用defer推迟关闭
fp, err := os.Create("1.txt")
if err != nil {
    return
}
defer fp.Close()

panic

一般情况下我们会将错误返回,然后判断错误是否存在并作出合理的处置,例如使用if err!= nil,但如果碰到无法恢复的错误不想让程序继续允许,我们就可以使用panic关键词来创建一个运行时错误,该错误将直接调用当前的defer后停止程序。

但我们应该尽量避免使用panic的方式来处理程序。

// 不会触发,panic只触发当前goroutine的defer
defer fmt.Println("defer 1")
go func() {
    // 触发调用后退出
    defer fmt.Println("defer 2")
    panic("exit")
}()
time.Sleep(1 * time.Second)

recover

当有panic被调用时,程序会崩溃后停止,如果我们想在停止前重新获得控制权,来做一些处理操作,则可以使用recover函数。recover()只有在defer中调用才会生效。

defer fmt.Println("defer 1")
defer func() {
    if err := recover(); err != nil {
        // recover exit
        // defer 1
        fmt.Println("recover", err)
    }
}()
panic("exit")

泛型

Go终于在1.18版本支持了泛型。在没有泛型之前,我们处理重复逻辑的代码一般有2种方式。

// 一种是我们定义多个逻辑一致的方法
// 增加了代码的复杂化,如果逻辑比较复杂会难以阅读增加出BUG的可能性
func MinInt32(a, b int32) int32 {
    // ...
}
func MinFloat32(a, b float32) float32 {
	// ...
}
// 另一种是我们使用万能的`interface{}`
// 但这种方式无法进行编译时检查
func Min(a, b interface{}) interface{} {
    // a.(type)
    // ... 
}

Go这次的泛型支持可谓是参考了众多语言的实现方式,虽然晚到,但带来了更好的解决方案。使用泛型时我们先更新gopls工具,避免提示语法错误。

// 泛型定义,T为参数,any是参数约束
func GenericFunc[T any](args T) {
   // ...
}

使用泛型就可以解决我们刚刚不合理的应用方式。

import "golang.org/x/exp/constraints"
func GMin[T constraints.Ordered](x, y T) T {
	if x < y {
		return x
	}
	return y
}

我们看下constraints.Signed的声明。

type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

这里面我们注意到一种特殊的~符号,这也是泛型支持中新增加的一种表达方式~T波浪号,用于标识近似约束,底层类型T必须是自身,不能是接口,并且非接口的类型集不能相交。

type MyFloat interface {
	~float32 | ~float64
}
// ~MyInt, 错误,MyInt的底层类型不是MyInt
// ~int | MyInt, 错误,~int包含MyInt,不能相交
// ~error, 错误,error是一个接口
// ~[]byte, 正确,[]byte 的底层类型是它本身
// float32 | Float, 正确,虽然重叠类型集,但Float是一个接口
func GAdd[T ~int | ~float32](x, y T) T {
	return x + y
}
type MyInt int
var s1 MyInt = 1
// 近似约束中~int包含MyInt
x1 := GAdd(s1, 2)
x2 := GAdd[float32](1, 2)
f3 := GAdd[float32]
x3 := f3(1, 2)

参考