11 - Go 语言入门指南:基础语法和常用特性解析 | 青训营

111 阅读29分钟

Go 语言是一种开源的编程语言,由 Google 的 Robert Griesemer, Rob Pike 和 Ken Thompson 在 2007 年设计,于 2009 年正式发布。Go 语言的目标是提供一种简洁、高效、可靠和跨平台的编程语言,适用于系统编程、网络编程、并发编程和分布式编程等领域。

本文将介绍 Go 语言的基础语法和常用特性,包括:

  • 变量、常量和类型
  • 函数和方法
  • 控制流程
  • 数组、切片和映射
  • 结构体和接口
  • 并发和通道

变量、常量和类型

在 Go 语言中,变量是用来存储数据的标识符,常量是不可改变的值,类型是用来描述变量或常量的性质的标签。

变量

Go 语言中声明变量的一般形式是:

var name type = expression

其中,name 是变量的名称,type 是变量的类型,expression 是变量的初始值。如果省略 type,则由编译器根据 expression 的类型推断变量的类型。如果省略 expression,则变量的初始值为该类型的零值(zero value),例如 0 对于数值类型,"" 对于字符串类型,nil 对于指针或引用类型等。

例如:

var x int = 10 // 声明一个整型变量 x,并赋值为 10
var y = 20     // 声明一个整型变量 y,并赋值为 20(类型推断)
var z string   // 声明一个字符串变量 z,并赋值为空字符串(零值)

在函数内部,可以使用短变量声明(short variable declaration)的形式来声明并初始化一个或多个变量,其语法为:

name := expression

其中,name 是一个或多个以逗号分隔的变量名称,expression 是一个或多个以逗号分隔的表达式。短变量声明可以重复声明已经存在的变量,只要至少有一个新的变量被定义。

例如:

a, b := 1, 2       // 声明并初始化两个整型变量 a 和 b
c := "Hello"       // 声明并初始化一个字符串变量 c
a, d := b, a + b   // 交换 a 和 b 的值,并声明一个新的整型变量 d
e, f := f, e + f   // 错误:f 没有被定义

常量

Go 语言中声明常量的一般形式是:

const name type = expression

其中,name 是常量的名称,type 是常量的类型,expression 是常量的值。如果省略 type,则由编译器根据 expression 的类型推断常量的类型。常量必须在声明时就赋值,并且不能再被修改。

例如:

const pi float64 = 3.14159 // 声明一个浮点型常量 pi,并赋值为 3.14159
const e = 2.71828          // 声明一个浮点型常量 e,并赋值为 2.71828(类型推断)
const s = "Hello"          // 声明一个字符串常量 s,并赋值为 "Hello"
const n                    // 错误:没有赋值
s = "World"                // 错误:不能修改常量

在 Go 语言中,还有一种特殊的常量,叫做无类型常量(untyped constant)。无类型常量没有明确的基础类型,而是根据上下文的需要,自动转换为合适的类型。例如,pi 和 e 就是无类型常量,它们可以被赋值给任何浮点型变量,而不会造成精度的损失。

例如:

var f32 float32 = pi // 无类型常量 pi 被转换为 float32 类型
var f64 float64 = pi // 无类型常量 pi 被转换为 float64 类型
var c64 complex64 = e + 0i // 无类型常量 e 被转换为 complex64 类型

类型

