Skip to content

GO

约 3182 个字 321 行代码 预计阅读时间 15 分钟

简介

Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言,它抛弃了继承,弱化了 OOP。值得一提的是,你可能会在其它地方看见有人叫它Golang ,但它的官方名字其实是一直是Go,早期的时候由于go 这个域名被抢注了,所以官网的域名就采用了 golang.org,导致了许多人误解。

GO 语言的结构和 C 家族语言差不多,在下面这个简单的例子中,第一行是包声明,第二行是导入的包,第三行是 main 函数。

package main
import "fmt"
func main() {
    /* 这是我的第一个简单的程序 */
    fmt.Println("Hello, World!")
}

可以看到行尾并没有分号,在 Go 程序中,一行代表一个语句结束,理论上可以写分号,然后把两条语句写在一行,但是一般不推荐这样做(除了部分错误处理那种习惯写在一行的)

基础语法

Go 语言中,包是组织代码的单位,导入的最小单位是包,而不是.go文件,所有的 Go 程序都必须属于一个包,包名通常是小写的,包名应该与文件所在的目录名一致。

// mvc 模式下的 service 层
package service

可见性

Go 语言中,没有privatepublic关键字,可见性是通过大写或小写字母来区分的,大写字母开头的标识符是公开的,小写字母开头的标识符是私有的。

导入

导入就是用import 加上包名,可以是一个一个导入,也可以是用括号括起来导入多个包,Go不允许导入但不使用(会报错)。

import (
    "fmt"
    "math/rand"
)

如果包名重复了,或者比较复杂,可以取别名,如下所示:

import e "example"

如今的 Go 项目推荐使用 go mod 管理依赖,vendor 基本成为了过去式。

# 创建项目
go mod init projectName
# 清理依赖
go mod tidy

go mod init会在项目中生成两个文件go.mod文件,相当于是 Maven 的 xml 配置文件,拉取一个库使用go get -u url,其中-u参数表示拉取最新版本,当然也可以指定版本。

注释

注释和 C 语言一样,单行注释用// ,多行注释用/**/,可以使用go doc命令查看包和函数的文档,比如:

go doc fmt

Go 提供文档工具godocgo install golang.org/x/tools/cmd/godoc@latest),使用godoc -http=:6060可以启动一个HTTP服务加载HTML文档,然后访问http://localhost:6060/pkg/就可以看到文档。

格式化

Go 提供格式化工具,使用go fmt path可以格式化path路径下的 Go 文件。

变量

GO 的变量写法比较独特,类型在变量名后面,如果声明时没有赋值,那么变量的值会自动初始化为零值

var a string = "zju"
var b,c int = 1,2

不过 GO 支持类型推断,所以类型可以省略。

var a = "zju"
var b,c = 1,2

还可以少写var,这是短变量声明语法糖,但如果变量已经声明过了,那么就会报错。

a := "zju"
b,c := 1,2

还有一个常见语法是一次性声明多个变量,使用()包裹

var (
    a = "zju"
    b = 1
    c = 2
)

Go 不允许声明变量但不使用,同样会报错

常量

Go 语言常量的定义方式和变量类似,但是使用 const 关键字,常量必须在声明时就制定初值。

const a string = "zju"
const b,c = 1,2

除此以外,GO 还支持 iota,iota 是一个常量生成器,iota 会自动递增,然后依次计算第一行所写的表达式的值。

const (
    a = iota // 0
    b // 1
    c // 2
    d // 3
)

这个式子是自动推导的

const (
    a = iota *2 + 1 // 1
    b // 3
    c // 5
    d // 7
)

输入输出

fmt 包是 Go 语言的格式化输入输出包,常用fmt.Println输出一行内容

fmt.Println("Hello, World!") // 输出单行内容

支持C风格的格式化输出

fmt.Printf("%s", "123")

输入也是标准的 C 风格,要取地址

var a, b int
fmt.Scan(&a, &b)

分支与循环

支持ifswitch,但是条件不用打括号, 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一样使用,即只写循环条件,同样条件不需要打括号

// 一个简单的循环
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

Go 是用for-each风格的循环的,但是到数组再说。

基础数据类型

GO 是一个静态强类型的语言,GO 语言的声明始终遵循名称在前,类型在后的原则。可以通过type关键字声明新的类型,虽然前后的两个类型本质上是一个类型,但是编译器认为它们不一样。

type MyMap map[string]int

此外还可以取别名,这个前后两个类型就是被编译器认为是同一个类型

type Int = int

在GO 中,只存在显式类型转换,不存在隐式类型转换,转换类型必须是可以被目标类型代表的类型(即往大的转换),比如int16可以转化为int32,但是反过来不行,明显可能溢出。

零值

零值准确来说应该就默认值(毕竟不一定真的是数字0),一些引用类型的零值是nil,这个显然和null差不多,需要注意nil不属于任何类型,nil==nil是无法通过编译的。

数组

数组是值类型,所以函数传参时会拷贝,数组声明时的长度只能是常量。

var arr [5]int

可以有初始值

arr := [5]int{1, 2, 3, 4, 5}

有初始值的情况下长度可以省略

arr := [...]int{1, 2, 3, 4, 5}

还可以通过new函数获取一个指针

arr := new([5]int)

GO 内置了lencap 函数用于获取序列的长度和容量。此外,数组有切割语法,切割的结果即为切片,格式为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和普通函数的写法是一样的,并且切片是引用类型,故下面的代码不存在任何拷贝。

