GO
约 3182 个字 321 行代码 预计阅读时间 15 分钟
简介
Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言,它抛弃了继承,弱化了 OOP。值得一提的是,你可能会在其它地方看见有人叫它Golang ,但它的官方名字其实是一直是Go,早期的时候由于go 这个域名被抢注了,所以官网的域名就采用了 golang.org,导致了许多人误解。
GO 语言的结构和 C 家族语言差不多,在下面这个简单的例子中,第一行是包声明,第二行是导入的包,第三行是 main 函数。
可以看到行尾并没有分号,在 Go 程序中,一行代表一个语句结束,理论上可以写分号,然后把两条语句写在一行,但是一般不推荐这样做(除了部分错误处理那种习惯写在一行的)
基础语法
包
Go 语言中,包是组织代码的单位,导入的最小单位是包,而不是.go文件,所有的 Go 程序都必须属于一个包,包名通常是小写的,包名应该与文件所在的目录名一致。
可见性
Go 语言中,没有private和public关键字,可见性是通过大写或小写字母来区分的,大写字母开头的标识符是公开的,小写字母开头的标识符是私有的。
导入
导入就是用import 加上包名,可以是一个一个导入,也可以是用括号括起来导入多个包,Go不允许导入但不使用(会报错)。
如果包名重复了,或者比较复杂,可以取别名,如下所示:
如今的 Go 项目推荐使用 go mod 管理依赖,vendor 基本成为了过去式。
go mod init会在项目中生成两个文件go.mod文件,相当于是 Maven 的 xml 配置文件,拉取一个库使用go get -u url,其中-u参数表示拉取最新版本,当然也可以指定版本。
注释
注释和 C 语言一样,单行注释用// ,多行注释用/**/,可以使用go doc命令查看包和函数的文档,比如:
Go 提供文档工具godoc(go install golang.org/x/tools/cmd/godoc@latest),使用godoc -http=:6060可以启动一个HTTP服务加载HTML文档,然后访问http://localhost:6060/pkg/就可以看到文档。
格式化
Go 提供格式化工具,使用go fmt path可以格式化path路径下的 Go 文件。
变量
GO 的变量写法比较独特,类型在变量名后面,如果声明时没有赋值,那么变量的值会自动初始化为零值。
不过 GO 支持类型推断,所以类型可以省略。
还可以少写var,这是短变量声明语法糖,但如果变量已经声明过了,那么就会报错。
还有一个常见语法是一次性声明多个变量,使用()包裹
Go 不允许声明变量但不使用,同样会报错
常量
Go 语言常量的定义方式和变量类似,但是使用 const 关键字,常量必须在声明时就制定初值。
除此以外,GO 还支持 iota,iota 是一个常量生成器,iota 会自动递增,然后依次计算第一行所写的表达式的值。
这个式子是自动推导的
输入输出
fmt 包是 Go 语言的格式化输入输出包,常用fmt.Println输出一行内容
支持C风格的格式化输出
输入也是标准的 C 风格,要取地址
分支与循环
支持if和switch,但是条件不用打括号, switch分支不再需要显式写break
// 一个简单的if-else
if a > b {
fmt.Println("a is greater than b")
} else {
fmt.Println("a is less than b")
}
没有while,但是for可以像while一样使用,即只写循环条件,同样条件不需要打括号
Go 是用for-each风格的循环的,但是到数组再说。
基础数据类型
GO 是一个静态强类型的语言,GO 语言的声明始终遵循名称在前,类型在后的原则。可以通过type关键字声明新的类型,虽然前后的两个类型本质上是一个类型,但是编译器认为它们不一样。
此外还可以取别名,这个前后两个类型就是被编译器认为是同一个类型
在GO 中,只存在显式类型转换,不存在隐式类型转换,转换类型必须是可以被目标类型代表的类型(即往大的转换),比如int16可以转化为int32,但是反过来不行,明显可能溢出。
零值
零值准确来说应该就默认值(毕竟不一定真的是数字0),一些引用类型的零值是nil,这个显然和null差不多,需要注意nil不属于任何类型,nil==nil是无法通过编译的。
数组
数组是值类型,所以函数传参时会拷贝,数组声明时的长度只能是常量。
可以有初始值
有初始值的情况下长度可以省略
还可以通过new函数获取一个指针
GO 内置了len 和cap 函数用于获取序列的长度和容量。此外,数组有切割语法,切割的结果即为切片,格式为arr[start:end] ,切割的区间为左闭右开,切割前后是共享内存的。
arr := [5]int{1, 2, 3, 4, 5}
arr1 := arr[1:3]
arr2 := arr[:3]
arr3 := arr[1:]
arr1[0] = 10
fmt.Println(arr)
// 结果为 [1, 10, 3, 4, 5]
切片
切片是动态的数组,是引用类型,初始化时自然不需要指定长度。
var slice []int
slice := []int{1, 2, 3, 4, 5}
slice := make([]int, 5) // 三个参数依次为类型、长度和容量
slice := new([]int)
切片可以使用append 添加元素,GO中并不存在OOP中的那种方法,所以append和普通函数的写法是一样的,并且切片是引用类型,故下面的代码不存在任何拷贝。
删除元素使用切割语法就能实现,内存是自动回收的
拷贝切片使用copy函数,会自动扩容
切片有for range遍历
Go 中表示舍弃值使用_
字符串
在 GO 中,字符串本质上是一个不可变只读的字节数组,普通的字符串使用双引号,此外还有原生字符串使用反引号。字符串全面支持utf-8编码,所以像"中文"这样的字符实际按字节存储为对应的unicode编码,但是这样在遍历时会出问题,按下标遍历到的不是单独的汉字,而是每个字节的内容,故需要使用for range遍历或者使用utf-8库。
Go 中涉及到转化为字符串常用fmt.Sprintf(),这个基本可以转换到任意格式,基本数据类型可以考虑使用strconv。
映射表
映射表是无序的键值对集合,也是引用类型,它的键必须是可比较的(实现comparable接口)
初始化映射表如下
也可以使用make获取指针
GO 的 map 访问不存在的键会返回默认值,而且访问键其实有两个返回值,第一个是对应的值,第二个是键是否存在(布尔类型)
添加元素直接赋值即可,删除元素使用delete函数
for range遍历是同时遍历键和值
GO 不提供 Set,但是可以使用 map 的 key 来实现,值取成一个空的结构体,一个空的结构体并不会占用内存。
指针
GO 中保留了指针,但是移除了对指针的运算,也就是一个指针指向的地址不能被改变,以防止指向一个异常的地址。
函数
GO 中函数是一等的公民,也就是支持函数式语法,声明有两种方式
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}// 这个相当于把一个匿名函数赋值给了sum
需要注意GO不支持函数重载,如果签名不一样,那就是不同的函数。
参数
Go 中不存在:
- GO 中不存在关键字参数
- GO 中也不存在默认参数
为了可读性,参数都应该写名称(除了接口,但是推荐都写上),对于类型相同的参数而言可以只声明一次类型,不过条件是必须相邻。
此外GO 还支持变长参数,变长参数必须声明在末尾
// 接收任意个 int 参数然后返回和
func sum(a ...int) int {
sum := 0
for _, v := range a {
sum += v
}
return sum
}
GO 中的参数传递都是值传递,但是像切片和映射表这种引用类型本质上都是指针。
func f(slice []int) {
for i := range slice {
slice[i] += 1
}
}
func main() {
slice := []int{1, 2, 3}
f(slice)
fmt.Println(slice)
// 输出 [2, 3, 4]
}
返回值
当不需要返回值时,不需要写void ,直接不写返回值即可。
GO 允许有多个返回值,此时需要用括号将返回值括起来写在最后,类似于返回一个元组,甚至也是可以给返回值命名的。
匿名函数与闭包
GO 的匿名函数和其他语言一样匿名函数主要是作为高阶函数的参数。
func mapping(f func(int) int, slice []int) []int {
for i, v := range slice {
slice[i] = f(v)
}
return slice
}
func main() {
slice := []int{1, 2, 3}
slice = mapping(func(x int) int { return x * x }, slice)
fmt.Println(slice)
// 输出 [1, 4, 9]
}
GO 的闭包和 Python 的闭包类似,就是高阶函数返回一个内部函数,然后高阶函数内部的变量并没有随着高阶函数的返回而消失,仍然可以被返回的内部函数使用。
func avg() func(int) float64 {
sum := 0
count := 0
return func(x int) float64 {
sum += x
count++
return float64(sum) / float64(count)
}
}
func main() {
avg := avg()
fmt.Println(avg(1))
fmt.Println(avg(2))
fmt.Println(avg(3))
// 输出 1, 1.5, 2
}
方法
方法与函数的区别在于,方法拥有接收者,而函数没有(和 OOP 中的方法类似),且只有自定义类型能够拥有方法,其使用也类似于调用一个类的成员方法。
type Point struct {
X int
Y int
}
func (p Point) Distance(q Point) int {
return int(math.Sqrt(float64((p.X-q.X)*(p.X-q.X) + (p.Y-q.Y)*(p.Y-q.Y))))
}
func main() {
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(p.Distance(q))
// 输出 5
}
但是上文提到过可以用type声明新类型,如果有type myInt int,那么可以给myInt绑定方法,这间接实现了给内置类型绑定方法。
上面示例中的接收者是值,就被称作值接收者,此外还有指针接收者,它们之间的区别显然是能否修改实际接收者的值,这里有个语法糖是对于结构体指针而言,不需要解引用就可以访问其内容,实际是编译时补全解引用。
type Vector struct {
X int
Y int
}
func (p *Vector) Add(q Vector) {
p.X += q.X
p.Y += q.Y
}
func main() {
p := &Vector{1, 2}
q := Vector{4, 6}
p.Add(q)
fmt.Println(p)
// 输出 &{5, 8}
}
结构体
初始化
GO 抛弃了 OOP,结构体自然也没有构造函数
初始值直接赋
Go 喜欢创建者模式,当参数较多时,可以单独编写一个函数来初始化,实际上自定义类型都推荐写创建函数,相当于写了构造函数。
组合
GO 中没有继承,只有组合
type Student struct {
p Person
grade int
class int
}
type Teacher struct {
p Person
subject string
}
也可以不具名
这两种写法的区别在于访问时的写法
// 具名
s := Student{Person{Person{name: "Alice", age: 20}, grade: 1, class: 1}}
s.p.name = "Bob"
s.grade = 2
s.class = 2
// 不具名可以直接访问
// s.Person.name 也是正确的
s := Student{Person{Person{name: "Alice", age: 20}, grade: 1, class: 1}}
s.name = "Bob"
s.grade = 2
s.class = 2
接口
在 Go 语言中,接口是一种抽象类型,用于定义一组方法签名而不提供方法的实现。接口的核心理念是描述行为,而具体的行为实现由实现接口的类型提供。
接口的实现在 Go 中是隐式的,只要类型拥有了接口中定义的所有方法,那么这个类型就实现了这个接口。
// 这里的 Dog 实现了 Animal 接口
type Dog struct {
name string
}
func (d Dog) Eat() {
fmt.Println("Dog" + d.name + " is eating")
}
func (d Dog) Bark() {
fmt.Println("Dog" + d.name + " is barking")
}
和其它语言中的差不多,实现了这个接口就可以用这个接口存储
根据实现的定义,任何自定义类型都可以实现接口,甚至从内置类型衍生出来的类型也可以实现接口。
空接口就是函数声明时的Any,任何类型都实现了空接口,所以 Any 接口可以保存任何类型的值。
GO 提供了类型推断语法,用于判断一个变量是否是某个类型或者是否实现了某个接口。
var b int = 1
var a interface{} = b
// 判断 a 是否是 int 类型
if intVal, ok := a.(int); ok {
fmt.Println(intVal)
} else {
fmt.Println("error type")
}
// 结果为 1
泛型
泛型函数的语法格式类似于:
- 类型形参:
T就是一个类型形参,形参具体是什么类型取决于传进来什么类型 - 类型约束:
int | float64构成了一个类型约束,这个类型约束内规定了哪些类型是允许的
实际调用时类型可以写出,也可以省略,省略时会自动推断,不过前提是不会出现类型推断不唯一的情况。
泛型类型的语法格式如下:
type GenericMap[K comparable, V int | string | byte] map[K]V
gmap1 := GenericMap[int, string]{1: "hello world"}
gmap2 := make(GenericMap[string, byte], 0)
错误
在 Go 语言中,错误处理与传统的 try-catch 机制有所不同。Go 鼓励通过函数返回多个值来显式地处理错误。函数在可能发生错误的情况下,会返回一个额外的 error 类型的值。如果操作成功,错误值通常为nil;否则,它将是一个实现了error接口的非nil值。
在 Go 中的异常有三种级别:
- error:正常的流程出错,需要处理,直接忽略掉不处理程序也不会崩溃
- panic:很严重的问题,程序应该在处理完问题后立即退出
- fatal:非常致命的问题,程序应该立即退出
这里只考虑最常见的error,它的接口定义为:
下面是一个错误处理的示例