Go语言深入解析与实践
1. 引言
Go语言是一种由谷歌创建的通用编程语言,大约在10年前开源。它最初被设计为一种“低位”系统编程语言,现在广泛应用于许多不同的系统和应用领域,包括Web编程。Go语言具有垃圾回收功能,这使得各种技能水平的开发人员更容易使用,并有助于减少许多与内存相关的问题。Go语言在语言层面原生支持并发编程,这使得它在现代多核处理器环境中表现出色。此外,Go语言通过内置的goroutine等独特功能与其他现代语言区分开来。
2. Go语言的特点
2.1 极简主义
Go语言是一种“极简主义”的语言,类似于Lua。这种极简主义不仅仅体现在语法上,更体现在语言设计的核心理念中。Go语言避免了过多的语言特性,专注于提供一个简洁、高效且易于理解的编程环境。这使得Go语言非常适合构建大规模系统,同时也降低了学习曲线。
2.2 稳定性
Go语言将语言的稳定性置于其他任何事情之上。这与许多其他编程语言形成鲜明对比,后者实际上是在进行“军备竞赛”,以增加越来越多的功能。Go语言的变化非常小,自四十年前创建以来,它一直保持稳定。这使得开发者可以放心地长期使用Go语言,而不必担心频繁的语法或特性变更。
2.3 包含电池
Go语言是一种“包含电池”的编程语言。这意味着Go语言自带了大量的标准库,这些库涵盖了从网络编程到文件处理等各种常用功能。这使得开发者可以快速上手,而无需过多依赖外部库。
3. Go语言的基本结构
3.1 包
Go语言中的包是程序的基本组织单位。一个Go程序由一个或多个包构成,每个包由一个或多个源代码文件构成。包通过包名来区分,并且包名必须是一个有效的非空标识符。包名通常与源文件所在的目录名相同,例如,源文件位于
src/image/color
目录下,包名应为
color
。
3.1.1 源文件组织
每个Go语言包的源文件由以下三个部分组成,顺序如下:
1. 定义它所属的包的包声明。
2. 一组声明导入的包的导入声明,如果有。
3. 一组(可能为空的)顶层常量、类型、变量、函数和方法的声明。
3.1.2 包条款
包条款是源文件中的第一个非空行,它声明了文件所属的包名。包条款以关键字
package
开始,后跟包名标识符。例如:
package main
3.2 程序初始化和执行
Go程序的执行从
main
包中的
main
函数开始。
main
函数不接受任何参数并且不返回任何值,这与C语言或其他类C语言不同。Go程序通过调用
main
函数启动,
main
函数可能会调用其他函数,这些函数又可能会调用其他函数,以此类推。当
main
函数最终返回时,程序退出。
3.2.1 初始化
如果一个包有任何导入,那么所有导入的包,无论是直接还是间接,都会首先被初始化,然后才是包本身的初始化。在一个包中,通过迭代过程初始化一个或多个源代码文件中的所有包级变量。初始化过程如下:
- 按声明顺序选择一个变量。
- 对于给定的变量,如果该变量没有依赖于未初始化变量的依赖,那么它将被初始化,否则,它将被跳过。
- 如果在此迭代中初始化了任何新变量,那么它将进入下一次迭代。
- 否则,过程终止。
3.2.2 初始化函数
一个包可以有一个或多个
init
函数,它不接受任何参数并且不返回任何值。
init
函数用于包作用域变量的初始化,特别是在那些不能用简单声明表达的初始化。
init
函数的另一个常见用途是在实际执行开始之前验证或修复程序状态的正确性。
4. Go模块与工作区
Go现在支持两种方式来组织和管理相关包:Go模块和Go工作区。模块和工作区不是Go语言规范的一部分,而是由当前的“标准”Go工具链指定的。
4.1 Go模块
一个模块是相关Go包的集合,这些Go包存储在一个文件树中,其根目录包含一个
go.mod
文件。Go模块是源代码共享和版本控制的单元,以及依赖管理。
4.1.1 go.mod文件
go.mod
文件定义了模块的模块路径,这也是用于根目录的导入路径,以及其依赖要求。每个依赖要求都写为模块路径加上一个特定的语义版本。
| 动词 | 描述 |
|---|---|
| module | 定义了模块路径。 |
| go | 设置预期的语言版本。 |
| require | 需要特定模块的给定版本或更高版本。 |
| exclude | 排除特定模块版本的使用。 |
| replace | 用不同的模块路径/版本替换模块路径/版本。 |
4.2 Go工作区
Go命令现在支持工作区模式。这可以通过在工作目录或父目录中放置一个
go.work
文件,或者通过设置
GOWORK
环境变量来启用。在工作区模式中,将使用
go.work
文件来确定一组一个或多个主模块,这些模块用作模块解析的根。
4.2.1 go.work文件
go.work
文件遵循与
go.mod
文件相同的句法结构。它是面向行的,每行包含一个指令,由动词后跟其参数组成。
graph TD;
A[创建Go工作区] --> B[创建`go.work`文件];
B --> C[指定模块];
C --> D[设置Go语言版本];
D --> E[使用模块];
E --> F[构建和相关操作];
5. 词法元素
Go语言支持两种类型的注释:C++-风格的行注释和C风格块注释。行注释以
//
开始,持续到行尾。代码块注释以
/*
开始并在第一个后续
*/
后停止。注释主要用于程序文档,
go doc
命令处理Go源文件以提取有关包内容的文档。
5.1 分号
Go语言的正式语法使用分号来终止语句,类似于大多数类C语言。然而,这些分号通常不会出现在Go源代码中。词法分析器自动添加分号;在一行的末尾,如果最后一个标记是以下之一:标识符、整型、浮点数类型、虚数、字符、字符串字面量、关键字
break
、
continue
、
fallthrough
、
return
、运算符和标点符号
++
、
--
、
)
、
]
或
}
。
5.2 标识符
标识符是程序实体(如变量和类型)的名称。一个标识符由一个或多个字母和数字组成,其首字母必须是字母。
5.3 关键字
Go语言中的一些关键字包括但不限于:
break
、
default
、
func
、
interface
、
select
、
case
、
defer
、
go
、
map
、
struct
、
chan
、
else
、
goto
、
package
、
switch
、
const
、
fallthrough
、
if
、
range
、
type
、
continue
、
for
、
import
、
return
、
var
。
5.4 运算符和标点符号
Go语言支持多种运算符和标点符号,包括但不限于:
&
、
+=
、
&=
、
&&
、
==
、
!=
、
\|
、
-=
、
\|=
、
\|\|
、
<
、
<=
、
^
、
\*=
、
^=
、
<->
、
>=
、
<<
、
/=
、
<<=
、
++
、
=
、
%
、
>>
、
%=
、
>>=
、
--
、
!
、
^
、
&^
。
6. 声明和作用域
Go语言使用(显式或隐式)代码块进行词法作用域划分。标识符的作用域是源文本的范围,在这个范围内标识符表示声明的常量、变量、类型、函数、方法、标签或导入的包。
6.1 声明语句
声明将一个(非空白)标识符绑定到一个常量、变量、类型、函数、方法、标签或导入的包。在Go程序中,每个非空白标识符在使用前必须声明。在同一个代码块中,或者在文件和包的代码块之间,不能声明相同的标识符两次。
6.2 顶层声明
一个Go包主要包含一些顶层声明(除了每个源文件需要的包条款和导入语句,如果有的话)。以下是在Go中被认为是顶层声明的:
- 常量声明
- 变量声明
- 类型/接口声明
- 函数声明
- 方法声明
6.3 代码块
代码块是零个、一个或多个语句的序列。代码块可以嵌套,并且它们影响作用域。语句可以使用一对花括号显式地组合成一个代码块。Go语言语法将以下内容视为不需要大括号的隐式代码块:
- “宇宙代码块”包括一个程序中所有包的所有源代码。
- 包代码块包含每个包所有Go源文本。
- 文件代码块包含给定源代码文件中所有Go源文本。
- 每个
if
、
for
和
switch
语句。
- 每个
case
/
默认
子句在一个
switch
或
select
语句中。
7. 常量
常量在Go语言中是在编译时创建的,因此它们应该用常量表达式定义,这些表达式可以被编译器评估。常量在声明时不能没有它们的初始值。只有以下内置类型(以及用这些类型定义的类型)可以用于常量:布尔型、数字(整型和浮点数类型)、符文和字符串。
7.1 常量声明
常量声明通过将一个或多个标识符列表与相应的值列表绑定,使用常量表达式创建常量。例如:
const (
KlingonPi, RomulanPi = 31.4, 314.2
)
7.2 iota
在每个常量声明中,预声明的标识符
iota
代表连续的未类型整数常量,从0开始在第一个标识符-表达式列表中。
iota
通常用于构造一组相关的常量(类似于其他编程语言中的枚举)。
const (
r, g, b = iota, iota + 10, iota + 20
y = iota
_ = iota
k = iota
)
8. 变量
变量是内存中用于存储值的位置。给定变量的允许值集合由其类型决定。当一个变量在表达式中被引用时,它的值将被返回。变量的值是最近一次分配给该变量的值。如果一个变量尚未被明确赋予一个值,那么它的值就是其类型的“零值”。
8.1 变量声明
变量声明将一组标识符绑定到相应的一组表达式的值,并为每个标识符指定一种类型,或静态类型,以及一个(显式或隐式)初始值。
var numGames int32
var numWins, numLosses = 0, 10
8.2 简短变量声明
短变量声明是对带有初始化表达式但没有显式类型说明的(非括号化的)变量声明的简写形式。短变量声明使用
:=
操作符,而不是常规的赋值操作符。它们只能在函数块中,或在函数块中的局部块内使用,以声明局部变量。
func noOp() {
a0, a1 := 0.0, 1.0
b0, b1 := 0.0, 1.0
print(a0, a1, b0, b1)
}
8.3 变量重声明
在Go语言中,标识符通常不能在同一代码块内重新声明。然而,当使用简短的多变量声明语法时,只要变量声明符合以下条件,变量就可以重新声明:
- 该变量在同一个代码块中之前已经用相同类型声明过,并且短变量声明语句至少包含了一个新的非空白标识符。
func doSomething() {
var c0 int = 10
print(c0)
c0, c1 := 20, 40
print(c0, c1)
}
9. 类型
类型本质上定义了一组所有可能的值,并且允许对这些值进行的操作。Go语言包含了许多预声明的命名类型,如布尔型、整型、浮点数类型等。此外,还可以创建新的类型,例如使用复合类型字面量:数组、结构体、指针、函数、接口、切片、映射和通道。
9.1 类型声明
类型声明(使用关键字
type
)将一组标识符(类型名称)绑定到一组类型。有两种类型声明:别名声明和类型定义。
9.1.1 别名声明
类型别名声明使用赋值似的语法,在类型关键字之后,将标识符绑定到给定(已存在的)类型。
type (
Rank = uint8
Suit = rune
)
9.1.2 类型定义
类型定义创建了一个新的、具有相同底层类型和操作的独立命名类型。类型定义中的标识符作为新类型的名称。
type Rank uint8
10. 接口
接口定义了一个类型,或者更广义地说,定义了一组类型(“类型集”)。一个基本接口类型的变量在运行时可以用于任何类型(称为“动态类型”),该类型属于声明的接口类型集(称为“静态类型”)。
10.1 接口类型
接口类型由关键字
interface
指定,后跟零个、一个或多个接口元素,这些元素被一对花括号包围。每个接口元素可以是方法规范、非接口类型或底层类型,或者两个或更多非接口类型或底层类型的联合。
type Mover interface {
Move() bool
}
10.2 类型集
接口或非接口类型的类型集确定如下:
- 空接口的类型集是所有非接口类型的集合。
- 一个非空接口类型的类型集是其接口元素的所有类型集的交集。
- 方法规范的类型集是所有方法集包含该方法的所有类型的集合。
- 非接口类型的类型集仅由该类型组成。
- 形如
{T}
的项,其中T是非接口类型,其类型集是其底层类型为T的所有类型的集合。
- 两个或更多项的联合,通过
|
分隔(例如,
t1 | t2 | ... | tn
),其类型集是这些项的类型集的联合。
11. 函数
函数是Go语言中最重要的构造之一。Go语言的函数与C语言的函数相当相似,但有一些小的差异。特别是,Go语言的函数可以返回零个、一个或多个值。
11.1 函数类型
函数签名是函数的参数类型列表和结果类型列表。函数类型表示所有具有相同函数签名的函数和方法的集合。未初始化的函数类型变量的值是
nil
。
func (value int, flag bool) int
11.2 函数声明
函数声明将一个标识符,即函数名,绑定到一个函数上。函数体在语法上是一个代码块。
func first(fst, snd int) int {
return fst
}
11.3 泛型函数
如果函数声明指定了类型参数,函数名表示一个泛型函数。泛型函数定义了一组由类型参数化的函数(或函数模板),并且它们必须在使用时实例化。
func Min[T Ordered](x, y T) T {
if x < y {
return x
}
return y
}
12. 方法
方法是一个带有接收者的函数。方法声明将一个标识符、一个方法名绑定到一个方法,并将该方法与接收者的基类型关联起来。
type Point struct {
X, Y float32
}
func (p Point) Dist() float32 {
if p >= 0 {
return p
}
return -p
}
12.1 方法声明
方法声明将一个标识符、一个方法名绑定到一个方法,并将该方法与接收者的基类型关联起来。这是方法声明的语法:
func (receiver parameter) methodName(parameterList) result functionBodyBlock
参数应该是以下两种形式之一:
-
name T
或
name *T
,对于一个非指针,非接口类型T。名称可以是空白标识符(
_
),或者如果接收者在方法体内部没有被引用,名称可以完全省略。
13. 表达式
表达式通过对其操作数应用函数或其他运算符来计算并返回一个值。
13.1 操作数
操作数表示表达式中的基本值。操作数可以是:
- 一个字面量
- 一个非空白标识符,表示常量、变量或函数
- 一个括号表达式
13.2 可寻址表达式
以下表达式被认为是“可寻址的”:
- 一个变量
- 指针间接寻址
- 切片索引操作
- 一个可寻址的结构体操作数的字段选择器
- 一个可寻址数组的数组索引操作
13.3 主要表达式
主要表达式是可以作为一元和二元运算符操作数使用的最简单的表达式。以下是主要表达式的语法:
- 类型转换表达式
- 方法表达式
- 选择器
- 索引
- 切片
- 类型断言
- 函数参数
13.4 常量表达式
常量表达式在编译时进行计算,并且只能包含常量操作数。常量可以是未指定类型:
- 未指定类型的布尔常量可以在布尔值可以使用的地方使用。
- 未指定类型的数值常量可以在整型或浮点数类型值可以使用的地方使用。
- 未指定类型的字符串常量可以在字符串可以使用的地方使用。
const (
Pi = 3.14
E = 2.718
)
13.5 复合字面量
复合字面量用于构造新的数组、切片、映射和结构体的值。每种字面量由相应的类型名称组成,后跟由一对匹配的大括号括起来的给定类型的元素列表。
type Location struct {
Lat, Lon float32
}
var loc = Location{37.7, -122.4}
14. 语句
语句控制程序的执行。Go语言中大约有20种不同类型的语句。以下被归类为简单语句:
- 空语句
- 简短变量声明
- 赋值语句
- 增减语句
- 表达式语句
- 发送语句
此外,还有13种不同的语句:
- 声明语句
- 标记语句
-
if
语句
-
for
语句
-
switch
语句
-
select
语句
-
fallthrough
语句
-
continue
语句
-
break
语句
-
jump
语句
-
defer
语句
-
return
语句
-
go
语句
此外,一个代码块在语法上是一个语句。也就是说,一个代码块可以在预期语句的地方使用。
14.1 空语句
空语句什么也不做。
package main
func main() {
; // 空语句
;;
}
14.2 赋值语句
赋值是一个简单语句,它将左侧的每个操作数绑定到一个表达式列表,操作符,将其对应值赋给右侧的另一表达式列表。
package main
import "fmt"
func main() {
var apple, orange string
apple = "sweet"
orange = "sour"
fmt.Println(apple, orange)
}
14.3 增加-减少语句
在Go语言中,
--
不是运算符。相反,Go语言提供了增加(
++
)和减少(
--
)语句,它们可以分别用来将它们的数字操作数增加和减少未指定类型的数字常量1。
package main
func main() {
i, j := 0, 0
i++
j--
print(i, j)
}
14.4 表达式语句
以下表达式可以在语句上下文中出现,它们的值将被忽略:
- 函数调用(除了少数内置函数外)
- 方法调用
- 接收操作
h(x + y)
f.Close()
<-ch
14.5 发送语句
发送语句在给定通道上发送一个值:
ch1 := make(chan int, 10)
ch2 := make(chan int)
ch1 <- 3
ch2 <- 2 + 5
15. 错误处理
程序在执行过程中可能会在代码的各个部分产生错误。如果Go函数或方法遇到无法处理的错误或异常情况,它应该向调用者返回某种错误指示。按照惯例,Go函数通常将错误作为它们的返回值之一返回,通常是最后一个。
15.1 错误接口
Go语言包含一个预声明的错误接口类型:
type Error interface {
Error() string
}
尽管不是必须的,但通常使用这种通用接口来表示错误情况是一个好习惯。在根据这个约定,
nil
错误值代表没有错误。当返回一个非
nil
错误时,通常会忽略正常的返回值。当发生错误时,函数应该只返回正常返回类型的零值。
func Read(f *File, b []byte) (n int, err error) {
// f: 文件句柄
// b: 从文件读取的字节
// n: 读取的字节数
// err: 错误
}
15.2 运行时恐慌
执行错误,如尝试将数字除以0,或尝试索引数组超出其合法范围,会触发运行时恐慌。这相当于调用内置函数
panic
,其值为错误类型,来自运行时包。
15.2.1 内置的恐慌函数
当错误情况“严重”到程序执行无法继续时,我们可以使用内置函数
panic
。调用
panic
实际上会创建一个运行时错误,该错误将沿着调用链冒泡并终止程序(除非以某种方式处理)。
panic("严重错误")
15.2.2 内置恢复函数
当程序发生恐慌时,无论是通过运行时错误还是通过显式调用
panic
函数,Go语言会立即停止当前协程中当前函数的执行,并开始展开调用栈。在这个过程中,所有的延迟函数都会被调用。如果这个调用链中的任何一个延迟函数包含了对内置恢复函数的调用,那么它将停止展开过程,并从那个点开始恢复协程的正常执行。恢复函数将返回传递给原始恐慌的参数。
defer func() {
if r := recover(); r != nil {
fmt.Println("恢复:", r)
}
}()
16. 示例代码
作为练习,让我们尝试实现一个栈。栈是一种支持至少两种操作的容器类型:向给定容器添加一个元素的方法,通常称为
add
、
push
等,以及从容器中取出一个元素的方法,通常称为
remove
、
pop
等。
16.1 泛型的非正式介绍
泛型是静态和强类型语言的类型系统固有的,无论该语言是否正式支持(例如,Go语言1.18之前与之后)。Go语言的内置集合类型,数组、切片、映射以及通道,都是泛型类型,无论我们是否这样称呼它们。
例如,让我们考虑创建一系列整型数组类型,其大小为10、11、12,等等。
type IntArray10 [10]int
type IntArray11 [11]int
type IntArray12 [12]int
整型数组10、整型数组11以及整型数组12都是完全不同的类型,尽管它们看起来非常相似。唯一的区别是数组中元素的数量。如果需要显式创建几十个,甚至只是几个整型数组类型,这将变得非常不便。Go语言内置的数组字面量语法支持通过它们的大小创建参数化的数组类型。
type IntArray[N] [N]int
16.2 泛型栈
作为练习,让我们尝试实现一个栈。栈是一种支持至少两种操作的容器类型:向给定容器添加一个元素的方法,通常称为
add
、
push
等,以及从容器中取出一个元素的方法,通常称为
remove
、
pop
等。
16.2.1 工作区设置
首先,让我们为我们的“堆栈演示”创建一个Go模块。
$ mkdir stack-demo && cd $_
$ mkdir stack && cd $_
$ go mod init gitlab.com/.../stack-demo/stack
$ cd ..
$ go mod init gitlab.com/.../stack-demo
$ go mod edit -require gitlab.com/.../stack-demo/stack@v0.1.0
$ go mod edit -replace gitlab.com/.../stack-demo/stack=./stack
$ go work init .
$ go work use stack
$ go work use .
16.2.2 堆栈库
现在我们已经完成了“行政”任务,让我们开始堆栈库的工作。首先,让我们定义一个堆栈类型。
package stack
type Pusher[E any] interface {
Push(item E)
}
type Popper[E any] interface {
PopOrError() (E, error)
}
type Stack[E any] interface {
Pusher[E]
Popper[E]
}
现在让我们实现一个链表。
package stack
type Node[E any] struct {
item E
next *Node[E]
}
type List[E any] struct {
head *Node[E]
}
func newList[E any]() *List[E] {
return &List[E]{head: nil}
}
func (l *List[E]) addToHead(n *Node[E]) {
n.next = l.head
l.head = n
}
func (l *List[E]) removeHead() *Node[E] {
n := l.head
if n == nil {
return nil
}
l.head = l.head.next
return n
}
16.2.3 驱动程序
最后,这里是一个简单的测试用的主函数:
package main
import (
"fmt"
"gitlab.com/.../stack-demo/stack"
)
func main() {
lStack := stack.New[int]()
lStack.Push(1)
lStack.Push(2)
lStack.Push(3)
fmt.Printf("原始栈=%s\n", lStack)
for {
if item, err := lStack.PopOrError(); err == nil {
fmt.Printf("弹出项=%v\n", item)
fmt.Printf("当前栈=%s\n", lStack)
} else {
break
}
}
}
17. 总结
Go语言以其简洁、高效的特性赢得了广泛的开发者喜爱。通过理解Go语言的基本结构和特性,我们可以更好地利用它来构建高效、可靠的软件系统。Go语言的极简主义设计哲学和强大的并发支持使得它在现代多核处理器环境中表现出色。通过掌握Go语言的类型系统、错误处理、泛型和并发模型,我们可以编写出更加健壮和高效的代码。
(上半部分结束)
(下半部分开始)
18. 表达式和语句
表达式通过对其操作数应用函数或其他运算符来计算并返回一个值。语句控制程序的执行。Go语言中大约有20种不同类型的语句。以下是几种常见的语句类型。
18.1 if语句
if
语句包含一个或多个基于
if
表达式的执行分支。
if
表达式可以可选地加上一个简单语句:
if x <= 10 {
fmt.Println("x is small")
} else if x == max {
fmt.Println("x is perfect")
} else {
fmt.Println("x is large")
}
18.2 for语句
for
语句用于重复执行一段代码块。
for
语句可以基于迭代控制方式的不同被分类为四种不同的类别:
- 无限
for
循环
- 带有单一条件的
for
语句
- 带有
for
子句的
for
语句
- 带有范围子句的
for
语句
18.2.1 无限
for
循环
for {
fmt.Println("我没有做这件事.")
}
18.2.2 带有单一条件的
for
语句
for i < 10 {
sum += i
i++
fmt.Println("总和 =", sum)
}
18.2.3 带有
for
子句的
for
语句
for i := 0; i < 10; i++ {
sum += i
}
18.2.4 带有范围子句的
for
语句
arr := []int{1, 3, 5}
length := 0
for range arr {
length++
}
fmt.Println("length", length)
sum := 0
for _, e := range arr {
sum += e
}
fmt.Println("sum", sum)
idx, max := -1, 0
for i, e := range arr {
if max < e {
idx, max = i, e
}
}
if idx != -1 {
fmt.Println("idx", idx, "max", max)
}
18.3 switch语句
switch
语句包含一个或多个基于
switch
表达式的执行分支,称为
case
。
18.3.1 表达式
switch
switch number {
case 1, 3, 5:
result = "奇数"
case 2, 4, 6:
result = "偶数"
default:
result = "未知"
}
18.3.2 类型
switch
switch x := f(); t := x.(type) {
case int:
fmt.Printf("Int x=%d\n", x)
case float64:
fmt.Printf("Float64 x=%f\n", x)
}
18.4 select语句
select
语句基于一组一个或多个(发送或接收)通道操作。它类似于
switch
语句,但是,在
select
语句中,所有
case
都涉及通信操作。
func lucasSequence(ch chan int, done chan bool) {
a, b := 2, 1
for {
select {
case ch <- a:
a, b = b, a+b
case <-done:
return
}
}
}
func main() {
ch := make(chan int)
done := make(chan bool)
go func() {
for i := 1; i <= 10; i++ {
fmt.Println(i, "->", <-ch)
}
done <- true
}()
lucasSequence(ch, done)
}
19. 并发编程
Go语言原生支持并发编程,这使得它在现代多核处理器环境中表现出色。Go语言通过
goroutine
和
channel
来实现并发编程。
19.1 goroutine
goroutine
是Go语言中的一种轻量级线程。通过
go
关键字可以启动一个新的
goroutine
。
go func() {
fmt.Println("Hello from goroutine!")
}()
19.2 channel
channel
用于在不同的
goroutine
之间传递数据。
channel
可以是单向的(发送或接收)或双向的。
ch := make(chan int)
go func() {
ch <- 42
}()
fmt.Println(<-ch)
19.3 select语句
select
语句用于在多个
channel
操作之间进行选择。它类似于
switch
语句,但在
select
语句中,所有
case
都涉及通信操作。
select {
case v := <-ch1:
fmt.Println("Received from ch1:", v)
case v := <-ch2:
fmt.Println("Received from ch2:", v)
case ch3 <- 42:
fmt.Println("Sent to ch3")
default:
fmt.Println("No communication")
}
20. 泛型编程
Go语言从1.18版本开始支持泛型编程。泛型允许编写参数化的类型和函数,从而提高代码的复用性和灵活性。
20.1 泛型类型
泛型类型定义了一组相关(实际/具体)类型。尽管名称如此,泛型类型并不是一个“实际类型”。它类似于实际/具体类型的模板。
type ListStack[E any] struct {
list *list[E]
}
func New[E any]() *ListStack[E] {
s := ListStack[E]{list: newList[E]()}
return &s
}
func (s *ListStack[E]) Push(item E) {
n := node[E]{item: item}
s.addToHead(&n)
}
func (s *ListStack[E]) PopOrError() (E, error) {
n := s.removeHead()
if n == nil {
var e E
return e, errors.New("空列表")
}
return n.item, nil
}
20.2 泛型函数
泛型函数允许编写参数化的函数,从而提高代码的复用性和灵活性。
func Min[T Ordered](x, y T) T {
if x < y {
return x
}
return y
}
21. 示例代码
作为练习,让我们实现一个泛型排序函数,例如使用快速排序算法。
func quickSort[T Ordered](arr []T) []T {
if len(arr) < 2 {
return arr
}
pivot := arr[0]
less := quickSort(filter(arr[1:], func(x T) bool { return x < pivot }))
greater := quickSort(filter(arr[1:], func(x T) bool { return x >= pivot }))
return append(less, append([]T{pivot}, greater...)...)
}
func filter[T any](arr []T, fn func(T) bool) []T {
var result []T
for _, v := range arr {
if fn(v) {
result = append(result, v)
}
}
return result
}
通过这些示例代码,我们可以更好地理解Go语言的泛型编程和并发编程特性。Go语言的极简主义设计哲学和强大的并发支持使得它在现代多核处理器环境中表现出色。通过掌握Go语言的类型系统、错误处理、泛型和并发模型,我们可以编写出更加健壮和高效的代码。
(下半部分
22. 泛型队列
队列是一种具有先进先出(FIFO)要求的集合。与栈不同,队列在队尾添加元素,在队首移除元素。我们可以使用Go语言的泛型特性来实现一个泛型队列。
22.1 队列接口
首先,我们定义一个队列接口,它包含两个方法:
Enqueue
和
Dequeue
。
package queue
type Queue[E any] interface {
Enqueue(E)
Dequeue() (E, error)
}
22.2 队列实现
接下来,我们实现一个基于链表的队列。队列的实现包括两个主要操作:在队尾添加元素(
Enqueue
)和从队首移除元素(
Dequeue
)。
package queue
type Node[E any] struct {
item E
next *Node[E]
}
type LinkedListQueue[E any] struct {
head *Node[E]
tail *Node[E]
}
func NewLinkedListQueue[E any]() *LinkedListQueue[E] {
return &LinkedListQueue[E]{}
}
func (q *LinkedListQueue[E]) Enqueue(item E) {
newNode := &Node[E]{item: item}
if q.tail != nil {
q.tail.next = newNode
}
q.tail = newNode
if q.head == nil {
q.head = q.tail
}
}
func (q *LinkedListQueue[E]) Dequeue() (E, error) {
if q.head == nil {
var zero E
return zero, errors.New("队列为空")
}
item := q.head.item
q.head = q.head.next
if q.head == nil {
q.tail = nil
}
return item, nil
}
22.3 驱动程序
最后,我们编写一个简单的测试用的主函数,展示队列的使用。
package main
import (
"fmt"
"github.com/your-repo/queue"
)
func main() {
q := queue.NewLinkedListQueue[int]()
q.Enqueue(1)
q.Enqueue(2)
q.Enqueue(3)
for {
if item, err := q.Dequeue(); err == nil {
fmt.Printf("弹出项=%v\n", item)
} else {
fmt.Println("队列为空")
break
}
}
}
23. 泛型排序列表
排序列表是一种支持添加、删除元素,并且可以根据索引获取元素的集合。我们希望添加的元素能够自动插入到正确的位置,以保持列表的有序性。
23.1 排序列表接口
首先,我们定义一个排序列表接口,它包含三个方法:
Add
、
Remove
和
Get
。
package sortedlist
type SortedList[E any] interface {
Add(E)
Remove(int) (E, error)
Get(int) (E, error)
}
23.2 排序列表实现
接下来,我们实现一个基于二分查找的排序列表。为了简化实现,我们假设元素类型实现了
Ordered
接口。
package sortedlist
type Element[E Ordered] struct {
item E
next *Element[E]
}
type SortedListImpl[E Ordered] struct {
head *Element[E]
size int
}
func NewSortedList[E Ordered]() *SortedListImpl[E] {
return &SortedListImpl[E]{}
}
func (sl *SortedListImpl[E]) Add(item E) {
newElem := &Element[E]{item: item}
if sl.head == nil || item < sl.head.item {
newElem.next = sl.head
sl.head = newElem
sl.size++
return
}
current := sl.head
for current.next != nil && item >= current.next.item {
current = current.next
}
newElem.next = current.next
current.next = newElem
sl.size++
}
func (sl *SortedListImpl[E]) Remove(index int) (E, error) {
if index < 0 || index >= sl.size {
var zero E
return zero, errors.New("索引超出范围")
}
if index == 0 {
item := sl.head.item
sl.head = sl.head.next
sl.size--
return item, nil
}
prev := sl.head
for i := 0; i < index-1; i++ {
prev = prev.next
}
item := prev.next.item
prev.next = prev.next.next
sl.size--
return item, nil
}
func (sl *SortedListImpl[E]) Get(index int) (E, error) {
if index < 0 || index >= sl.size {
var zero E
return zero, errors.New("索引超出范围")
}
current := sl.head
for i := 0; i < index; i++ {
current = current.next
}
return current.item, nil
}
23.3 驱动程序
最后,我们编写一个简单的测试用的主函数,展示排序列表的使用。
package main
import (
"fmt"
"github.com/your-repo/sortedlist"
)
func main() {
sl := sortedlist.NewSortedList[int]()
sl.Add(3)
sl.Add(1)
sl.Add(2)
for i := 0; i < sl.size; i++ {
if item, err := sl.Get(i); err == nil {
fmt.Printf("索引=%d, 元素=%v\n", i, item)
}
}
if item, err := sl.Remove(1); err == nil {
fmt.Printf("移除元素=%v\n", item)
}
for i := 0; i < sl.size; i++ {
if item, err := sl.Get(i); err == nil {
fmt.Printf("索引=%d, 元素=%v\n", i, item)
}
}
}
24. 泛型二叉树
二叉树是一种数据结构,其中每个节点最多有两个子节点。我们可以使用Go语言的泛型特性来实现一个泛型二叉树。
24.1 二叉树接口
首先,我们定义一个二叉树接口,它包含三个方法:
Insert
、
Delete
和
Search
。
package binarytree
type BinaryTree[E any] interface {
Insert(E)
Delete(E) bool
Search(E) bool
}
24.2 二叉树实现
接下来,我们实现一个基于二叉搜索树的二叉树。为了简化实现,我们假设元素类型实现了
Ordered
接口。
package binarytree
type TreeNode[E Ordered] struct {
item E
left *TreeNode[E]
right *TreeNode[E]
}
type BinaryTreeImpl[E Ordered] struct {
root *TreeNode[E]
}
func NewBinaryTree[E Ordered]() *BinaryTreeImpl[E] {
return &BinaryTreeImpl[E]{}
}
func (bt *BinaryTreeImpl[E]) Insert(item E) {
bt.root = bt.insert(bt.root, item)
}
func (bt *BinaryTreeImpl[E]) insert(node *TreeNode[E], item E) *TreeNode[E] {
if node == nil {
return &TreeNode[E]{item: item}
}
if item < node.item {
node.left = bt.insert(node.left, item)
} else {
node.right = bt.insert(node.right, item)
}
return node
}
func (bt *BinaryTreeImpl[E]) Delete(item E) bool {
if bt.root == nil {
return false
}
bt.root, _ = bt.delete(bt.root, item)
return true
}
func (bt *BinaryTreeImpl[E]) delete(node *TreeNode[E], item E) (*TreeNode[E], bool) {
if node == nil {
return nil, false
}
if item < node.item {
node.left, _ = bt.delete(node.left, item)
} else if item > node.item {
node.right, _ = bt.delete(node.right, item)
} else {
if node.left == nil {
return node.right, true
}
if node.right == nil {
return node.left, true
}
min := bt.findMin(node.right)
node.item = min.item
node.right, _ = bt.delete(node.right, min.item)
}
return node, true
}
func (bt *BinaryTreeImpl[E]) findMin(node *TreeNode[E]) *TreeNode[E] {
for node.left != nil {
node = node.left
}
return node
}
func (bt *BinaryTreeImpl[E]) Search(item E) bool {
return bt.search(bt.root, item)
}
func (bt *BinaryTreeImpl[E]) search(node *TreeNode[E], item E) bool {
if node == nil {
return false
}
if item < node.item {
return bt.search(node.left, item)
} else if item > node.item {
return bt.search(node.right, item)
}
return true
}
24.3 驱动程序
最后,我们编写一个简单的测试用的主函数,展示二叉树的使用。
package main
import (
"fmt"
"github.com/your-repo/binarytree"
)
func main() {
bt := binarytree.NewBinaryTree[int]()
bt.Insert(5)
bt.Insert(3)
bt.Insert(7)
bt.Insert(2)
bt.Insert(4)
bt.Insert(6)
bt.Insert(8)
fmt.Println("搜索 5:", bt.Search(5))
fmt.Println("搜索 10:", bt.Search(10))
fmt.Println("删除 5:", bt.Delete(5))
fmt.Println("搜索 5:", bt.Search(5))
}
25. 泛型多映射
多映射是一种映射/字典类型,在该类型中,可以有多个具有相同键的元素。我们可以使用Go语言的泛型特性来实现一个泛型多映射。
25.1 多映射接口
首先,我们定义一个多映射接口,它包含两个方法:
Add
和
GetAll
。
package multimap
type MultiMap[K comparable, V any] interface {
Add(K, V)
GetAll(K) []V
}
25.2 多映射实现
接下来,我们实现一个多映射。为了简化实现,我们使用一个映射,其中键映射到一个切片。
package multimap
type MultiMapImpl[K comparable, V any] struct {
data map[K][]V
}
func NewMultiMap[K comparable, V any]() *MultiMapImpl[K, V] {
return &MultiMapImpl[K, V]{data: make(map[K][]V)}
}
func (mm *MultiMapImpl[K, V]) Add(key K, value V) {
mm.data[key] = append(mm.data[key], value)
}
func (mm *MultiMapImpl[K, V]) GetAll(key K) []V {
return mm.data[key]
}
25.3 驱动程序
最后,我们编写一个简单的测试用的主函数,展示多映射的使用。
package main
import (
"fmt"
"github.com/your-repo/multimap"
)
func main() {
mm := multimap.NewMultiMap[string, int]()
mm.Add("key1", 1)
mm.Add("key1", 2)
mm.Add("key2", 3)
fmt.Println("key1:", mm.GetAll("key1"))
fmt.Println("key2:", mm.GetAll("key2"))
}
26. Go语言的并发模型
Go语言的并发模型基于
goroutine
和
channel
。
goroutine
是Go语言中的一种轻量级线程,而
channel
用于在不同的
goroutine
之间传递数据。
26.1 goroutine
goroutine
是Go语言中的一种轻量级线程。通过
go
关键字可以启动一个新的
goroutine
。
package main
import "fmt"
func say(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
26.2 channel
channel
用于在不同的
goroutine
之间传递数据。
channel
可以是单向的(发送或接收)或双向的。
package main
import "fmt"
func sum(a, b int, ch chan int) {
ch <- a + b
}
func main() {
ch := make(chan int)
go sum(3, 4, ch)
fmt.Println(<-ch)
}
26.3 select语句
select
语句用于在多个
channel
操作之间进行选择。它类似于
switch
语句,但在
select
语句中,所有
case
都涉及通信操作。
package main
import (
"fmt"
"time"
)
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go fibonacci(c, quit)
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
fmt.Println("done")
}
27. Go语言的错误处理
Go语言的错误处理机制非常简洁,通常通过返回
error
接口类型的值来处理错误。按照惯例,
nil
错误值代表没有错误。当返回一个非
nil
错误时,通常会忽略正常的返回值。当发生错误时,函数应该只返回正常返回类型的零值。
27.1 内置错误接口
Go语言包含一个预声明的错误接口类型:
type Error interface {
Error() string
}
27.2 错误处理示例
以下是一个简单的错误处理示例,展示了如何在函数中处理错误。
package main
import (
"fmt"
"os"
)
func read(filename string) ([]byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
data := make([]byte, 100)
count, err := file.Read(data)
if err != nil {
return nil, err
}
return data[:count], nil
}
func main() {
data, err := read("example.txt")
if err != nil {
fmt.Println("读取文件时出错:", err)
return
}
fmt.Println(string(data))
}
28. Go语言的模块和工作区
Go语言支持两种方式来组织和管理相关包:Go模块和Go工作区。模块和工作区不是Go语言规范的一部分,而是由当前的“标准”Go工具链指定的。
28.1 Go模块
一个模块是相关Go包的集合,这些Go包存储在一个文件树中,其根目录包含一个
go.mod
文件。Go模块是源代码共享和版本控制的单元,以及依赖管理。
28.1.1 go.mod文件
go.mod
文件定义了模块的模块路径,这也是用于根目录的导入路径,以及其依赖要求。每个依赖要求都写为模块路径加上一个特定的语义版本。
| 动词 | 描述 |
|---|---|
| module | 定义了模块路径。 |
| go | 设置预期的语言版本。 |
| require | 需要特定模块的给定版本或更高版本。 |
| exclude | 排除特定模块版本的使用。 |
| replace | 用不同的模块路径/版本替换模块路径/版本。 |
28.2 Go工作区
Go命令现在支持工作区模式。这可以通过在工作目录或父目录中放置一个
go.work
文件,或者通过设置
GOWORK
环境变量来启用。在工作区模式中,将使用
go.work
文件来确定一组一个或多个主模块,这些模块用作模块解析的根。
28.2.1 go.work文件
go.work
文件遵循与
go.mod
文件相同的句法结构。它是面向行的,每行包含一个指令,由动词后跟其参数组成。
graph TD;
A[创建Go工作区] --> B[创建`go.work`文件];
B --> C[指定模块];
C --> D[设置Go语言版本];
D --> E[使用模块];
29. Go语言的类型系统
Go语言的类型系统非常强大且灵活。它不仅支持基本类型,如布尔型、整型、浮点数类型、字符串等,还支持复合类型,如数组、切片、映射、结构体、指针、接口和通道。
29.1 类型声明
类型声明(使用关键字
type
)将一组标识符(类型名称)绑定到一组类型。有两种类型声明:别名声明和类型定义。
29.1.1 别名声明
类型别名声明使用赋值似的语法,在类型关键字之后,将标识符绑定到给定(已存在的)类型。
type (
Rank = uint8
Suit = rune
)
29.1.2 类型定义
类型定义创建了一个新的、具有相同底层类型和操作的独立命名类型。类型定义中的标识符作为新类型的名称。
type Rank uint8
29.2 类型参数列表
Go允许创建泛型类型,泛型函数,具有类型参数。泛型类型也可以用于方法声明的接收者规范。
type List[T any] struct {
items []T
}
func (l *List[T]) Append(item T) {
l.items = append(l.items, item)
}
30. Go语言的接口
接口定义了一个类型,或者更广义地说,定义了一组类型(“类型集”)。一个基本接口类型的变量在运行时可以用于任何类型(称为“动态类型”),该类型属于声明的接口类型集(称为“静态类型”)。
30.1 接口类型
接口类型由关键字
interface
指定,后跟零个、一个或多个接口元素,这些元素被一对花括号包围。每个接口元素可以是方法规范、非接口类型或底层类型,或者两个或更多非接口类型或底层类型的联合。
type Mover interface {
Move() bool
}
30.2 类型集
接口或非接口类型的类型集确定如下:
- 空接口的类型集是所有非接口类型的集合。
- 一个非空接口类型的类型集是其接口元素的所有类型集的交集。
- 方法规范的类型集是所有方法集包含该方法的所有类型的集合。
- 非接口类型的类型集仅由该类型组成。
- 形如
{T}
的项,其中T是非接口类型,其类型集是其底层类型为T的所有类型的集合。
- 两个或更多项的联合,通过
|
分隔(例如,
t1 | t2 | ... | tn
),其类型集是这些项的类型集的联合。
31. Go语言的函数
函数是Go语言中最重要的构造之一。Go语言的函数与C语言的函数相当相似,但有一些小的差异。特别是,Go语言的函数可以返回零个、一个或多个值。
31.1 函数类型
函数签名是函数的参数类型列表和结果类型列表。函数类型表示所有具有相同函数签名的函数和方法的集合。未初始化的函数类型变量的值是
nil
。
func (value int, flag bool) int
31.2 函数声明
函数声明将一个标识符,即函数名,绑定到一个函数上。函数体在语法上是一个代码块。
func first(fst, snd int) int {
return fst
}
31.3 泛型函数
如果函数声明指定了类型参数,函数名表示一个泛型函数。泛型函数定义了一组由类型参数化的函数(或函数模板),并且它们必须在使用时实例化。
func Min[T Ordered](x, y T) T {
if x < y {
return x
}
return y
}
32. Go语言的方法
方法是一个带有接收者的函数。方法声明将一个标识符、一个方法名绑定到一个方法,并将该方法与接收者的基类型关联起来。
32.1 方法声明
方法声明将一个标识符、一个方法名绑定到一个方法,并将该方法与接收者的基类型关联起来。这是方法声明的语法:
func (receiver parameter) methodName(parameterList) result functionBodyBlock
参数应该是以下两种形式之一:
-
name T
或
name *T
,对于一个非指针,非接口类型T。名称可以是空白标识符(
_
),或者如果接收者在方法体内部没有被引用,名称可以完全省略。
type Point struct {
X, Y float32
}
func (p Point) Dist() float32 {
if p >= 0 {
return p
}
return -p
}
33. Go语言的表达式
表达式通过对其操作数应用函数或其他运算符来计算并返回一个值。
33.1 操作数
操作数表示表达式中的基本值。操作数可以是:
- 一个字面量
- 一个非空白标识符,表示常量、变量或函数
- 一个括号表达式
33.2 可寻址表达式
以下表达式被认为是“可寻址的”:
- 一个变量
- 指针间接寻址
- 切片索引操作
- 一个可寻址的结构体操作数的字段选择器
- 一个可寻址数组的数组索引操作
33.3 主要表达式
主要表达式是可以作为一元和二元运算符操作数使用的最简单的表达式。以下是主要表达式的语法:
- 类型转换表达式
- 方法表达式
- 选择器
- 索引
- 切片
- 类型断言
- 函数参数
33.4 常量表达式
常量表达式在编译时进行计算,并且只能包含常量操作数。常量可以是未指定类型:
- 未指定类型的布尔常量可以在布尔值可以使用的地方使用。
- 未指定类型的数值常量可以在整型或浮点数类型值可以使用的地方使用。
- 未指定类型的字符串常量可以在字符串可以使用的地方使用。
const (
Pi = 3.14
E = 2.718
)
33.5 复合字面量
复合字面量用于构造新的数组、切片、映射和结构体的值。每种字面量由相应的类型名称组成,后跟由一对匹配的大括号括起来的给定类型的元素列表。
type Location struct {
Lat, Lon float32
}
var loc = Location{37.7, -122.4}
34. Go语言的语句
语句控制程序的执行。Go语言中大约有20种不同类型的语句。以下被归类为简单语句:
- 空语句
- 简短变量声明
- 赋值语句
- 增减语句
- 表达式语句
- 发送语句
此外,还有13种不同的语句:
- 声明语句
- 标记语句
-
if
语句
-
for
语句
-
switch
语句
-
select
语句
-
fallthrough
语句
-
continue
语句
-
break
语句
-
jump
语句
-
defer
语句
-
return
语句
-
go
语句
此外,一个代码块在语法上是一个语句。也就是说,一个代码块可以在预期语句的地方使用。
34.1 空语句
空语句什么也不做。
package main
func main() {
; // 空语句
;;
}
34.2 赋值语句
赋值是一个简单语句,它将左侧的每个操作数绑定到一个表达式列表,操作符,将其对应值赋给右侧的另一表达式列表。
package main
import "fmt"
func main() {
var apple, orange string
apple = "sweet"
orange = "sour"
fmt.Println(apple, orange)
}
34.3 增加-减少语句
在Go语言中,
--
不是运算符。相反,Go语言提供了增加(
++
)和减少(
--
)语句,它们可以分别用来将它们的数字操作数增加和减少未指定类型的数字常量1。
package main
func main() {
i, j := 0, 0
i++
j--
print(i, j)
}
34.4 表达式语句
以下表达式可以在语句上下文中出现,它们的值将被忽略:
- 函数调用(除了少数内置函数外)
- 方法调用
- 接收操作
h(x + y)
f.Close()
<-ch
34.5 发送语句
发送语句在给定通道上发送一个值:
ch1 := make(chan int, 10)
ch2 := make(chan int)
ch1 <- 3
ch2 <- 2 + 5
35. Go语言的错误处理
程序在执行过程中可能会在代码的各个部分产生错误。如果Go函数或方法遇到无法处理的错误或异常情况,它应该向调用者返回某种错误指示。按照惯例,Go函数通常将错误作为它们的返回值之一返回,通常是最后一个。
35.1 错误接口
Go语言包含一个预声明的错误接口类型:
type Error interface {
Error() string
}
尽管不是必须的,但通常使用这种通用接口来表示错误情况是一个好习惯。在根据这个约定,
nil
错误值代表没有错误。当返回一个非
nil
错误时,通常会忽略正常的返回值。当发生错误时,函数应该只返回正常返回类型的零值。
func Read(f *File, b []byte) (n int, err error) {
// f: 文件句柄
// b: 从文件读取的字节
// n: 读取的字节数
// err: 错误
}
35.2 运行时恐慌
执行错误,如尝试将数字除以0,或尝试索引数组超出其合法范围,会触发运行时恐慌。这相当于调用内置函数
panic
,其值为错误类型,来自运行时包。
35.2.1 内置的恐慌函数
当错误情况“严重”到程序执行无法继续时,我们可以使用内置函数
panic
。调用
panic
实际上会创建一个运行时错误,该错误将沿着调用链冒泡并终止程序(除非以某种方式处理)。
panic("严重错误")
35.2.2 内置恢复函数
当程序发生恐慌时,无论是通过运行时错误还是通过显式调用
panic
函数,Go语言会立即停止当前协程中当前函数的执行,并开始展开调用栈。在这个过程中,所有的延迟函数都会被调用。如果这个调用链中的任何一个延迟函数包含了对内置恢复函数的调用,那么它将停止展开过程,并从那个点开始恢复协程的正常执行。恢复函数将返回传递给原始恐慌的参数。
defer func() {
if r := recover(); r != nil {
fmt.Println("恢复:", r)
}
}()
36. Go语言的泛型编程
Go语言从1.18版本开始支持泛型编程。泛型允许编写参数化的类型和函数,从而提高代码的复用性和灵活性。
36.1 泛型类型
泛型类型定义了一组相关(实际/具体)类型。尽管名称如此,泛型类型并不是一个“实际类型”。它类似于实际/具体类型的模板。
type List[T any] struct {
items []T
}
func (l *List[T]) Append(item T) {
l.items = append(l.items, item)
}
36.2 泛型函数
泛型函数允许编写参数化的函数,从而提高代码的复用性和灵活性。
func Min[T Ordered](x, y T) T {
if x < y {
return x
}
return y
}
37. Go语言的并发编程
Go语言原生支持并发编程,这使得它在现代多核处理器环境中表现出色。Go语言通过
goroutine
和
channel
来实现并发编程。
37.1 goroutine
goroutine
是Go语言中的一种轻量级线程。通过
go
关键字可以启动一个新的
goroutine
。
go func() {
fmt.Println("Hello from goroutine!")
}()
37.2 channel
channel
用于在不同的
goroutine
之间传递数据。
channel
可以是单向的(发送或接收)或双向的。
ch := make(chan int)
go func() {
ch <- 42
}()
fmt.Println(<-ch)
37.3 select语句
select
语句用于在多个
channel
操作之间进行选择。它类似于
switch
语句,但在
select
语句中,所有
case
都涉及通信操作。
func lucasSequence(ch chan int, done chan bool) {
a, b := 2, 1
for {
select {
case ch <- a:
a, b = b, a+b
case <-done:
return
}
}
}
func main() {
ch := make(chan int)
done := make(chan bool)
go func() {
for i := 1; i <= 10; i++ {
fmt.Println(i, "->", <-ch)
}
done <- true
}()
lucasSequence(ch, done)
}
38. Go语言的类型系统
Go语言的类型系统非常强大且灵活。它不仅支持基本类型,如布尔型、整型、浮点数类型、字符串等,还支持复合类型,如数组、切片、映射、结构体、指针、接口和通道。
38.1 类型声明
类型声明(使用关键字
type
)将一组标识符(类型名称)绑定到一组类型。有两种类型声明:别名声明和类型定义。
38.1.1 别名声明
类型别名声明使用赋值似的语法,在类型关键字之后,将标识符绑定到给定(已存在的)类型。
type (
Rank = uint8
Suit = rune
)
38.1.2 类型定义
类型定义创建了一个新的、具有相同底层类型和操作的独立命名类型。类型定义中的标识符作为新类型的名称。
type Rank uint8
38.2 类型参数列表
Go允许创建泛型类型,泛型函数,具有类型参数。泛型类型也可以用于方法声明的接收者规范。
type List[T any] struct {
items []T
}
func (l *List[T]) Append(item T) {
l.items = append(l.items, item)
}
39. Go语言的接口
接口定义了一个类型,或者更广义地说,定义了一组类型(“类型集”)。一个基本接口类型的变量在运行时可以用于任何类型(称为“动态类型”),该类型属于声明的接口类型集(称为“静态类型”)。
39.1 接口类型
接口类型由关键字
interface
指定,后跟零个、一个或多个接口元素,这些元素被一对花括号包围。每个接口元素可以是方法规范、非接口类型或底层类型,或者两个或更多非接口类型或底层类型的联合。
type Mover interface {
Move() bool
}
39.2 类型集
接口或非接口类型的类型集确定如下:
- 空接口的类型集是所有非接口类型的集合。
- 一个非空接口类型的类型集是其接口元素的所有类型集的交集。
- 方法规范的类型集是所有方法集包含该方法的所有类型的集合。
- 非接口类型的类型集仅由该类型组成。
- 形如
{T}
的项,其中T是非接口类型,其类型集是其底层类型为T的所有类型的集合。
- 两个或更多项的联合,通过
|
分隔(例如,
t1 | t2 | ... | tn
),其类型集是这些项的类型集的联合。
40. Go语言的函数
函数是Go语言中最重要的构造之一。Go语言的函数与C语言的函数相当相似,但有一些小的差异。特别是,Go语言的函数可以返回零个、一个或多个值。
40.1 函数类型
函数签名是函数的参数类型列表和结果类型列表。函数类型表示所有具有相同函数签名的函数和方法的集合。未初始化的函数类型变量的值是
nil
。
func (value int, flag bool) int
40.2 函数声明
函数声明将一个标识符,即函数名,绑定到一个函数上。函数体在语法上是一个代码块。
func first(fst, snd int) int {
return fst
}
40.3 泛型函数
如果函数声明指定了类型参数,函数名表示一个泛型函数。泛型函数定义了一组由类型参数化的函数(或函数模板),并且它们必须在使用时实例化。
func Min[T Ordered](x, y T) T {
if x < y {
return x
}
return y
}
41. Go语言的方法
方法是一个带有接收者的函数。方法声明将一个标识符、一个方法名绑定到一个方法,并将该方法与接收者的基类型关联起来。
41.1 方法声明
方法声明将一个标识符、一个方法名绑定到一个方法,并将该方法与接收者的基类型关联起来。这是方法声明的语法:
func (receiver parameter) methodName(parameterList) result functionBodyBlock
参数应该是以下两种形式之一:
-
name T
或
name *T
,对于一个非指针,非接口类型T。名称可以是空白标识符(
_
),或者如果接收者在方法体内部没有被引用,名称可以完全省略。
type Point struct {
X, Y float32
}
func (p Point) Dist() float32 {
if p >= 0 {
return p
}
return -p
}
42. Go语言的表达式
表达式通过对其操作数应用函数或其他运算符来计算并返回一个值。
42.1 操作数
操作数表示表达式中的基本值。操作数可以是:
- 一个字面量
- 一个非空白标识符,表示常量、变量或函数
- 一个括号表达式
42.2 可寻址表达式
以下表达式被认为是“可寻址的”:
- 一个变量
- 指针间接寻址
- 切片索引操作
- 一个可寻址的结构体操作数的字段选择器
- 一个可寻址数组的数组索引操作
42.3 主要表达式
主要表达式是可以
超级会员免费看
991

被折叠的 条评论
为什么被折叠?



