96、Go语言深入解析与实践

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 初始化

如果一个包有任何导入,那么所有导入的包,无论是直接还是间接,都会首先被初始化,然后才是包本身的初始化。在一个包中,通过迭代过程初始化一个或多个源代码文件中的所有包级变量。初始化过程如下:

  1. 按声明顺序选择一个变量。
  2. 对于给定的变量,如果该变量没有依赖于未初始化变量的依赖,那么它将被初始化,否则,它将被跳过。
  3. 如果在此迭代中初始化了任何新变量,那么它将进入下一次迭代。
  4. 否则,过程终止。
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 主要表达式

主要表达式是可以

内容概要:文章以“智能网页数据标注工具”为例,深入探讨了谷歌浏览器扩展在毕业设计中的实战应用。通过开发具备实体识别、情感分类等功能的浏览器扩展,学生能够融合前端开发、自然语言处理(NLP)、本地存储推理等技术,实现高效的网页数据标注系统。文中详细解析了扩展的技术架构,涵盖Manifest V3配置、内容脚本Service Worker协作、TensorFlow.js模在浏览器端的轻量化部署推理流程,并提供了核心代码实现,包括文本选择、标注工具栏动态生成、高亮显示及模预测功能。同时展望了多模态标注、主动学习边缘计算协同等未来发展方向。; 适合人群:具备前端开发基础、熟悉JavaScript和浏览器机制,有一定AI模应用经验的计算机相关专业本科生或研究生,尤其适合将浏览器扩展人工智能结合进行毕业设计的学生。; 使用场景及目标:①掌握浏览器扩展开发全流程,理解内容脚本、Service Worker弹出页的通信机制;②实现在浏览器端运行轻量级AI模(如NER、情感分析)的技术方案;③构建可用于真实场景的数据标注工具,提升标注效率并探索主动学习、协同标注等智能化功能。; 阅读建议:建议结合代码实例搭建开发环境,逐步实现标注功能并集成本地模推理。重点关注模轻量化、内存管理DOM操作的稳定性,在实践中理解浏览器扩展的安全机制性能优化策略。
基于Gin+GORM+Casbin+Vue.js的权限管理系统是一个采用前后端分离架构的企业级权限管理解决方案,专为软件工程和计算机科学专业的毕业设计项目开发。该系统基于Go语言构建后端服务,结合Vue.js前端框架,实现了完整的权限控制和管理功能,适用于各类需要精细化权限管理的应用场景。 系统后端采用Gin作为Web框架,提供高性能的HTTP服务;使用GORM作为ORM框架,简化数据库操作;集成Casbin实现灵活的权限控制模。前端基于vue-element-admin模板开发,提供现代化的用户界面和交互体验。系统采用分层架构和模块化设计,确保代码的可维护性和可扩展性。 主要功能包括用户管理、角色管理、权限管理、菜单管理、操作日志等核心模块。用户管理模块支持用户信息的增删改查和状态管理;角色管理模块允许定义不同角色并分配相应权限;权限管理模块基于Casbin实现细粒度的访问控制;菜单管理模块动态生成前端导航菜单;操作日志模块记录系统关键操作,便于审计和追踪。 技术栈方面,后端使用Go语言开发,结合Gin、GORM、Casbin等成熟框架;前端使用Vue.js、Element UI等现代前端技术;数据库支持MySQL、PostgreSQL等主流关系数据库;采用RESTful API设计规范,确保前后端通信的标准化。系统还应用了单例模式、工厂模式、依赖注入等设计模式,提升代码质量和可测试性。 该权限管理系统适用于企业管理系统、内部办公平台、多租户SaaS应用等需要复杂权限控制的场景。作为毕业设计项目,它提供了完整的源码和论文文档,帮助学生深入理解前后端分离架构、权限控制原理、现代Web开发技术等关键知识点。系统设计规范,代码结构清晰,注释完整,非常适合作为计算机相关专业的毕业设计参考或实际项目开发的基础框架。 资源包含完整的系统源码、数据库设计文档、部署说明和毕
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值