Go 语言是一种静态类型(statically typed)的语言,也就是说,每个变量都有一个明确的类型,在编译时就确定了,不能在运行时改变。Go 语言提供了多种基本的数据类型,包括:

  • 布尔型(bool):表示真或假的值,只有两个可能的值:true 或 false
  • 数值型(numeric):表示整数或浮点数的值,有多种不同的长度和精度的类型,例如 int8int16int32int64uint8uint16uint32uint64float32float64complex64complex128 等。
  • 字符串型(string):表示一系列的字节(byte)或字符(rune),使用双引号(")或反引号(`)来定义。
  • 指针型(pointer):表示一个变量的内存地址,使用星号(*)来定义和解引用。
  • 数组型(array):表示一组固定长度且相同类型的元素,使用方括号([])来定义和索引。
  • 切片型(slice):表示一个可变长度且相同类型的元素序列,使用方括号([])来定义和索引。
  • 映射型(map):表示一个由键值对组成的无序集合,使用关键字 map 和方括号([])来定义和索引。
  • 结构体型(struct):表示一个由若干字段(field)组成的聚合类型,使用关键字 struct 和花括号({})来定义和初始化。
  • 接口型(interface):表示一组方法(method)的集合,用来描述一个抽象的行为,使用关键字 interface 和花括号({})来定义和实现。
  • 函数型(function):表示一段可执行的代码块,用来完成一定的任务,使用关键字 func 和圆括号(()) 来定义和调用。

除了基本的数据类型外,Go 语言还支持自定义类型(custom type),也就是使用已有的类型作为基础,创建一个新的类型。自定义类型可以通过关键字 type 来定义,其语法为:

type name underlying-type

其中,name 是自定义类型的名称,underlying-type 是自定义类型的基础类型。自定义类型可以拥有自己的方法和属性,并且可以实现接口。

例如:

type Point struct { // 定义一个结构体类型 Point
    x, y float64
}

type Color string // 定义一个字符串类型 Color

type Circle struct { // 定义一个结构体类型 Circle
    center Point
    radius float64
    color  Color
}

func (p Point) Distance(q Point) float64 { // 定义一个方法 Distance,计算两个点之间的距离
    return math.Sqrt((p.x-q.x)*(p.x-q.x) + (p.y-q.y)*(p.y-q.y))
}

func (c Circle) Area() float64 { // 定义一个方法 Area,计算圆的面续写:

    return math.Pi * c.radius * c.radius // 返回圆周率乘以半径的平方

}

func (c Circle) Perimeter() float64 { // 定义一个方法 Perimeter,计算圆的周长

    return 2 * math.Pi * c.radius // 返回 2 乘以圆周率乘以半径
    
}
func main() { // 定义一个主函数,用来测试自定义类型和方法 
    p := Point{1, 2} // 创建一个 Point 类型的变量 p,并初始化为 (1, 2) 
    q := Point{3, 4} // 创建一个 Point 类型的变量 q,并初始化为 (3, 4)
    fmt.Println(p.Distance(q)) // 调用 p 的 Distance 方法,传入 q 作为参数,打印结果
    c := Circle{Point{0, 0}, 5, "red"} // 创建一个 Circle 类型的变量 c,并初始化为圆心为 (0, 0),半径为 5,颜色为 "red"
    fmt.Println(c.Area()) // 调用 c 的 Area 方法,打印结果
    fmt.Println(c.Perimeter()) // 调用 c 的 Perimeter 方法,打印结果
}

函数和方法

在 Go 语言中,函数是一种可以被调用的代码块,用来完成一定的任务。函数可以接受零个或多个参数,并且可以返回零个或多个结果。函数的一般形式是:

func name(parameter-list) (result-list) {
    body
}

其中,name 是函数的名称,parameter-list 是函数的参数列表,result-list 是函数的结果列表,body 是函数的主体部分。如果函数没有参数或结果,可以省略圆括号。如果函数只有一个结果,可以省略结果的名称和圆括号。

例如:

func add(x, y int) int { // 定义一个函数 add,接受两个整型参数 x 和 y,返回一个整型结果
    return x + y // 返回 x 和 y 的和
}

func swap(x, y string) (string, string) { // 定义一个函数 swap,接受两个字符串参数 x 和 y,返回两个字符串结果
    return y, x // 返回 x 和 y 的交换
}

func hello() { // 定义一个函数 hello,没有参数和结果
    fmt.Println("Hello, world!") // 打印 "Hello, world!"
}

在 Go 语言中,函数是一种值类型(value type),也就是说,函数可以被赋值给变量,也可以作为参数或结果传递给其他函数。此外,Go 语言还支持匿名函数(anonymous function),也就是没有名称的函数,通常用来定义闭包(closure)或者作为回调(callback)。

例如:

f := func(x, y int) int { // 定义一个匿名函数,并赋值给变量 f
    return x * y // 返回 x 和 y 的积
}

fmt.Println(f(2, 3)) // 调用 f,并打印结果

func apply(f func(int, int) int, x, y int) int { // 定义一个函数 apply,接受一个函数类型的参数 f 和两个整型参数 x 和 y,返回一个整型结果
    return f(x, y) // 返回 f 对 x 和 y 的调用结果
}

fmt.Println(apply(add, 4, 5)) // 调用 apply,并传入 add 函数和 4 和 5 作为参数,并打印结果

fmt.Println(apply(func(x, y int) int { // 调用 apply,并传入一个匿名函数和 6 和 7 作为参数,并打印结果
    return x - y // 返回 x 和 y 的差
}, 6, 7))

在 Go 语言中,方法是一种特殊的函数,它是绑定在某个类型上的,只能通过该类型的变量或指针来调用。方法的一般形式是:

func (receiver type) name(parameter-list) (result-list) {
    body
}

其中,receiver type 是方法的接收者类型,它表示该方法属于哪个类型。nameparameter-listresult-list 和 body 的含义与普通函数相同。方法可以看作是一种语法糖(syntactic sugar),它实际上是将接收者作为第一个参数传递给函数。

type Point struct { // 定义一个结构体类型 Point
    x, y float64
}

func (p Point) Distance(q Point) float64 { // 定义一个方法 Distance,接受一个 Point 类型的接收者 p 和一个 Point 类型的参数 q,返回一个浮点型结果
    return math.Sqrt((p.x-q.x)*(p.x-q.x) + (p.y-q.y)*(p.y-q.y)) // 返回 pq 之间的距离
}

func (p *Point) ScaleBy(factor float64) { // 定义一个方法 ScaleBy,接受一个 Point 类型的指针接收者 p 和一个浮点型参数 factor
    p.x *= factor // 将 p 的 x 坐标乘以 factor
    p.y *= factor // 将 p 的 y 坐标乘以 factor
}

p := Point{1, 2} // 创建一个 Point 类型的变量 p,并初始化为 (1, 2)
q := Point{3, 4} // 创建一个 Point 类型的变量 q,并初始化为 (3, 4)
fmt.Println(p.Distance(q)) // 调用 p 的 Distance 方法,传入 q 作为参数,打印结果
p.ScaleBy(2) // 调用 p 的 ScaleBy 方法,传入 2 作为参数
fmt.Println(p) // 打印 p 的值

控制流程

在 Go 语言中,控制流程是指程序的执行顺序和分支。Go 语言提供了三种基本的控制流程结构:顺序结构(sequential structure),选择结构(selection structure)和循环结构(loop structure)。

顺序结构

顺序结构是最简单的控制流程结构,它表示程序按照代码的书写顺序,从上到下,依次执行每一条语句。顺序结构是程序的默认执行方式,不需要任何特殊的语法。

例如:

fmt.Println("Hello, world!") // 打印 "Hello, world!"
x := 10 // 声明并初始化一个整型变量 x
y := x + 5 // 声明并初始化一个整型变量 y,赋值为 x + 5
fmt.Println(y) // 打印 y 的值

选择结构

选择结构是一种根据条件来决定程序执行哪一条分支的控制流程结构。Go 语言提供了两种选择结构:if-else 结构和 switch-case 结构。

if-else 结构

if-else 结构是一种最常见的选择结构,它表示如果满足某个条件,就执行某个语句块,否则就执行另一个语句块。if-else 结构的一般形式是:

if condition {
    statements
} else {
    statements
}

其中,condition 是一个布尔型的表达式,用来判断条件是否成立。如果 condition 的值为 true,就执行 if 后面的语句块;如果 condition 的值为 false,就执行 else 后面的语句块。如果只有一个分支需要执行,可以省略 else 部分。

例如:

x := 10 // 声明并初始化一个整型变量 x
if x > 0 { // 判断 x 是否大于 0
    fmt.Println("x is positive") // 如果是,打印 "x is positive"
} else { // 否则
    fmt.Println("x is negative or zero") // 打印 "x is negative or zero"
}

y := -5 // 声明并初始化一个整型变量 y
if y < 0 { // 判断 y 是否小于 0
    fmt.Println("y is negative") // 如果是,打印 "y is negative"
}

在 Go 语言中,还可以在 if 条件之前添加一个简短的语句,用来声明或初始化一个变量,该变量的作用域仅限于 if-else 结构内部。

例如:

if z := x + y; z > 0 { // 在 if 条件之前声明并初始化一个整型变量 z,赋值为 x + y,并判断 z 是否大于 0
    fmt.Println("z is positive") // 如果是,打印 "z is positive"
} else { // 否则
    fmt.Println("z is negative or zero") // 打印 "z is negative or zero"
}
fmt.Println(z) // 错误:z 的作用域仅限于 if-else 结构内部

在 Go 语言中,还可以使用 if-else if-else 结构来表示多个条件之间的选择。其语法为:

if condition1 {
    statements
} else if condition2 {
    statements
} else if condition3 {
    statements
} else {
    statements
}

其中,condition1condition2condition3 等都是布尔型的表达式,用来判断条件是否成立。程序会从上到下依次检查每个条件,如果某个条件为 true,就执行对应的语句块,并跳出整个结构;如果所有条件都为 false,就执行最后的 else 部分。如果只有一个分支需要执行,可以省略 else 部分。

例如:

score := 85 // 声明并初始化一个整型变量 score
if score >= 90 { // 判断 score 是否大于等于 90
    fmt.Println("A") // 如果是,打印 "A"
} else if score >= 80 { // 否则,判断 score 是否大于等于 80
    fmt.Println("B") // 如果是,打印 "B"
} else if score >= 70 { // 否则,判断 score 是否大于等于 70
    fmt.Println("C") // 如果是,打印 "C"
} else if score >= 60 { // 否则,判断 score 是否大于等于 60
    fmt.Println("D") // 如果是,打印 "D"
} else { // 否则
    fmt.Println("F") // 打印 "F"
}

循环结构

循环结构是一种重复执行某个语句块,直到满足某个条件为止的控制流程结构。Go 语言只提供了一种循环结构:for 结构。

for 结构

for 结构是一种最通用的循环结构,它表示在满足某个条件的情况下,重复执行某个语句块。for 结构的一般形式是:

for init; condition; post {
    statements
}

其中,init 是一个可选的语句,用来初始化循环变量;condition 是一个布尔型的表达式,用来判断循环是否继续;post 是一个可选的语句,用来更新循环变量;statements 是循环体部分,用来执行重复的操作。如果省略 init 和 post,可以省略分号;如果省略 condition,则表示无限循环。

例如:

sum := 0 // 声明并初始化一个整型变量 sum
for i := 1; i <= 10; i++ { // 使用 for 结构创建一个循环,初始化 i 为 1,判断 i 是否小于等于 10,每次循环后 i 自增 1
    sum += i // 将 sum 加上 i
}
fmt.Println(sum) // 打印 sum 的值

n := 1 // 声明并初始化一个整型变量 n
for n < 100 { // 使用 for 结构创建一个循环,判断 n 是否小于 100
    n *= 2 // 将 n 乘以 2
}
fmt.Println(n) // 打印 n 的值

for { // 使用 for 结构创建一个无限循环
    fmt.Println("Hello, world!") // 打印 "Hello, world!"
}

在 Go 语言中,还可以使用 range 关键字来遍历数组、切片、映射或字符串等可迭代的对象。其语法为:

for index, value := range iterable {
    statements
}

其中,index 是迭代对象的索引或键,value 是迭代对象的元素或值,iterable 是一个可迭代的对象,如数组、切片、映射或字符串等。如果不需要使用 index 或 value,可以使用空白标识符(blank identifier) _ 来忽略它们。

例如:

a := [5]int{1, 2, 3, 4, 5} // 声明并初始化一个数组 a
for i, v := range a { // 使用 for-range 结构遍历数组 a
    fmt.Printf("a[%d] = %d\n", i, v) // 打印数组 a 的索引和元素
}

s := "Hello" // 声明并初始化一个字符串 s
for _, c := range s { // 使用 for-range 结构遍历字符串 s
    fmt.Printf("%c ", c) // 打印字符串 s 的字符
}
fmt.Println()

在 Go 语言中,还可以使用 break 和 continue 关键字来控制循环的流程。break 关键字用来跳出当前的循环体;continue 关键字用来跳过当前的循环迭代。如果有多层嵌套的循环,可以使用标签(label)来指定要跳出或跳过的循环层次。标签是一个以冒号结尾的标识符,放在循环体之前。

例如:

outer: 
for i := 0; i < 10; i++ { // 使用 for 结构创建一个外层循环,初始化 i0,判断 i 是否小于 10,每次循环后 i 自增 1
    for j := 0; j < 10; j++ { // 使用 for 结构创建一个内层循环,初始化 j 为 0,判断 j 是否小于 10,每次循环后 j 自增 1
        if i + j == 10 { // 判断 i + j 是否等于 10
            break outer // 如果是,使用 break 关键字跳出外层循环
        }
        fmt.Printf("(%d, %d) ", i, j) // 打印 i 和 j 的值
    }
    fmt.Println()
}
fmt.Println("Done") // 打印 "Done"

数组、切片和映射

在 Go 语言中,数组、切片和映射是三种常用的复合数据类型,它们可以用来存储和操作多个值。

数组

数组是一种固定长度且相同类型的元素序列,它在内存中占据一块连续的空间。数组的长度是数组类型的一部分,不同长度的数组属于不同的类型。数组的一般形式是:

var name [length]type

其中,name 是数组的名称,length 是数组的长度,type 是数组的元素类型。数组可以在声明时就初始化,也可以在声明后再赋值。

例如:

var a [5]int // 声明一个长度为 5 的整型数组 a
a[0] = 1 // 给数组 a 的第一个元素赋值为 1
a[1] = 2 // 给数组 a 的第二个元素赋值为 2
a[2] = 3 // 给数组 a 的第三个元素赋值为 3
a[3] = 4 // 给数组 a 的第四个元素赋值为 4
a[4] = 5 // 给数组 a 的第五个元素赋值为 5

var b [3]string = [3]string{"Hello", "World", "!"} // 声明并初始化一个长度为 3 的字符串数组 b

c := [...]int{1, 2, 3, 4, 5} // 使用 ... 来让编译器推断数组的长度,并声明并初始化一个整型数组 c

在 Go 语言中,数组是一种值类型(value type),也就是说,数组之间的赋值或传递都会导致整个数组的复制。如果想要避免复制,可以使用指向数组的指针。

例如:

d := a // 将 a 赋值给 d,会复制整个数组 a
d[0] = 10 // 修改 d 的第一个元素
fmt.Println(a[0]) // 打印 a 的第一个元素,不受 d 的影响

e := &a // 将 a 的地址赋值给 e,得到一个指向数组 a 的指针
e[0] = 10 // 修改 e 指向的数组的第一个元素
fmt.Println(a[0]) // 打印 a 的第一个元素,受 e 的影响

切片

切片是一种可变长度且相同类型的元素序列,它是对底层数组的一个视图(view)。切片可以通过截取(slicing)已有的数组或切片来创建,也可以通过内置函数 make 来创建。切片的一般形式是:

var name []type

其中,name 是切片的名称,type 是切片的元素类型。切片可以在声明时就初始化,也可以在声明后再赋值。

例如:

f := a[1:3] // 使用截取操作符 [low:high] 来从数组 a 中创建一个切片 f,包含从索引 low 到 high-1 的元素
g := f[:2] // 使用截取操作符 [:high] 来从切片 f 中创建一个切片 g,包含从索引 0 到 high-1 的元素
h := g[1:] // 使用截取操作符 [low:] 来从切片 g 中创建一个切片 h,包含从索引 low 到末尾的元素

var i []int = []int{1, 2, 3, 4, 5} // 声明并初始化一个整型切片 i

j := []string{"Hello", "World", "!"} // 使用简短声明语法来声明并初始化一个字符串切片 j

k := make([]float64, 3) // 使用 make 函数来创建一个长度为 3 的浮点型切片 k,并初始化为零值

在 Go 语言中,切片是一种引用类型(reference type),也就是说,切片之间的赋值或传递都不会导致切片的复制,而是共享同一个底层数组。如果想要复制切片,可以使用内置函数 copy 来创建一个新的切片。

例如:

l := i // 将 i 赋值给 l,不会复制切片 i
l[0] = 10 // 修改 l 的第一个元素
fmt.Println(i[0]) // 打印 i 的第一个元素,受 l 的影响

m := make([]int, len(i)) // 使用 make 函数来创建一个长度与 i 相同的整型切片 m
copy(m, i) // 使用 copy 函数来复制切片 i 到 m
m[0] = 10 // 修改 m 的第一个元素
fmt.Println(i[0]) // 打印 i 的第一个元素,不受 m 的影响

在 Go 语言中,切片有三个属性:长度(length),容量(capacity)和指针(pointer)。长度表示切片中当前元素的个数,容量表示切片在底层数组中能够容纳的最大元素个数,指针表示切片在底层数组中的起始位置。可以使用内置函数 len 和 cap 来获取切片的长度和容量,也可以使用内置函数 append 来向切片中追加元素。

例如:

n := []int{1, 2, 3} // 声明并初始化一个整型切片 n
fmt.Println(len(n)) // 打印 n 的长度,为 3
fmt.Println(cap(n)) // 打印 n 的容量,为 3

n = append(n, 4) // 使用 append 函数向 n 中追加一个元素 4
fmt.Println(len(n)) // 打印 n 的长度,为 4
fmt.Println(cap(n)) // 打印 n 的容量,为 6(容量会自动扩展)

映射

映射是一种由键值对组成的无序集合,它可以用来存储和检索任意类型的数据。映射的键必须是可比较的类型,如整型、字符串、布尔型等,而值可以是任意类型。映射的一般形式是:

var name map[key-type]value-type

其中,name 是映射的名称,key-type 是映射的键类型,value-type 是映射的值类型。映射可以在声明时就初始化,也可以在声明后再赋值。

例如:

var m map[string]int // 声明一个以字符串为键,以整型为值的映射 m
m = make(map[string]int) // 使用 make 函数来创建一个空的映射 m
m["one"] = 1 // 给映射 m 的键 "one" 赋值为 1
m["two"] = 2 // 给映射 m 的键 "two" 赋值为 2
m["three"] = 3 // 给映射 m 的键 "three" 赋值为 3

n := map[string]string{"name": "Alice", "age": "20", "gender": "female"} // 声明并初始化一个以字符串为键,以字符串为值的映射 n

o := map[int][]int{1: {1, 2, 3}, 2: {4, 5, 6}, 3: {7, 8, 9}} // 声明并初始化一个以整型为键,以切片为值的映射 o

在 Go 语言中,映射是一种引用类型(reference type),也就是说,映射之间的赋值或传递都不会导致映射的复制,而是共享同一个底层数据结构。如果想要复制映射,可以使用循环来创建一个新的映射。

例如:

p := n // 将 n 赋值给 p,不会复制映射 n
p["name"] = "Bob" // 修改 p 的键 "name" 的值
fmt.Println(n["name"]) // 打印 n 的键 "name" 的值,受 p 的影响

q := make(map[string]string) // 使用 make 函数来创建一个空的字符串映射 q
for k, v := range n { // 使用 for-range 结构遍历映射 n
    q[k] = v // 将 n 的键值对复制到 q 中
}
q["name"] = "Bob" // 修改 q 的键 "name" 的值
fmt.Println(n["name"]) // 打印 n 的键 "name" 的值,不受 q 的影响

在 Go 语言中,可以使用索引操作符([])来获取或设置映射中的键值对。如果访问一个不存在的键,会得到该类型的零值。如果想要判断一个键是否存在于映射中,可以使用 ok-idiom 来获取一个布尔型的返回值。如果想要删除一个键值对,可以使用内置函数 delete 来实现。

例如:

fmt.Println(m["one"]) // 打印 m 的键 "one" 的值,为 1
fmt.Println(m["four"]) // 打印 m 的键 "four" 的值,为 0(不存在)

v, ok := m["two"] // 使用 ok-idiom 来判断 m 中是否有键 "two"
if ok { // 如果有
    fmt.Println(v) // 打印该键的值,为 2
} else { // 如果没有
    fmt.Println("not found") // 打印 "not found"
}

delete(m, "three") // 使用 delete 函数删除 m 中的键 "three"
fmt.Println(m["three"]) // 打印 m 的键 "three" 的值,为 0(不存在)

结构体和接口

在 Go 语言中,结构体和接口是两种重要的自定义类型,它们可以用来描述和抽象数据和行为。

结构体

结构体是一种由若干字段(field)组成的聚合类型,它可以用来表示一个实体的属性和状态。结构体的一般形式是:

type name struct {
    field1 type1
    field2 type2
    ...
}

其中,name 是结构体的名称,field1field2 等是结构体的字段名称,type1type2 等是结构体的字段类型。结构体可以在声明时就初始化,也可以在声明后再赋值。

例如:

type Person struct { // 定义一个结构体类型 Person
    name string // 定义一个字符串类型的字段 name
    age  int    // 定义一个整型类型的字段 age
}

var p Person // 声明一个 Person 类型的变量 p
p.name = "Alice" // 给 p 的 name 字段赋值为 "Alice"
p.age = 20 // 给 p 的 age 字段赋值为 20

q := Person{"Bob", 25} // 声明并初始化一个 Person 类型的变量 q,按照字段顺序赋值

r := Person{name: "Charlie", age: 30} // 声明并初始化一个 Person 类型的变量 r,按照字段名称赋值

在 Go 语言中,结构体是一种值类型(value type),也就是说,结构体之间的赋值或传递都会导致结构体的复制。如果想要避免复制,可以使用指向结构体的指针。

例如:

s := p // 将 p 赋值给 s,会复制整个结构体 p
s.name = "David" // 修改 s 的 name 字段
fmt.Println(p.name) // 打印 p 的 name 字段,不受 s 的影响

t := &p // 将 p 的地址赋值给 t,得到一个指向结构体 p 的指针
t.name = "David" // 修改 t 指向的结构体的 name 字段
fmt.Println(p.name) // 打印 p 的 name 字段,受 t 的影响

在 Go 语言中,可以使用点操作符(.)来访问或修改结构体的字段。如果使用指针来访问或修改结构体的字段,可以省略解引用操作符(*),编译器会自动处理。

例如:

fmt.Println(q.name) // 打印 q 的 name 字段,为 "Bob"
fmt.Println(q.age) // 打印 q 的 age 字段,为 25

fmt.Println((*t).name) // 打印 t 指向的结构体的 name 字段,为 "David"
fmt.Println(t.age) // 打印 t 指向的结构体的 age 字段,为 20(省略了解引用操作符)

接口

接口是一种由一组方法(method)签名组成的集合,它可以用来描述一个抽象的行为。接口的一般形式是:

type name interface {
    method1(parameter-list) (result-list)
    method2(parameter-list) (result-list)
    ...
}

其中,name 是接口的名称,method1method2 等是接口的方法签名,包括方法的名称、参数列表和结果列表。接口只定义了方法的签名,而不定义方法的具体实现。任何类型只要实现了接口中所有方法的签名,就可以说该类型实现了该接口。

例如:

type Shape interface { // 定义一个接口类型 Shape
    Area() float64 // 定义一个无参数且返回浮点型结果的方法签名 Area
    Perimeter() float64 // 定义一个无参数且返回浮点型结果的方法签名 Perimeter
}

type Rectangle struct { // 定义一个结构体类型 Rectangle
    length, width float64
}

func (r Rectangle) Area() float64 { // 为 Rectangle 类型定义一个方法 Area,实现 Shape 接口的方法签名
    return r.length * r.width // 返回矩形的面积
}

func (r Rectangle) Perimeter() float64 { // 为 Rectangle 类型定义一个方法 Perimeter,实现 Shape 接口的方法签名
    return 2 * (r.length + r.width) // 返回矩形的周长
}

type Circle struct { // 定义一个结构体类型 Circle
    radius float64
}

func (c Circle) Area() float64 { // 为 Circle 类型定义一个方法 Area,实现 Shape 接口的方法签名
    return math.Pi * c.radius * c.radius // 返回圆的面积
}

func (c Circle) Perimeter() float64 { // 为 Circle 类型定义一个方法 Perimeter,实现 Shape 接口的方法签名
    return 2 * math.Pi * c.radius // 返回圆的周长
}

在 Go 语言中,接口是一种引用类型(reference type),也就是说,接口变量存储的是实现接口的类型的值或指针。接口变量可以赋值给任何实现了该接口的类型的变量,也可以从任何实现了该接口的类型的变量中获取值。接口变量可以使用点操作符(.)来调用接口中定义的方法。

例如:

var s Shape // 声明一个 Shape 类型的接口变量 s

r := Rectangle{10, 5} // 声明并初始化一个 Rectangle 类型的变量 r
s = r // 将 r 赋值给 s,s 存储了 r 的值
fmt.Println(s.Area()) // 调用 s 的 Area 方法,打印结果
fmt.Println(s.Perimeter()) // 调用 s 的 Perimeter 方法,打印结果

c := Circle{5} // 声明并初始化一个 Circle 类型的变量 c
s = &c // 将 c 的地址赋值给 s,s 存储了 c 的指针
fmt.Println(s.Area()) // 调用 s 的 Area 方法,打印结果
fmt.Println(s.Perimeter()) // 调用 s 的 Perimeter 方法,打印结果

t := s.(*Circle) // 使用类型断言(type assertion)来从 s 中获取 Circle 类型的指针 t
fmt.Println(t.radius) // 打印 t 的 radius 字段,为 5

并发和通道

在 Go 语言中,并发(concurrency)是一种编程模式,它可以让多个任务在同一时间段内交替执行,从而提高程序的效率和响应性。Go 语言提供了两个关键的特性来支持并发编程:goroutine 和 channel。

goroutine

goroutine 是一种轻量级的线程(thread),它由 Go 运行时(runtime)管理和调度,可以在一个进程(process)中同时运行多个 goroutine。goroutine 的一般形式是:

go function(arguments)

其中,go 是一个关键字,用来创建一个新的 goroutine;function 是一个函数名称,用来指定要执行的任务;arguments 是一个或多个参数,用来传递给函数。当程序执行到 go 语句时,会立即返回主函数(main function),而不会等待 goroutine 完成。

例如:

func say(s string) { // 定义一个函数 say,接受一个字符串参数 s
    for i := 0; i < 5; i++ { // 使用 for 结构创建一个循环,循环 5 次
        time.Sleep(100 * time.Millisecond) // 每次循环前暂停 100 毫秒
        fmt.Println(s) // 打印 s 的值
    }
}

func main() { // 定义一个主函数
    go say("Hello") // 使用 go 关键字创建一个新的 goroutine,并调用 say 函数,传入 "Hello" 作为参数
    say("World") // 在主函数中调用 say 函数,传入 "World" 作为参数
}

在 Go 语言中,主函数(main function)也是一个特殊的 goroutine,当主函数返回时,所有的其他 goroutine 都会被终止。如果想要让主函数等待其他 goroutine 完成,可以使用 sync.WaitGroup 类型来实现。

例如:

var wg sync.WaitGroup // 声明一个 sync.WaitGroup 类型的变量 wg

func say(s string) { // 定义一个函数 say,接受一个字符串参数 s
    defer wg.Done() // 使用 defer 关键字在函数返回前调用 wg 的 Done 方法,表示该 goroutine 完成
    for i := 0; i < 5; i++ { // 使用 for 结构创建一个循环,循环 5 次
        time.Sleep(100 * time.Millisecond) // 每次循环前暂停 100 毫秒
        fmt.Println(s) // 打印 s 的值
    }
}

func main() { // 定义一个主函数
    wg.Add(1) // 调用 wg 的 Add 方法,传入 1 作为参数,表示有一个新的 goroutine 要等待
    go say("Hello") // 使用 go 关键字创建一个新的 goroutine,并调用 say 函数,传入 "Hello" 作为参数
    wg.Add(1) // 调用 wg 的 Add 方法,传入 1 作为参数,表示有一个新的 goroutine 要等待
    go say("World") // 使用 go 关键字创建一个新的 goroutine,并调用 say 函数,传入 "World" 作为参数
    wg.Wait() // 调用 wg 的 Wait 方法,阻塞主函数,直到所有的 goroutine 完成
}

channel

channel 是一种可以在不同的 goroutine 之间传递数据的管道(pipe),它可以实现数据的共享和同步。channel 的一般形式是:

var name chan type

其中,name 是 channel 的名称,chan 是一个关键字,用来表示 channel 类型,type 是 channel 中传递的数据类型。channel 可以使用内置函数 make 来创建,并指定其容量(capacity)。如果不指定容量,则表示创建一个无缓冲(unbuffered)的 channel。

var c chan int // 声明一个以整型为数据类型的 channel c
c = make(chan int) // 使用 make 函数创建一个无缓冲的 channel c

d := make(chan string, 10) // 使用 make 函数创建一个容量为 10 的以字符串为数据类型的 channel d

在 Go 语言中,可以使用发送操作符(<-)来向 channel 中发送数据,也可以使用接收操作符(<-)来从 channel 中接收数据。如果 channel 是无缓冲的,那么发送和接收操作都会阻塞当前的 goroutine,直到另一个 goroutine 准备好进行对应的操作。如果 channel 是有缓冲的,那么发送操作会在 channel 未满时进行,接收操作会在 channel 非空时进行,否则也会阻塞当前的 goroutine。

例如:

func sum(a []int, c chan int) { // 定义一个函数 sum,接受一个整型切片 a 和一个整型 channel c 作为参数
    sum := 0 // 声明并初始化一个整型变量 sum
    for _, v := range a { // 使用 for-range 结构遍历切片 a
        sum += v // 将 sum 加上切片 a 的元素
    }
    c <- sum // 向 channel c 中发送 sum 的值
}

func main() { // 定义一个主函数
    a := []int{1, 2, 3, 4, 5} // 声明并初始化一个整型切片 a
    c := make(chan int) // 使用 make 函数创建一个无缓冲的整型 channel c
    go sum(a[:len(a)/2], c) // 使用 go 关键字创建一个新的 goroutine,并调用 sum 函数,传入切片 a 的前半部分和 channel c 作为参数
    go sum(a[len(a)/2:], c) // 使用 go 关键字创建一个新的 goroutine,并调用 sum 函数,传入切片 a 的后半部分和 channel c 作为参数
    x, y := <-c, <-c // 从 channel c 中接收两个值,并赋值给变量 x 和 y
    fmt.Println(x, y, x+y) // 打印 x, y 和 x+y 的值
}

在 Go 语言中,还可以使用 range 关键字来遍历 channel 中的数据,直到 channel 被关闭(close)。可以使用内置函数 close 来关闭一个 channel,表示不再向该 channel 发送数据。关闭一个已经关闭的 channel 或者向一个已经关闭的 channel 发送数据都会导致运行时(runtime)错误。

例如:

func fibonacci(n int, c chan int) { // 定义一个函数 fibonacci,接受一个整型参数 n 和一个整型 channel c 作为参数
    x, y := 0, 1 // 声明并初始化两个整型变量 x 和 y,分别赋值为 0 和 1
    for i := 0; i < n; i++ { // 使用 for 结构创建一个循环,循环 n 次
        c <- x // 向 channel c 中发送 x 的值
        x, y = y, x+y // 更新 x 和 y 的值,为斐波那契数列(Fibonacci sequence)中的下一个数
    }
    close(c) // 关闭 channel c,表示不再发送数据
}

func main() { // 定义一个主函数
    c := make(chan int, 10) // 使用 make 函数创建一个容量为 10 的整型 channel c
    go fibonacci(cap(c), c) // 使用 go 关键字创建一个新的 goroutine,并调用 fibonacci 函数,传入 channel c 的容量和 channel c 作为参数
    for i := range c { // 使用 for-range 结构遍历 channel c 中的数据,直到 channel c 被关闭
        fmt.Println(i) // 打印 i 的值
    }
}