slice := []int{1, 2, 3}
slice = append(slice, 4)

删除元素使用切割语法就能实现,内存是自动回收的

slice := []int{1, 2, 3, 4, 5}
// 删除第二个元素之后的元素
slice = slice[:2]

拷贝切片使用copy函数,会自动扩容

a := make([]int, 0)
b := []int{1, 2, 3}
copy(a, b)

切片有for range遍历

for i, v := range a {
fmt.Println(i, v)
}// 输出索引和值

Go 中表示舍弃值使用_

for _, v := range a{
    fmt.Println(v)
}// 仅输出值

字符串

在 GO 中,字符串本质上是一个不可变只读的字节数组,普通的字符串使用双引号,此外还有原生字符串使用反引号。字符串全面支持utf-8编码,所以像"中文"这样的字符实际按字节存储为对应的unicode编码,但是这样在遍历时会出问题,按下标遍历到的不是单独的汉字,而是每个字节的内容,故需要使用for range遍历或者使用utf-8库。

Go 中涉及到转化为字符串常用fmt.Sprintf(),这个基本可以转换到任意格式,基本数据类型可以考虑使用strconv

映射表

映射表是无序的键值对集合,也是引用类型,它的键必须是可比较的(实现comparable接口)

初始化映射表如下

mp := map[int]string{
    1: "a",
    2: "b",
    3: "c",
}

也可以使用make获取指针

mp := make(map[int]string, 8)// 参数依次是类型和容量

GO 的 map 访问不存在的键会返回默认值,而且访问键其实有两个返回值,第一个是对应的值,第二个是键是否存在(布尔类型)

v, ok := mp[1]
if ok {
    fmt.Println(v)
}

添加元素直接赋值即可,删除元素使用delete函数

// 删除键值为 1
delete(mp, 1)

for range遍历是同时遍历键和值

for k, v := range mp {
    fmt.Println(k, v)
}

GO 不提供 Set,但是可以使用 map 的 key 来实现,值取成一个空的结构体,一个空的结构体并不会占用内存。

mp := map[int]struct{}{}
mp[1] = struct{}{}
if _, ok := mp[1]; ok {
    fmt.Println("1 exists")
}

指针

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 中也不存在默认参数

为了可读性,参数都应该写名称(除了接口,但是推荐都写上),对于类型相同的参数而言可以只声明一次类型,不过条件是必须相邻。

// a、b 均为 int,可以只声明一次
func sum(a, b int) int {
    return a + b
}

此外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 ,直接不写返回值即可。

func f() {
    fmt.Println("Hello, World!")
}

GO 允许有多个返回值,此时需要用括号将返回值括起来写在最后,类似于返回一个元组,甚至也是可以给返回值命名的。

func f() (int, int) {
    return 1, 2
}

func g() (a, b int) {
    a = 1
    b = 2
    return
}

匿名函数与闭包

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,结构体自然也没有构造函数

type Person struct {
    name string
    age int
}

初始值直接赋

p := Person{name: "Alice", age: 20}

Go 喜欢创建者模式,当参数较多时,可以单独编写一个函数来初始化,实际上自定义类型都推荐写创建函数,相当于写了构造函数。

func NewPerson(name string, age int) Person {
    return Person{name: name, age: age}
}

组合

GO 中没有继承,只有组合

type Student struct {
    p Person
    grade int
    class int
}

type Teacher struct {
    p Person
    subject string
}

也可以不具名

type Student struct {
    Person
    grade int
    class int
}

type Teacher struct {
    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 语言中,接口是一种抽象类型,用于定义一组方法签名而不提供方法的实现。接口的核心理念是描述行为,而具体的行为实现由实现接口的类型提供。

type Animal interface {
    Eat()
}

接口的实现在 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")
}

和其它语言中的差不多,实现了这个接口就可以用这个接口存储

var a Animal = Dog{name: "Alice"}
a.Eat()
// 错误
// a.Bark()

根据实现的定义,任何自定义类型都可以实现接口,甚至从内置类型衍生出来的类型也可以实现接口。

type Int int

type Adder interface {
    Add(Int) Int
}

func (i Int) Add(j Int) Int {
    return i + j
}

空接口就是函数声明时的Any,任何类型都实现了空接口,所以 Any 接口可以保存任何类型的值。

type Any interface{
}

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

泛型

泛型函数的语法格式类似于:

func Sum[T int | float64](a, b T) T {
    return a + b
}
  • 类型形参:T 就是一个类型形参,形参具体是什么类型取决于传进来什么类型
  • 类型约束:int | float64 构成了一个类型约束,这个类型约束内规定了哪些类型是允许的

实际调用时类型可以写出,也可以省略,省略时会自动推断,不过前提是不会出现类型推断不唯一的情况。

res1 := Sum[int](1, 2)
res2 := Sum(1, 2)

泛型类型的语法格式如下:

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,它的接口定义为:

type error interface {
    Error() string
}

下面是一个错误处理的示例

package main

import (
    "fmt"
    "strconv"
)

func main() {
    var numStr string
    fmt.Scan("%s", &numStr)
    num, err := strconv.Atoi(numStr) // Atoi 函数返回 (int, error)
    if err != nil {
        fmt.Printf("转换字符串到整数失败: %v\n", err)
    } else {
        fmt.Printf("成功将 %s 转换为整数: %d\n", numStr, num)
    }
}