Go语言中的接口与泛型详解
1. Go语言简介
Go语言是由谷歌创建的一种通用编程语言,大约在10年前开源。它最初被设计为一种“低位”系统编程语言,现在广泛应用于许多不同的系统和应用领域,包括Web编程。Go语言是一种强类型语言,适合构建大规模系统。它具有垃圾回收功能,这使得各种技能水平的开发人员更容易使用,并有助于减少许多与内存相关的问题。Go语言在语言层面原生支持并发编程,这使得它在处理并发任务时表现出色。
2. Go语言的极简主义设计
Go语言的设计理念是极简主义。它让人联想到像Lua这样的简单语言,强调简洁性和高效性。Go语言的极简主义体现在以下几个方面:
- 简洁的语法 :Go语言的语法非常简洁,易于学习和使用。它摒弃了许多复杂的语法特性,专注于核心功能。
- 稳定的语言特性 :Go语言将语言的稳定性置于其他任何事情之上。这与许多其他编程语言形成鲜明对比,后者实际上是在进行“军备竞赛”,以增加越来越多的功能。Go语言的变化非常少,自四十年前创建以来,变化绝对是最小的。
- 内置的标准库 :Go语言包含丰富的标准库,提供了许多常用的功能,使得开发者无需依赖外部库即可完成大部分开发任务。
3. Go语言中的接口
3.1 接口的定义
接口是Go语言中非常重要的概念。它定义了一组方法的集合,而不是具体实现。接口类型可以用于实现运行时多态性。一个(非接口)类型可以通过隐式“实现一个接口”,通过实现给定接口中声明的所有方法。使用接口类型声明的变量可以在运行时用于实现此接口的任何类型。这为Go语言提供了运行时多态性。
3.2 接口的实现
接口的实现非常灵活。只要一个类型实现了接口中声明的所有方法,它就被认为实现了该接口。Go语言中的接口实现是隐式的,不需要显式声明。例如:
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct{}
func (c Cat) Speak() string {
return "Meow!"
}
func main() {
var s Speaker
s = Dog{}
fmt.Println(s.Speak()) // 输出: Woof!
s = Cat{}
fmt.Println(s.Speak()) // 输出: Meow!
}
在这个例子中,
Dog
和
Cat
类型都实现了
Speaker
接口,因此它们可以被赋值给
Speaker
类型的变量。
3.3 接口的嵌套
接口可以嵌套其他接口,形成更复杂的接口。例如:
type Animal interface {
Eat()
Sleep()
}
type Mammal interface {
Animal
Walk()
}
type Bird interface {
Animal
Fly()
}
type Human interface {
Mammal
Laugh()
}
func main() {
var h Human
// Human接口包括Eat()、Sleep()、Walk()和Laugh()方法
}
在这个例子中,
Human
接口不仅包括
Mammal
接口中的所有方法,还包括
Animal
接口中的方法。
4. Go语言中的泛型
4.1 泛型的引入
自Go 1.18版本起,Go语言引入了泛型。泛型允许我们编写更通用的代码,适用于多种类型。泛型的引入解决了长期以来Go语言缺乏类型参数化的问题。泛型不仅可以用于类型定义,还可以用于函数和方法声明。
4.2 泛型的使用
泛型的使用非常直观。我们可以通过在类型或函数声明中引入类型参数来创建泛型类型或函数。例如:
type List[T any] struct {
items []T
}
func New[T any]() *List[T] {
return &List[T]{items: []T{}}
}
func (l *List[T]) Add(item T) {
l.items = append(l.items, item)
}
func (l *List[T]) Get(index int) T {
return l.items[index]
}
func main() {
list := New[int]()
list.Add(1)
list.Add(2)
fmt.Println(list.Get(0)) // 输出: 1
}
在这个例子中,
List
是一个泛型类型,
T
是类型参数。
New
函数创建了一个新的
List
实例,
Add
和
Get
方法可以操作
List
中的元素。
4.3 泛型的约束
Go语言中的泛型类型参数可以有约束。约束可以是任何接口类型。例如:
type Ordered interface {
~int | ~float64 | ~string
}
func Min[T Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(1, 2)) // 输出: 1
fmt.Println(Min(1.5, 2.5)) // 输出: 1.5
fmt.Println(Min("a", "b")) // 输出: a
}
在这个例子中,
Min
函数的类型参数
T
被约束为
Ordered
接口,这意味着
T
只能是整型、浮点型或字符串类型。
5. Go语言中的类型系统
5.1 类型的基本概念
Go语言的类型系统非常强大且灵活。它支持多种内置类型,如整型、浮点型、字符串、布尔型等。此外,Go语言还支持复合类型,如数组、切片、映射和通道。每种类型都有其特定的用途和操作。
5.2 类型的定义
Go语言允许通过类型定义创建新的类型。类型定义可以是别名声明或类型定义。例如:
type MyInt int
type Point struct {
X, Y float64
}
type Map[string] int
func main() {
var a MyInt = 10
p := Point{X: 1.0, Y: 2.0}
m := Map[string]int{"one": 1, "two": 2}
}
在这个例子中,
MyInt
是
int
类型的别名,
Point
是一个结构体类型,
Map[string]int
是一个映射类型。
5.3 类型的约束
Go语言中的类型约束用于限制泛型类型参数的范围。例如:
type Constraint interface {
~int | ~float64
}
func Sum[T Constraint](a, b T) T {
return a + b
}
func main() {
fmt.Println(Sum(1, 2)) // 输出: 3
fmt.Println(Sum(1.5, 2.5)) // 输出: 4
}
在这个例子中,
Sum
函数的类型参数
T
被约束为
Constraint
接口,这意味着
T
只能是整型或浮点型。
5.4 类型的底层类型
每个类型都有一个底层类型。底层类型决定了类型的内部表示和操作。例如:
type MyInt int
func main() {
var a MyInt = 10
var b int = int(a)
}
在这个例子中,
MyInt
的底层类型是
int
,因此可以将
MyInt
类型的值转换为
int
类型。
6. Go语言中的表达式
6.1 表达式的分类
Go语言中的表达式可以分为常量表达式、复合字面量、索引表达式、选择器表达式、函数调用表达式等。每种表达式都有其特定的用途和操作。
6.2 表达式的操作
表达式可以进行各种操作,如算术运算、逻辑运算、比较运算等。例如:
func main() {
a := 1 + 2
b := a * 3
c := b / 4
d := c % 5
fmt.Println(a, b, c, d) // 输出: 3 9 2 2
}
在这个例子中,
a
、
b
、
c
和
d
都是通过算术运算得到的。
6.3 表达式的优先级
Go语言中的运算符有优先级。一元运算符具有最高的优先级,其次是乘法运算符,然后是加法运算符,再然后是比较运算符,最后是逻辑运算符。例如:
func main() {
a := 1 + 2 * 3
b := (1 + 2) * 3
fmt.Println(a, b) // 输出: 7 9
}
在这个例子中,
a
的计算顺序是先乘法后加法,而
b
的计算顺序是先括号内的加法后乘法。
7. Go语言中的声明与作用域
7.1 声明语句
声明语句将一个(非空白)标识符绑定到一个常量、变量、类型、函数、方法、标签或导入的包。在Go程序中,每个非空白标识符在使用前必须声明。在同一个代码块中,或者在文件和包的代码块之间,不能声明相同的标识符两次。
7.2 顶层声明
Go包主要包含一些顶层声明(除了每个源文件需要的包条款和导入语句,如果有的话)。以下是在Go中被认为是顶层声明的:
- 常量声明
- 变量声明
- 类型/接口声明
- 函数声明
- 方法声明
7.3 代码块
代码块是零个、一个或多个语句的序列。代码块可以嵌套,并且它们影响作用域。例如:
graph TD;
A[代码块] --> B[语句1];
A --> C[语句2];
C --> D[代码块];
D --> E[语句3];
D --> F[语句4];
在这个流程图中,代码块
A
包含两个语句
B
和
C
,而语句
C
又包含一个代码块
D
,代码块
D
包含两个语句
E
和
F
。
7.4 作用域
Go语言使用(显式或隐式)代码块进行词法作用域划分。标识符的作用域是源文本的范围,在这个范围内标识符表示声明的常量、变量、类型、函数、方法、标签或导入的包。例如:
| 类型 | 作用域 |
|---|---|
| 全局代码块 | 整个程序 |
| 包代码块 | 包中的所有Go源文本 |
| 文件代码块 | 文件中的所有Go源文本 |
在这个表格中,全局代码块的作用域是整个程序,包代码块的作用域是包中的所有Go源文本,文件代码块的作用域是文件中的所有Go源文本。
8. Go语言中的错误处理
8.1 错误接口
Go语言包含一个预声明的错误接口类型:
type error interface {
Error() string
}
尽管不是必须的,但通常使用这种通用接口来表示错误情况是一个好习惯。在Go语言中,
nil
错误值代表没有错误。当返回一个非
nil
错误时,通常会忽略正常的返回值。例如:
func Read(f *File, b []byte) (n int, err error) {
// 实现省略
}
func main() {
n, err := Read(file, buffer)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(n)
}
在这个例子中,
Read
函数返回两个值:读取的字节数
n
和错误
err
。如果
err
不为
nil
,则表示出现了错误,程序会打印错误信息并返回。
8.2 运行时恐慌
执行错误,如尝试将数字除以0,或尝试索引数组超出其合法范围,会触发运行时恐慌。这相当于调用内置函数
panic
,其值为错误类型,来自运行时包。例如:
func main() {
panic("something went wrong")
}
在这个例子中,
panic
函数会创建一个运行时错误,该错误将沿着调用链冒泡并终止程序(除非以某种方式处理)。
8.3 恢复函数
当程序发生恐慌时,Go语言会立即停止当前协程中当前函数的执行,并开始展开调用栈。在这个过程中,所有的延迟函数都会被调用。如果这个调用链中的任何一个延迟函数包含了对内置恢复函数的调用,那么它将停止展开过程,并从那个点开始恢复协程的正常执行。例如:
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("something went wrong")
}
在这个例子中,
defer
语句中的匿名函数会在
panic
发生时捕获并恢复程序的执行。
9. Go语言中的函数
9.1 函数类型
函数签名是函数的参数类型列表和结果类型列表。函数类型表示所有具有相同函数签名的函数和方法的集合。未初始化的函数类型变量的值是
nil
。例如:
func add(a, b int) int {
return a + b
}
func main() {
var f func(int, int) int
f = add
fmt.Println(f(1, 2)) // 输出: 3
}
在这个例子中,
add
函数的签名是
func(int, int) int
,表示它接受两个整型参数并返回一个整型值。
9.2 可变参数函数
函数的最后一个输入参数的类型可以前缀一个标记
...
。具有此类参数的函数称为可变参数函数,它可以被调用时该参数可以传入零个或多个参数。例如:
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func main() {
fmt.Println(sum()) // 输出: 0
fmt.Println(sum(1, 2, 3)) // 输出: 6
}
在这个例子中,
sum
函数是一个可变参数函数,它可以接受零个或多个整型参数。
9.3 泛型函数
如果函数声明指定了类型参数,函数名表示一个泛型函数。泛型函数定义了一组由类型参数化的函数(或函数模板),并且它们必须在使用时实例化。例如:
func Min[T Ordered](x, y T) T {
if x < y {
return x
}
return y
}
func main() {
fmt.Println(Min(1, 2)) // 输出: 1
fmt.Println(Min(1.5, 2.5)) // 输出: 1.5
}
在这个例子中,
Min
函数是一个泛型函数,它接受两个
Ordered
类型的参数,并返回较小的那个。
10. Go语言中的方法
10.1 方法声明
方法是一个带有接收者的函数。方法声明将一个标识符、一个方法名绑定到一个方法,并将该方法与接收者的基类型关联起来。例如:
type Point struct {
X, Y float64
}
func (p Point) Distance() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
func main() {
p := Point{3, 4}
fmt.Println(p.Distance()) // 输出: 5
}
在这个例子中,
Distance
方法的接收者是
Point
类型的值。
10.2 方法的使用
方法可以在接收者类型上直接调用。例如:
type Point struct {
X, Y float64
}
func (p Point) Distance() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
func main() {
p := Point{3, 4}
fmt.Println(p.Distance()) // 输出: 5
}
在这个例子中,
p.Distance()
直接调用了
Distance
方法。
10.3 泛型方法
泛型方法允许方法的接收者类型是泛型类型。例如:
type List[T any] struct {
items []T
}
func (l *List[T]) Add(item T) {
l.items = append(l.items, item)
}
func main() {
list := &List[int]{}
list.Add(1)
list.Add(2)
fmt.Println(list.items) // 输出: [1 2]
}
在这个例子中,
List
是一个泛型类型,
Add
方法的接收者是
List
类型的指针。
11. Go语言中的表达式
11.1 操作数
操作数表示表达式中的基本值。操作数可以是字面量、非空白标识符、表示常量、变量或函数,或者是一个括号表达式。例如:
func main() {
a := 1
b := 2
c := a + b
fmt.Println(c) // 输出: 3
}
在这个例子中,
a
和
b
是操作数,
a + b
是表达式。
11.2 主要表达式
主要表达式是可以作为一元和二元运算符操作数使用的最简单的表达式。例如:
func main() {
a := 1
b := 2
c := a + b
fmt.Println(c) // 输出: 3
}
在这个例子中,
a + b
是主要表达式。
11.3 常量表达式
常量表达式在编译时进行计算,并且只能包含常量操作数。例如:
const (
Pi = 3.14
Tau = Pi * 2
)
func main() {
fmt.Println(Tau) // 输出: 6.28
}
在这个例子中,
Tau
是一个常量表达式,它在编译时计算。
11.4 转换
一个常量值
x
可以转换为类型
T
如果
x
可以由
T
类型的值表示。例如:
func main() {
x := uint(10)
y := float32(-1e-100)
fmt.Println(x, y) // 输出: 10 -1e-100
}
在这个例子中,
uint(10)
和
float32(-1e-100)
是转换表达式。
12. Go语言中的声明
12.1 简短变量声明
短变量声明是对带有初始化表达式但没有显式类型说明的(非括号化的)变量声明的简写形式。例如:
func main() {
a, b := 0.0, 1.0
fmt.Println(a, b) // 输出: 0 1
}
在这个例子中,
a
和
b
是通过短变量声明初始化的。
12.2 声明语句
声明语句将一个标识符,即函数名,绑定到一个函数上。函数体在语法上是一个代码块。例如:
func add(a, b int) int {
return a + b
}
func main() {
fmt.Println(add(1, 2)) // 输出: 3
}
在这个例子中,
add
函数的声明将
add
标识符绑定到了一个函数上。
12.3 变量重声明
在Go语言中,标识符通常不能在同一代码块内重新声明。然而,当使用简短的多变量声明语法时,只要变量声明符合以下条件,变量就可以重新声明:
- 该变量在同一个代码块中之前已经用相同类型声明过
- 短变量声明语句至少包含了一个新的非空白标识符
例如:
func doSomething() {
var c0 int = 10
fmt.Println(c0)
c0, c1 := 20, 40
fmt.Println(c0, c1)
}
在这个例子中,
c0
被重新声明了两次,首先是通过常规
var
声明,初始值为10,其次是通过简短变量声明,新的初始值为20。因为
c1
在这个语句中首次声明,所以
c0
的重新声明是允许的。
13. Go语言中的结构体
13.1 结构体的定义
结构体是有限数量的元素,称为字段的集合。每个字段都有一个名称和一个类型,在一个结构体中,它们的顺序是重要的。例如:
type Point struct {
X, Y float64
}
func main() {
p := Point{X: 1.0, Y: 2.0}
fmt.Println(p) // 输出: {1 2}
}
在这个例子中,
Point
是一个结构体类型,包含两个字段
X
和
Y
。
13.2 嵌入字段
声明了类型但未明确字段名称的字段称为嵌入字段。嵌入字段必须指定为类型名称
T
或作为指向非接口类型的指针
*T
,
T
本身不能是指针类型。未限定的类型名称作为字段名称。例如:
type Circle struct {
Point
Radius float64
}
func main() {
c := Circle{Point{1.0, 2.0}, 3.0}
fmt.Println(c) // 输出: {{1 2} 3}
}
在这个例子中,
Circle
结构体嵌入了
Point
结构体。
13.3 标签
字段声明后面可以跟一个可选的字符串字面量标签,该标签成为相应字段声明中所有字段的属性。例如:
type Point struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
func main() {
p := Point{X: 1.0, Y: 2.0}
fmt.Println(p) // 输出: {1 2}
}
在这个例子中,
X
和
Y
字段带有
json
标签。
14. Go语言中的控制结构
14.1 if语句
if
语句用于条件判断。
if
语句可以被可选地跟随
else
关键字,以及另一个语句块,或者另一个
if
语句。例如:
func main() {
x := 10
if x > 5 {
fmt.Println("x is greater than 5")
} else {
fmt.Println("x is less than or equal to 5")
}
}
在这个例子中,
if
语句判断
x
是否大于5,并根据结果执行相应的代码块。
14.2 for语句
for
语句用于循环。
for
语句可以基于迭代控制方式的不同被分类为四种不同的类别:无限循环、带有单一条件的循环、带有
for
子句的循环和带有范围子句的循环。例如:
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
在这个例子中,
for
语句基于
for
子句进行循环,循环次数为10次。
14.3 switch语句
switch
语句包含一个或多个基于
switch
表达式的执行分支,称为
case
。
switch
表达式可以是表达式
switch
或类型
switch
。例如:
func main() {
switch x := 5; x {
case 1, 3, 5:
fmt.Println("Odd")
case 2, 4, 6:
fmt.Println("Even")
default:
fmt.Println("Unknown")
}
}
在这个例子中,
switch
语句根据
x
的值选择执行相应的代码块。
15. Go语言中的并发编程
15.1 goroutine
goroutine是Go语言中轻量级的并发单元。goroutine经常与通道一起使用。例如:
func main() {
go func() {
fmt.Println("Hello from goroutine")
}()
time.Sleep(time.Second)
fmt.Println("Hello from main")
}
在这个例子中,
go
关键字启动了一个新的goroutine,
time.Sleep
用于等待goroutine执行完毕。
15.2 通道
通道用于在goroutine之间传递数据。通道可以是单向或双向的。例如:
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
fmt.Println(<-ch) // 输出: 1
}
在这个例子中,
ch
是一个双向通道,
ch <- 1
将1发送到通道,
<-ch
从通道接收1。
15.3 通道的方向
通道可以指定方向,发送(
channel<-
)或接收(
<-channel
)。如果未指定方向,给定的通道类型是双向的。例如:
func main() {
sender := make(chan<- int)
receiver := make(<-chan int)
go func() {
sender <- 1
}()
fmt.Println(<-receiver) // 输出: 1
}
在这个例子中,
sender
是只写通道,
receiver
是只读通道。
16. Go语言中的模块与工作区
16.1 Go模块
Go模块是相关Go包的集合,这些Go包存储在一个文件树中,其根目录包含一个
go.mod
文件。Go模块是源代码共享和版本控制的单元,以及依赖管理。例如:
module gitlab.com/banana-farm/producer
go 1.19
require gitlab.com/banana-farm/consumer v0.1.0
replace gitlab.com/banana-farm/consumer => ../consumer
在这个例子中,
go.mod
文件定义了模块路径和依赖关系。
16.2 Go工作区
Go命令支持工作区模式。这可以通过在工作目录或父目录中放置一个
go.work
文件,或者通过设置
GOWORK
环境变量来启用。例如:
go 1.19
use ./producer
use ./consumer
use ./driver
在这个例子中,
go.work
文件指定了工作区中的主模块。
17. Go语言中的包与导入
17.1 包的定义
Go包是Go程序的基本组织单位。一个Go程序是由一个或多个包构成的。一个Go包是由一个或多个源代码文件构成的。例如:
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Pi)
}
在这个例子中,
main
包导入了
fmt
和
math
包。
17.2 导入声明
源文件可能包含一组导入声明,这表明包含声明的文件依赖于导入包的功能。导入的包是程序的一部分,并且它们与程序的源代码一起在本地机器上编译。例如:
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
在这个例子中,
fmt
包被导入并在
main
函数中使用。
17.3 导入副作用
一个特殊的使用场景是使用
import
声明语法,将空白标识符(
_
)作为包名别名。例如:
import _ "fmt"
func main() {
// 导入fmt包,但不使用其功能
}
在这个例子中,
fmt
包被导入,但其功能未被使用。
18. Go语言中的词法元素
18.1 注释
Go语言支持两种类型的注释:行注释和块注释。行注释以字符序列
//
开始,并且它会持续到行尾。块注释以字符序列
/*
开始,并在第一个后续字符序列
*/
后停止。例如:
// 这是行注释
/*
这是块注释
*/
在这个例子中,
//
和
/* */
分别用于行注释和块注释。
18.2 分号
Go语言的正式语法使用分号来终止语句,类似于大多数类C语言。然而,这些分号通常不会出现在Go源代码中。词法分析器自动添加分号。例如:
func main() {
a := 1
b := 2
c := a + b
fmt.Println(c)
}
在这个例子中,分号被自动添加到每个语句的末尾。
18.3 标识符
标识符是程序实体(如变量和类型)的名称。一个标识符由一个或多个字母和数字组成,其首字母必须是字母。例如:
func main() {
var myVariable int = 10
fmt.Println(myVariable)
}
在这个例子中,
myVariable
是一个标识符。
18.4 关键字
Go语言中有许多关键字,它们不能在Go程序中用作标识符。例如:
func main() {
var i int = 10
if i > 5 {
fmt.Println("i is greater than 5")
}
}
在这个例子中,
if
是一个关键字。
18.5 运算符和标点符号
Go语言中的运算符和标点符号用于表达式和语句。例如:
func main() {
a := 1 + 2
b := a * 3
c := b / 4
d := c % 5
fmt.Println(a, b, c, d)
}
在这个例子中,
+
、
*
、
/
和
%
是运算符。
18.6 字面量
Go语言支持多种字面量,如整数字面量、浮点数字面量、想象字面量、符文字面量和字符串字面量。例如:
func main() {
i := 100
f := 3.14
c := 1 + 2i
r := 'A'
s := "Hello, World!"
fmt.Println(i, f, c, r, s)
}
在这个例子中,
i
、
f
、
c
、
r
和
s
是不同类型的字面量。
19. Go语言中的数组与切片
19.1 数组
数组类型是一种复合类型,由一系列单一类型的元素(称为“元素类型”)组成。数组的元素在内存中连续存储,它们可以被索引。例如:
func main() {
arr := [3]int{1, 2, 3}
fmt.Println(arr[0]) // 输出: 1
}
在这个例子中,
arr
是一个包含3个整型元素的数组。
19.2 切片
切片是Go语言中的另一类复合类型,由其他类型构建而成。切片是在底层数组上定义的,并代表该数组的一个连续段。例如:
func main() {
slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice[1:3]) // 输出: [2 3]
}
在这个例子中,
slice
是一个包含5个整型元素的切片,
slice[1:3]
表示从索引1到索引3(不包括3)的子切片。
19.3 切片的构造
切片可以通过切片字面量、切片表达式和
make
函数构造。例如:
func main() {
slice := []int{1, 2, 3}
slice2 := slice[1:3]
slice3 := make([]int, 5, 10)
fmt.Println(slice, slice2, slice3)
}
在这个例子中,
slice
是一个切片字面量,
slice2
是一个切片表达式,
slice3
是通过
make
函数构造的切片。
20. Go语言中的映射类型
20.1 映射的定义
映射是一个或多个值或变量的集合,这些值或变量具有指定的类型(“元素类型”),通过另一指定类型(“键类型”)的键集进行索引。例如:
func main() {
m := map[string]int{"one": 1, "two": 2}
fmt.Println(m["one"]) // 输出: 1
}
在这个例子中,
m
是一个映射类型,键类型为
string
,元素类型为
int
。
20.2 映射的构造
映射可以通过
make
函数或映射字面量构造。例如:
func main() {
m1 := make(map[string]int)
m2 := map[string]int{"one": 1, "two": 2}
fmt.Println(m1, m2)
}
在这个例子中,
m1
是通过
make
函数构造的映射,
m2
是通过映射字面量构造的映射。
20.3 映射的操作
映射支持插入、删除和查询操作。例如:
func main() {
m := map[string]int{"one": 1, "two": 2}
m["three"] = 3
delete(m, "two")
fmt.Println(m) // 输出: map[one:1 three:3]
}
在这个例子中,
m["three"] = 3
插入了一个新的键值对,
delete(m, "two")
删除了一个键值对。
21. Go语言中的通道类型
21.1 通道的定义
通道持有特定元素类型的值,这些值可以在多个并发执行的函数之间访问。函数可以向通道“发送”和/或从通道“接收”值。例如:
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
fmt.Println(<-ch) // 输出: 1
}
在这个例子中,
ch
是一个通道类型,
ch <- 1
将1发送到通道,
<-ch
从通道接收1。
21.2 通道的方向
通道可以指定方向,发送(
channel<-
)或接收(
<-channel
)。如果未指定方向,给定的通道类型是双向的。例如:
func main() {
sender := make(chan<- int)
receiver := make(<-chan int)
go func() {
sender <- 1
}()
fmt.Println(<-receiver) // 输出: 1
}
在这个例子中,
sender
是只写通道,
receiver
是只读通道。
21.3 通道的容量
通道的大小,或者通道中缓冲区的大小,被称为“容量”。可以使用内置函数
make
创建一个新的通道值,通道类型和容量作为其两个参数。例如:
func main() {
ch := make(chan int, 100)
ch <- 1
fmt.Println(<-ch) // 输出: 1
}
在这个例子中,
ch
是一个容量为100的通道。
22. Go语言中的指针类型
22.1 指针的定义
指针是对变量(非指针类型)的地址。未初始化指针的值是
nil
。指针类型表示所有指向给定类型变量的指针集合,称为指针的基础类型。例如:
func main() {
i := 10
p := &i
fmt.Println(*p) // 输出: 10
}
在这个例子中,
p
是指向
i
的指针,
*p
表示`
22.2 指针的应用
指针在Go语言中主要用于提高性能和实现引用语义。例如,当需要修改函数内部的变量时,可以通过传递指针来实现。以下是使用指针的一个典型例子:
func modifyValue(p *int) {
*p = 20
}
func main() {
i := 10
modifyValue(&i)
fmt.Println(i) // 输出: 20
}
在这个例子中,
modifyValue
函数通过指针修改了
i
的值。
22.3 指针的注意事项
使用指针时需要注意以下几点:
-
空指针
:未初始化的指针值是
nil。访问nil指针会导致运行时恐慌。 - 指针算术 :Go语言不支持指针算术,这与C语言不同。
- 指针的安全性 :Go语言的垃圾回收机制会自动管理指针,避免内存泄漏。
23. Go语言中的结构体
23.1 结构体的定义与初始化
结构体是有限数量的元素,称为字段的集合。每个字段都有一个名称和一个类型。结构体可以通过结构体字面量进行初始化。例如:
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
fmt.Println(p) // 输出: {Alice 30}
}
在这个例子中,
Person
是一个结构体类型,包含
Name
和
Age
两个字段。
23.2 结构体的方法
结构体可以定义方法。方法的接收者可以是指针或值类型。例如:
type Person struct {
Name string
Age int
}
func (p Person) SayHello() {
fmt.Println("Hello, my name is", p.Name)
}
func main() {
p := Person{Name: "Alice", Age: 30}
p.SayHello() // 输出: Hello, my name is Alice
}
在这个例子中,
SayHello
方法的接收者是
Person
类型的值。
23.3 结构体的嵌入
结构体可以嵌入其他结构体。嵌入的结构体字段可以直接访问。例如:
type Address struct {
City string
}
type Person struct {
Name string
Age int
Address
}
func main() {
p := Person{Name: "Alice", Age: 30, Address: Address{City: "Beijing"}}
fmt.Println(p.City) // 输出: Beijing
}
在这个例子中,
Person
结构体嵌入了
Address
结构体,可以直接访问
City
字段。
24. Go语言中的接口与实现
24.1 接口的定义
接口定义了一组方法的集合。接口类型可以用于实现运行时多态性。例如:
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct{}
func (c Cat) Speak() string {
return "Meow!"
}
func main() {
var s Speaker
s = Dog{}
fmt.Println(s.Speak()) // 输出: Woof!
s = Cat{}
fmt.Println(s.Speak()) // 输出: Meow!
}
在这个例子中,
Dog
和
Cat
类型都实现了
Speaker
接口,因此它们可以被赋值给
Speaker
类型的变量。
24.2 接口的实现
接口的实现非常灵活。只要一个类型实现了接口中声明的所有方法,它就被认为实现了该接口。Go语言中的接口实现是隐式的,不需要显式声明。例如:
type Animal interface {
Eat()
Sleep()
}
type Mammal interface {
Animal
Walk()
}
type Bird interface {
Animal
Fly()
}
type Human interface {
Mammal
Laugh()
}
func main() {
var h Human
// Human接口包括Eat()、Sleep()、Walk()和Laugh()方法
}
在这个例子中,
Human
接口不仅包括
Mammal
接口中的所有方法,还包括
Animal
接口中的方法。
24.3 接口的使用场景
接口在Go语言中广泛用于实现多态性和解耦代码。例如,接口可以用于定义一组通用的行为,而具体的实现由不同的类型提供。接口还可以用于编写更加通用的代码,提高代码的复用性。
25. Go语言中的泛型
25.1 泛型的引入
自Go 1.18版本起,Go语言引入了泛型。泛型允许我们编写更通用的代码,适用于多种类型。泛型的引入解决了长期以来Go语言缺乏类型参数化的问题。泛型不仅可以用于类型定义,还可以用于函数和方法声明。
25.2 泛型的使用
泛型的使用非常直观。我们可以通过在类型或函数声明中引入类型参数来创建泛型类型或函数。例如:
type List[T any] struct {
items []T
}
func New[T any]() *List[T] {
return &List[T]{items: []T{}}
}
func (l *List[T]) Add(item T) {
l.items = append(l.items, item)
}
func (l *List[T]) Get(index int) T {
return l.items[index]
}
func main() {
list := New[int]()
list.Add(1)
list.Add(2)
fmt.Println(list.Get(0)) // 输出: 1
}
在这个例子中,
List
是一个泛型类型,
T
是类型参数。
New
函数创建了一个新的
List
实例,
Add
和
Get
方法可以操作
List
中的元素。
25.3 泛型的约束
Go语言中的泛型类型参数可以有约束。约束可以是任何接口类型。例如:
type Ordered interface {
~int | ~float64 | ~string
}
func Min[T Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(1, 2)) // 输出: 1
fmt.Println(Min(1.5, 2.5)) // 输出: 1.5
fmt.Println(Min("a", "b")) // 输出: a
}
在这个例子中,
Min
函数的类型参数
T
被约束为
Ordered
接口,这意味着
T
只能是整型、浮点型或字符串类型。
25.4 泛型的优化
泛型可以显著提高代码的复用性和灵活性。通过使用泛型,可以避免为每种类型编写重复的代码。例如,创建一个通用的排序函数:
type Ordered interface {
~int | ~float64 | ~string
}
func Sort[T Ordered](slice []T) {
sort.Slice(slice, func(i, j int) bool {
return slice[i] < slice[j]
})
}
func main() {
numbers := []int{3, 1, 2}
Sort(numbers)
fmt.Println(numbers) // 输出: [1 2 3]
floats := []float64{3.5, 1.2, 2.8}
Sort(floats)
fmt.Println(floats) // 输出: [1.2 2.8 3.5]
strings := []string{"banana", "apple", "cherry"}
Sort(strings)
fmt.Println(strings) // 输出: [apple banana cherry]
}
在这个例子中,
Sort
函数可以对不同类型的切片进行排序,而不需要为每种类型编写不同的排序函数。
26. Go语言中的错误处理
26.1 错误接口
Go语言包含一个预声明的错误接口类型:
type error interface {
Error() string
}
尽管不是必须的,但通常使用这种通用接口来表示错误情况是一个好习惯。在Go语言中,
nil
错误值代表没有错误。当返回一个非
nil
错误时,通常会忽略正常的返回值。例如:
func Read(f *File, b []byte) (n int, err error) {
// 实现省略
}
func main() {
n, err := Read(file, buffer)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(n)
}
在这个例子中,
Read
函数返回两个值:读取的字节数
n
和错误
err
。如果
err
不为
nil
,则表示出现了错误,程序会打印错误信息并返回。
26.2 运行时恐慌
执行错误,如尝试将数字除以0,或尝试索引数组超出其合法范围,会触发运行时恐慌。这相当于调用内置函数
panic
,其值为错误类型,来自运行时包。例如:
func main() {
panic("something went wrong")
}
在这个例子中,
panic
函数会创建一个运行时错误,该错误将沿着调用链冒泡并终止程序(除非以某种方式处理)。
26.3 恢复函数
当程序发生恐慌时,Go语言会立即停止当前协程中当前函数的执行,并开始展开调用栈。在这个过程中,所有的延迟函数都会被调用。如果这个调用链中的任何一个延迟函数包含了对内置恢复函数的调用,那么它将停止展开过程,并从那个点开始恢复协程的正常执行。例如:
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("something went wrong")
}
在这个例子中,
defer
语句中的匿名函数会在
panic
发生时捕获并恢复程序的执行。
27. Go语言中的表达式与运算符
27.1 表达式的分类
Go语言中的表达式可以分为常量表达式、复合字面量、索引表达式、选择器表达式、函数调用表达式等。每种表达式都有其特定的用途和操作。
27.2 表达式的操作
表达式可以进行各种操作,如算术运算、逻辑运算、比较运算等。例如:
func main() {
a := 1 + 2
b := a * 3
c := b / 4
d := c % 5
fmt.Println(a, b, c, d) // 输出: 3 9 2 2
}
在这个例子中,
a
、
b
、
c
和
d
都是通过算术运算得到的。
27.3 表达式的优先级
Go语言中的运算符有优先级。一元运算符具有最高的优先级,其次是乘法运算符,然后是加法运算符,再然后是比较运算符,最后是逻辑运算符。例如:
func main() {
a := 1 + 2 * 3
b := (1 + 2) * 3
fmt.Println(a, b) // 输出: 7 9
}
在这个例子中,
a
的计算顺序是先乘法后加法,而
b
的计算顺序是先括号内的加法后乘法。
27.4 表达式的类型转换
类型转换是Go语言中常见的操作。例如,将整型转换为浮点型:
func main() {
x := uint(10)
y := float32(-1e-100)
fmt.Println(x, y) // 输出: 10 -1e-100
}
在这个例子中,
uint(10)
和
float32(-1e-100)
是转换表达式。
28. Go语言中的声明与作用域
28.1 声明语句
声明语句将一个(非空白)标识符绑定到一个常量、变量、类型、函数、方法、标签或导入的包。在Go程序中,每个非空白标识符在使用前必须声明。在同一个代码块中,或者在文件和包的代码块之间,不能声明相同的标识符两次。
28.2 顶层声明
Go包主要包含一些顶层声明(除了每个源文件需要的包条款和导入语句,如果有的话)。以下是在Go中被认为是顶层声明的:
- 常量声明
- 变量声明
- 类型/接口声明
- 函数声明
- 方法声明
28.3 代码块
代码块是零个、一个或多个语句的序列。代码块可以嵌套,并且它们影响作用域。例如:
graph TD;
A[代码块] --> B[语句1];
A --> C[语句2];
C --> D[代码块];
D --> E[语句3];
D --> F[语句4];
在这个流程图中,代码块
A
包含两个语句
B
和
C
,而语句
C
又包含一个代码块
D
,代码块
D
包含两个语句
E
和
F
。
28.4 作用域
Go语言使用(显式或隐式)代码块进行词法作用域划分。标识符的作用域是源文本的范围,在这个范围内标识符表示声明的常量、变量、类型、函数、方法、标签或导入的包。例如:
| 类型 | 作用域 |
|---|---|
| 全局代码块 | 整个程序 |
| 包代码块 | 包中的所有Go源文本 |
| 文件代码块 | 文件中的所有Go源文本 |
在这个表格中,全局代码块的作用域是整个程序,包代码块的作用域是包中的所有Go源文本,文件代码块的作用域是文件中的所有Go源文本。
29. Go语言中的函数与方法
29.1 函数类型
函数签名是函数的参数类型列表和结果类型列表。函数类型表示所有具有相同函数签名的函数和方法的集合。未初始化的函数类型变量的值是
nil
。例如:
func add(a, b int) int {
return a + b
}
func main() {
var f func(int, int) int
f = add
fmt.Println(f(1, 2)) // 输出: 3
}
在这个例子中,
add
函数的签名是
func(int, int) int
,表示它接受两个整型参数并返回一个整型值。
29.2 可变参数函数
函数的最后一个输入参数的类型可以前缀一个标记
...
。具有此类参数的函数称为可变参数函数,它可以被调用时该参数可以传入零个或多个参数。例如:
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func main() {
fmt.Println(sum()) // 输出: 0
fmt.Println(sum(1, 2, 3)) // 输出: 6
}
在这个例子中,
sum
函数是一个可变参数函数,它可以接受零个或多个整型参数。
29.3 泛型函数
如果函数声明指定了类型参数,函数名表示一个泛型函数。泛型函数定义了一组由类型参数化的函数(或函数模板),并且它们必须在使用时实例化。例如:
func Min[T Ordered](x, y T) T {
if x < y {
return x
}
return y
}
func main() {
fmt.Println(Min(1, 2)) // 输出: 1
fmt.Println(Min(1.5, 2.5)) // 输出: 1.5
}
在这个例子中,
Min
函数是一个泛型函数,它接受两个
Ordered
类型的参数,并返回较小的那个。
29.4 方法声明
方法是一个带有接收者的函数。方法声明将一个标识符、一个方法名绑定到一个方法,并将该方法与接收者的基类型关联起来。例如:
type Point struct {
X, Y float64
}
func (p Point) Distance() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
func main() {
p := Point{3, 4}
fmt.Println(p.Distance()) // 输出: 5
}
在这个例子中,
Distance
方法的接收者是
Point
类型的值。
29.5 泛型方法
泛型方法允许方法的接收者类型是泛型类型。例如:
type List[T any] struct {
items []T
}
func (l *List[T]) Add(item T) {
l.items = append(l.items, item)
}
func main() {
list := &List[int]{}
list.Add(1)
list.Add(2)
fmt.Println(list.items) // 输出: [1 2]
}
在这个例子中,
List
是一个泛型类型,
Add
方法的接收者是
List
类型的指针。
30. Go语言中的控制结构
30.1 if语句
if
语句用于条件判断。
if
语句可以被可选地跟随
else
关键字,以及另一个语句块,或者另一个
if
语句。例如:
func main() {
x := 10
if x > 5 {
fmt.Println("x is greater than 5")
} else {
fmt.Println("x is less than or equal to 5")
}
}
在这个例子中,
if
语句判断
x
是否大于5,并根据结果执行相应的代码块。
30.2 for语句
for
语句用于循环。
for
语句可以基于迭代控制方式的不同被分类为四种不同的类别:无限循环、带有单一条件的循环、带有
for
子句的循环和带有范围子句的循环。例如:
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
在这个例子中,
for
语句基于
for
子句进行循环,循环次数为10次。
30.3 switch语句
switch
语句包含一个或多个基于
switch
表达式的执行分支,称为
case
。
switch
表达式可以是表达式
switch
或类型
switch
。例如:
func main() {
switch x := 5; x {
case 1, 3, 5:
fmt.Println("Odd")
case 2, 4, 6:
fmt.Println("Even")
default:
fmt.Println("Unknown")
}
}
在这个例子中,
switch
语句根据
x
的值选择执行相应的代码块。
30.4 select语句
select
语句基于一组一个或多个(发送或接收)通道操作。它类似于
switch
语句,但是在
select
语句中,所有
case
都涉及通信操作。例如:
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
ch1 <- 1
}()
go func() {
ch2 <- 2
}()
select {
case v := <-ch1:
fmt.Println("Received from ch1:", v)
case v := <-ch2:
fmt.Println("Received from ch2:", v)
}
}
在这个例子中,
select
语句监听两个通道
ch1
和
ch2
,并根据最先接收到的数据执行相应的代码块。
30.5 break与continue语句
break
和
continue
语句用于控制循环的执行。
break
语句终止执行同一函数内最内层的复合语句,
continue
语句开始在同一个函数内执行最内层的
for
循环的下一次迭代。例如:
func main() {
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Println("An odd number:", i)
}
}
在这个例子中,
continue
语句跳过了偶数,只打印奇数。
30.6 defer语句
defer
语句用于延迟函数调用,直到包含它的函数返回之前。例如:
func main() {
defer fmt.Println("Deferred call")
fmt.Println("Direct call")
}
在这个例子中,
defer
语句中的
fmt.Println("Deferred call")
会在
main
函数返回之前执行。
31. Go语言中的并发编程
31.1 goroutine
goroutine是Go语言中轻量级的并发单元。goroutine经常与通道一起使用。例如:
func main() {
go func() {
fmt.Println("Hello from goroutine")
}()
time.Sleep(time.Second)
fmt.Println("Hello from main")
}
在这个例子中,
go
关键字启动了一个新的goroutine,
time.Sleep
用于等待goroutine执行完毕。
31.2 通道
通道用于在goroutine之间传递数据。通道可以是单向或双向的。例如:
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
fmt.Println(<-ch) // 输出: 1
}
在这个例子中,
ch
是一个双向通道,
ch <- 1
将1发送到通道,
<-ch
从通道接收1。
31.3 通道的方向
通道可以指定方向,发送(
channel<-
)或接收(
<-channel
)。如果未指定方向,给定的通道类型是双向的。例如:
func main() {
sender := make(chan<- int)
receiver := make(<-chan int)
go func() {
sender <- 1
}()
fmt.Println(<-receiver) // 输出: 1
}
在这个例子中,
sender
是只写通道,
receiver
是只读通道。
31.4 通道的容量
通道的大小,或者通道中缓冲区的大小,被称为“容量”。可以使用内置函数
make
创建一个新的通道值,通道类型和容量作为其两个参数。例如:
func main() {
ch := make(chan int, 100)
ch <- 1
fmt.Println(<-ch) // 输出: 1
}
在这个例子中,
ch
是一个容量为100的通道。
32. Go语言中的模块与工作区
32.1 Go模块
Go模块是相关Go包的集合,这些Go包存储在一个文件树中,其根目录包含一个
go.mod
文件。Go模块是源代码共享和版本控制的单元,以及依赖管理。例如:
module gitlab.com/banana-farm/producer
go 1.19
require gitlab.com/banana-farm/consumer v0.1.0
replace gitlab.com/banana-farm/consumer => ../consumer
在这个例子中,
go.mod
文件定义了模块路径和依赖关系。
32.2 Go工作区
Go命令支持工作区模式。这可以通过在工作目录或父目录中放置一个
go.work
文件,或者通过设置
GOWORK
环境变量来启用。例如:
go 1.19
use ./producer
use ./consumer
use ./driver
在这个例子中,
go.work
文件指定了工作区中的主模块。
33. Go语言中的包与导入
33.1 包的定义
Go包是Go程序的基本组织单位。一个Go程序是由一个或多个包构成的。一个Go包是由一个或多个源代码文件构成的。例如:
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Pi)
}
在这个例子中,
main
包导入了
fmt
和
math
包。
33.2 导入声明
源文件可能包含一组导入声明,这表明包含声明的文件依赖于导入包的功能。导入的包是程序的一部分,并且它们与程序的源代码一起在本地机器上编译。例如:
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
在这个例子中,
fmt
包被导入并在
main
函数中使用。
33.3 导入副作用
一个特殊的使用场景是使用
import
声明语法,将空白标识符(
_
)作为包名别名。例如:
import _ "fmt"
func main() {
// 导入fmt包,但不使用其功能
}
在这个例子中,
fmt
包被导入,但其功能未被使用。
34. Go语言中的词法元素
34.1 注释
Go语言支持两种类型的注释:行注释和块注释。行注释以字符序列
//
开始,并且它会持续到行尾。块注释以字符序列
/*
开始,并在第一个后续字符序列
*/
后停止。例如:
// 这是行注释
/*
这是块注释
*/
在这个例子中,
//
和
/* */
分别用于行注释和块注释。
34.2 分号
Go语言的正式语法使用分号来终止语句,类似于大多数类C语言。然而,这些分号通常不会出现在Go源代码中。词法分析器自动添加分号。例如:
func main() {
a := 1
b := 2
c := a + b
fmt.Println(c)
}
在这个例子中,分号被自动添加到每个语句的末尾。
34.3 标识符
标识符是程序实体(如变量和类型)的名称。一个标识符由一个或多个字母和数字组成,其首字母必须是字母。例如:
func main() {
var myVariable int = 10
fmt.Println(myVariable)
}
在这个例子中,
myVariable
是一个标识符。
34.4 关键字
Go语言中有许多关键字,它们不能在Go程序中用作标识符。例如:
func main() {
var i int = 10
if i > 5 {
fmt.Println("i is greater than 5")
}
}
在这个例子中,
if
是一个关键字。
34.5 运算符和标点符号
Go语言中的运算符和标点符号用于表达式和语句。例如:
func main() {
a := 1 + 2
b := a * 3
c := b / 4
d := c % 5
fmt.Println(a, b, c, d)
}
在这个例子中,
+
、
*
、
/
和
%
是运算符。
34.6 字面量
Go语言支持多种字面量,如整数字面量、浮点数字面量、想象字面量、符文字面量和字符串字面量。例如:
func main() {
i := 100
f := 3.14
c := 1 + 2i
r := 'A'
s := "Hello, World!"
fmt.Println(i, f, c, r, s)
}
在这个例子中,
i
、
f
、
c
、
r
和
s
是不同类型的字面量。
35. Go语言中的数组与切片
35.1 数组
数组类型是一种复合类型,由一系列单一类型的元素(称为“元素类型”)组成。数组的元素在内存中连续存储,它们可以被索引。例如:
func main() {
arr := [3]int{1, 2, 3}
fmt.Println(arr[0]) // 输出: 1
}
在这个例子中,
arr
是一个包含3个整型元素的数组。
35.2 切片
切片是Go语言中的另一类复合类型,由其他类型构建而成。切片是在底层数组上定义的,并代表该数组的一个连续段。例如:
func main() {
slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice[1:3]) // 输出: [2 3]
}
在这个例子中,
slice
是一个包含5个整型元素的切片,
slice[1:3]
表示从索引1到索引3(不包括3)的子切片。
35.3 切片的构造
切片可以通过切片字面量、切片表达式和
make
函数构造。例如:
func main() {
slice := []int{1, 2, 3}
slice2 := slice[1:3]
slice3 := make([]int, 5, 10)
fmt.Println(slice, slice2, slice3)
}
在这个例子中,
slice
是一个切片字面量,
slice2
是一个切片表达式,
slice3
是通过
make
函数构造的切片。
36. Go语言中的映射类型
36.1 映射的定义
映射是一个或多个值或变量的集合,这些值或变量具有指定的类型(“元素类型”),通过另一指定类型(“键类型”)的键集进行索引。例如:
func main() {
m := map[string]int{"one": 1, "two": 2}
fmt.Println(m["one"]) // 输出: 1
}
在这个例子中,
m
是一个映射类型,键类型为
string
,元素类型为
int
。
36.2 映射的构造
映射可以通过
make
函数或映射字面量构造。例如:
func main() {
m1 := make(map[string]int)
m2 := map[string]int{"one": 1, "two": 2}
fmt.Println(m1, m2)
}
在这个例子中,
m1
是通过
make
函数构造的映射,
m2
是通过映射字面量构造的映射。
36.3 映射的操作
映射支持插入、删除和查询操作。例如:
func main() {
m := map[string]int{"one": 1, "two": 2}
m["three"] = 3
delete(m, "two")
fmt.Println(m) // 输出: map[one:1 three:3]
}
在这个例子中,
m["three"] = 3
插入了一个新的键值对,
delete(m, "two")
删除了一个键值对。
37. Go语言中的通道类型
37.1 通道的定义
通道持有特定元素类型的值,这些值可以在多个并发执行的函数之间访问。函数可以向通道“发送”和/或从通道“接收”值。例如:
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
fmt.Println(<-ch) // 输出: 1
}
在这个例子中,
ch
是一个通道类型,
ch <- 1
将1发送到通道,
<-ch
从通道接收1。
37.2 通道的方向
通道可以指定方向,发送(
channel<-
)或接收(
<-channel
)。如果未指定方向,给定的通道类型是双向的。例如:
func main() {
sender := make(chan<- int)
receiver := make(<-chan int)
go func() {
sender <- 1
}()
fmt.Println(<-receiver) // 输出: 1
}
在这个例子中,
sender
是只写通道,
receiver
是只读通道。
37.3 通道的容量
通道的大小,或者通道中缓冲区的大小,被称为“容量”。可以使用内置函数
make
创建一个新的通道值,通道类型和容量作为其两个参数。例如:
func main() {
ch := make(chan int, 100)
ch <- 1
fmt.Println(<-ch) // 输出: 1
}
在这个例子中,
ch
是一个容量为100的通道。
38. Go语言中的指针类型
38.1 指针的定义
指针是对变量(非指针类型)的地址。未初始化指针的值是
nil
。指针类型表示所有指向给定类型变量的指针集合,称为指针的基础类型。例如:
func main() {
i := 10
p := &i
fmt.Println(*p) // 输出: 10
}
在这个例子中,
p
是指向
i
的指针,
*p
表示
i
的值。
38.2 指针的应用
指针在Go语言中主要用于提高性能和实现引用语义。例如,当需要修改函数内部的变量时,可以通过传递指针来实现。以下是使用指针的一个典型例子:
func modifyValue(p *int) {
*p = 20
}
func main() {
i := 10
modifyValue(&i)
fmt.Println(i) // 输出: 20
}
在这个例子中,
modifyValue
函数通过指针修改了
i
的值。
38.3 指针的注意事项
使用指针时需要注意以下几点:
-
空指针
:未初始化的指针值是
nil。访问nil指针会导致运行时恐慌。 - 指针算术 :Go语言不支持指针算术,这与C语言不同。
- 指针的安全性 :Go语言的垃圾回收机制会自动管理指针,避免内存泄漏。
39. Go语言中的结构体
39.1 结构体的定义与初始化
结构体是有限数量的元素,称为字段的集合。每个字段都有一个名称和一个类型,在一个结构体中,它们的顺序是重要的。结构体可以通过结构体字面量进行初始化。例如:
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
fmt.Println(p) // 输出: {Alice 30}
}
在这个例子中,
Person
是一个结构体类型,包含
Name
和
Age
两个字段。
39.2 结构体的方法
结构体可以定义方法。方法的接收者可以是指针或值类型。例如:
type Person struct {
Name string
Age int
}
func (p Person) SayHello() {
fmt.Println("Hello, my name is", p.Name)
}
func main() {
p := Person{Name: "Alice", Age: 30}
p.SayHello() // 输出: Hello, my name is Alice
}
在这个例子中,
SayHello
方法的接收者是
Person
类型的值。
39.3 结构体的嵌入
结构体可以嵌入其他结构体。嵌入的结构体字段可以直接访问。例如:
type Address struct {
City string
}
type Person struct {
Name string
Age int
Address
}
func main() {
p := Person{Name: "Alice", Age: 30, Address: Address{City: "Beijing"}}
fmt.Println(p.City) // 输出: Beijing
}
在这个例子中,
Person
结构体嵌入了
Address
结构体,可以直接访问
City
字段。
超级会员免费看

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



