版本升级
这节会学到接口的定义和泛型的相关知识,而泛型是在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)
参考
- https://go.dev/doc/
- https://docs.microsoft.com/zh-cn/learn/paths/go-first-steps/
- 《Go语言设计与实现》 - 左书祺(@Draven)