Go
Go
开始
1 | package main |
1 | 要运行这个程序,先将将代码放到名为 hello-world.go 的文件中,然后执行 go run |
变量
1 | package main |
1 | $ go run variables.go |
常量
1 | package main |
1 | $ go run constant.go |
循环
for
是 Go 中唯一的循环结构。这里会展示for
循环的一些基本使用方式。
1 | package main |
1 | $ go run for.go |
if/else分支
注意,在 Go 中,条件语句的圆括号不是必需的,但是花括号是必需的。
Go 没有三目运算符, 即使是基本的条件判断,依然需要使用完整的
if
语句。
1 | package main |
1 | $ go run if-else.go |
switch分支
switch 是多分支情况时快捷的条件语句。
1 | package main |
1 | $ go run switch.go |
数组
在 Go 语言中,数组(Array)是一种固定长度的集合类型,它存储一组同类型的元素。数组的长度是固定的,一旦定义了数组的大小,就不能改变。
1. 数组的定义
数组的定义包括指定元素的类型和数组的长度。例如,一个存储 5 个整数的数组可以这样定义:
1 | var arr [5]int // 定义一个长度为 5 的整数数组 |
也可以在定义时直接初始化数组的元素:
1 | var arr = [3]int{1, 2, 3} // 定义并初始化数组 |
或者 Go 支持根据初始化值自动推导数组的长度:
1 | var arr = [...]int{1, 2, 3} // 自动推导长度为 3 |
2. 数组的访问和修改
数组的元素通过索引访问,索引从 0 开始。可以通过下标访问和修改数组的元素:
1 | arr := [3]int{1, 2, 3} |
3. 数组的长度
数组的长度是固定的,可以使用 len()
函数获取数组的长度:
1 | arr := [3]int{1, 2, 3} |
4. 数组的传递
在 Go 中,数组是值类型,当你将一个数组作为参数传递给函数时,实际上是传递了该数组的副本。如果你修改了副本中的元素,原始数组不会受到影响。
1 | func modifyArray(arr [3]int) { |
如果你希望修改原数组的内容,可以传递指向数组的指针:
1 | func modifyArray(arr *[3]int) { |
5. 多维数组
Go 支持多维数组,可以通过嵌套的方式定义多维数组。例如,定义一个 2 行 3 列的数组:
1 | var arr [2][3]int |
输出:
1 | [[1 2 3] [4 5 6]] |
6. 数组的初始化
数组可以在声明时进行初始化,也可以在定义之后进行逐一赋值。以下是几种初始化数组的方式:
声明并初始化:
1
arr := [3]int{1, 2, 3} // 定义并初始化数组
使用
...
自动推导数组长度:1
arr := [...]int{1, 2, 3} // 自动推导数组长度为 3
使用
new
函数创建一个指向数组的指针:1
2
3arr := new([3]int) // 创建一个指向长度为 3 的数组的指针
arr[0] = 10
fmt.Println(arr) // 输出 &[10 0 0]
7. 数组与切片的区别
数组和切片的区别是一个重要的概念,尤其是 Go 语言的切片(Slice)非常常用。切片是对数组的一个抽象,允许灵活的动态扩展和缩减,而数组的大小是固定的。
- 数组:长度固定,类型和值传递。
- 切片:动态大小,引用传递,可以灵活扩展。
示例,使用切片:
1 | arr := [3]int{1, 2, 3} // 数组 |
8. 数组的应用场景
数组在 Go 中通常用于以下几种情况:
- 当需要固定大小的数据集合时。
- 需要用到固定大小的内存时(例如嵌入式系统或性能优化的场景)。
- 数组的值传递方式可以避免不必要的内存共享,确保数据不会被意外修改。
总结
- Go 语言中的数组是固定长度的,定义时需要指定数组的长度。
- 数组通过下标访问和修改元素,且下标从 0 开始。
- Go 中的数组是值类型,传递数组时会复制副本。如果希望修改原始数组,需要传递指针。
- Go 支持多维数组,可以使用嵌套方式定义。
- 数组与切片不同,切片更灵活且常用于实际编程中。
1 | package main |
1 | $ go run arrays.go |
切片
在 Go 语言中,slice
(切片)是一种灵活、动态的数组类型,它是对数组的一个引用,具有更高效的内存管理和更灵活的功能。切片不像数组那样具有固定的长度,它可以动态增长和缩小。
1. 创建切片
切片的创建有几种方式:
使用
make
函数:1
slice := make([]int, 5) // 创建一个长度为5的切片
你还可以指定切片的容量:
1
slice := make([]int, 5, 10) // 创建一个长度为5,容量为10的切片
直接使用数组字面量:
1
slice := []int{1, 2, 3, 4, 5} // 创建一个包含5个元素的切片
2. 切片的属性
每个切片都包含三个基本属性:
- 指针(Pointer): 指向底层数组的指针。
- 长度(Length): 切片中元素的数量。
- 容量(Capacity): 切片底层数组的大小,从切片的起始位置到底层数组的末尾。
你可以通过
len()
和cap()
函数获取切片的长度和容量:1
2fmt.Println(len(slice)) // 输出切片的长度
fmt.Println(cap(slice)) // 输出切片的容量
3. 切片的切割操作
切片可以从另一个切片或数组中通过索引和范围来切割:
1
2
3slice := []int{1, 2, 3, 4, 5}
subSlice := slice[1:4] // 获取从索引1到索引3的元素
fmt.Println(subSlice) // 输出: [2 3 4]你还可以指定切片的容量:
1
2subSlice := slice[1:4:5] // 切片从索引1到索引3,容量为5
fmt.Println(subSlice) // 输出: [2 3 4]
4. 动态扩展切片
使用
append
扩展切片:1
2
3slice := []int{1, 2, 3}
slice = append(slice, 4, 5) // 扩展切片
fmt.Println(slice) // 输出: [1 2 3 4 5]
append
函数用于向切片中添加元素,并且如果需要,它会自动调整切片的容量。
5. 删除切片元素
删除指定索引的元素:
1
2
3
4slice = append(slice[:index], slice[index+1:]...)
slice := []int{1, 2, 3, 4, 5}
slice = append(slice[:2], slice[3:]...) // 删除索引2的元素
fmt.Println(slice) // 输出: [1 2 4 5]
6. 切片的反转
反转切片元素:
1
2
3for i := 0; i < len(slice)/2; i++ {
slice[i], slice[len(slice)-1-i] = slice[len(slice)-1-i], slice[i]
}
7. 切片的查找
查找指定元素:
1
2
3
4
5
6
7found := false
for _, v := range slice {
if v == target {
found = true
break
}
}
8. 切片的分块
将切片分割成多个小块:
1
2
3
4
5
6
7
8
9
10
11func chunk(slice []int, size int) [][]int {
var chunks [][]int
for i := 0; i < len(slice); i += size {
end := i + size
if end > len(slice) {
end = len(slice)
}
chunks = append(chunks, slice[i:end])
}
return chunks
}
9. 切片排序
对切片进行排序:
1
2import "sort"
sort.Ints(slice) // 对整数切片进行升序排序
10. 删除重复元素
去除切片中的重复元素:
1
2
3
4
5
6
7
8
9
10
11func removeDuplicates(slice []int) []int {
seen := make(map[int]struct{})
result := []int{}
for _, v := range slice {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
result = append(result, v)
}
}
return result
}
11. copy
函数
复制切片内容:
1
copy(dst, src) // 将 src 的内容复制到 dst
总结
Go 切片提供了高效且灵活的操作方式,以下是常见的切片操作总结:
- 创建:通过
make
或字面量创建。 - 切割:使用切片操作符可以截取切片的部分。
- 扩展:通过
append
函数动态扩展切片。 - 删除:通过
append
删除切片中的元素。 - 反转:通过交换元素实现反转。
- 查找:通过遍历切片查找元素。
- 分块:将切片分割成多个小块。
- 排序:使用标准库中的
sort
对切片进行排序。 - 去重:通过
map
去除切片中的重复元素。 - 复制:使用
copy
将一个切片的内容复制到另一个切片中。
1 | package main |
1 | $ go run slices.go |
map
在Go语言中,map
是一种内建的数据类型,用于存储键值对(key-value pairs)。它类似于其他语言中的哈希表或字典,能够快速地根据键查找对应的值。
1. 创建 map
Go语言中的 map
可以通过内建的 make
函数或者字面量(literal)来创建。
使用 make
函数
1 | m := make(map[string]int) |
上面的代码创建了一个空的 map
,键为 string
类型,值为 int
类型。
使用字面量(literal)创建
1 | m := map[string]int{ |
上面的代码创建了一个初始包含两个键值对的 map
。
2. 向 map
中添加和更新元素
你可以直接通过键来添加或更新值:
1 | m["apple"] = 10 // 更新值 |
3. 获取 map
中的值
通过键来访问对应的值:
1 | value := m["apple"] |
4. 删除 map
中的元素
使用 delete
函数来删除指定键的元素:
1 | delete(m, "banana") // 删除键为"banana"的元素 |
5. 检查键是否存在
通过多返回值的方式来判断键是否存在。如果键存在,第二个返回值为 true
,否则为 false
。
1 | value, ok := m["apple"] |
6. 遍历 map
使用 for
循环来遍历 map
的所有键值对:
1 | for key, value := range m { |
7. map
的特性
- 无序性:
map
中的元素是无序的,遍历时顺序是随机的。 - 线程不安全:在多线程(goroutines)中并发修改同一个
map
时,Go 运行时会引发恐慌(panic)。如果需要并发访问map
,应使用sync.Mutex
或者sync.RWMutex
来加锁。
8. map
的容量
可以使用 len()
函数获取 map
中元素的数量:
1 | fmt.Println(len(m)) // 输出 map 中的元素个数 |
9. 空 map
和 nil
map
- 如果
map
没有被初始化(nil
),则它的行为类似于空map
,但是尝试向nil
map
中添加或删除元素会引发运行时错误。 - 可以使用
make
函数初始化一个空map
。
示例代码:
1 | package main |
1 | package main |
1 | $ go run maps.go |
range
在Go语言中,range
是一个非常强大的关键字,用于遍历数组、切片、map
和通道等数据结构。通过 range
,可以轻松遍历数据结构的元素,并同时获得索引(或键)和值。
1. range
遍历数组和切片
数组
1 | arr := [3]int{1, 2, 3} |
在遍历数组时,range
返回两个值:
index
:当前元素的索引。value
:当前元素的值。
切片
1 | slice := []string{"apple", "banana", "cherry"} |
对于切片,range
也返回两个值:
index
:当前元素的索引。value
:当前元素的值。
2. range
遍历 map
在 map
中,range
返回的是键值对:
1 | m := map[string]int{"apple": 5, "banana": 3} |
对于 map
,range
返回两个值:
key
:当前元素的键。value
:当前元素的值。
注意事项:
map
的遍历顺序是无序的,每次遍历的顺序可能不同。
3. range
遍历字符串
range
也可以用于遍历字符串。它会逐个获取字符串中的 Unicode 字符,而不是按字节遍历:
1 | str := "hello" |
在遍历字符串时,range
返回两个值:
index
:当前字符的索引。runeValue
:当前字符的 Unicode 码点值(rune
类型)。
4. 只获取值或索引
你可以通过 _
忽略 range
返回的某些值。
- 只获取索引:
1 | for index := range arr { |
- 只获取值:
1 | for _, value := range arr { |
5. range
遍历通道(channel)
range
也可以用来遍历通道(channel)中的元素。当通道关闭时,range
循环会停止。
1 | ch := make(chan int, 3) |
当通道关闭后,range
会自动遍历通道中的所有元素。
6. 示例:综合使用 range
1 | package main |
总结
range
是 Go 语言中遍历数据结构的关键字,可以遍历数组、切片、map
、通道等。- 在遍历
map
时,range
返回的是键和值;遍历数组或切片时,返回的是索引和值。 - 可以使用
_
来忽略不需要的返回值(如只关注索引或只关注值)。 range
在处理通道时会在通道关闭时停止。
range
是一个高效且灵活的工具,能够简化代码并提高可读性。
1 | package main |
1 | $ go run range.go |
函数
在 Go 语言中,函数(function)是基本的代码结构之一,用于封装一组语句,并通过函数调用来执行这些语句。函数可以有输入参数、返回值,也可以没有参数或返回值。
1. 定义函数
Go 语言使用 func
关键字来定义函数。
基本函数定义
1 | func add(a int, b int) int { |
这个函数的定义包含了:
func
关键字,表示定义一个函数。add
,函数的名称。(a int, b int)
,是函数的参数列表,表示这个函数接收两个int
类型的参数。int
,是返回值的类型,表示该函数返回一个int
类型的结果。
2. 调用函数
函数定义后,可以通过函数名调用它,并传递参数:
1 | result := add(3, 4) |
3. 函数参数
- 单一参数类型:可以在函数参数中使用相同类型的多个参数,简化代码。
1 | func add(a, b int) int { |
- 可变参数:Go 支持传递可变数量的参数,使用
...
来表示可变参数。
1 | func sum(numbers ...int) int { |
调用:
1 | fmt.Println(sum(1, 2, 3, 4)) // 输出 10 |
4. 返回值
Go 函数可以返回多个值:
1 | func swap(a, b int) (int, int) { |
调用:
1 | x, y := swap(1, 2) |
5. 命名返回值
Go 允许给返回值命名,返回值就像局部变量一样,可以在函数体内直接使用。这也简化了代码,使得不需要显式地使用 return
语句。
1 | func add(a, b int) (sum int) { |
6. 函数作为值
Go 允许函数作为值传递。这意味着可以将函数赋值给变量或作为参数传递给其他函数。
将函数赋值给变量
1 | func multiply(a, b int) int { |
函数作为参数
1 | func operate(a, b int, op func(int, int) int) int { |
7. 匿名函数
Go 支持匿名函数,即没有函数名的函数。匿名函数常常作为回调函数或临时使用。
1 | func() { |
8. 函数闭包
Go 支持闭包,闭包是一个函数,它可以“记住”并访问其外部作用域中的变量。即使外部函数返回,闭包仍然可以访问这些变量。
1 | func outer() func() int { |
在上面的例子中,increment
是一个闭包,它记住了 x
的值,并且每次调用时都会更新并返回新的值。
9. 函数的递归调用
Go 支持函数的递归调用,即一个函数在其定义中调用自己。
1 | func factorial(n int) int { |
在这个例子中,factorial
函数调用自己来计算阶乘。
10. 多返回值函数
Go 允许函数返回多个值,常用于处理错误处理等场景。
1 | func divide(a, b int) (int, error) { |
总结
- Go 语言中的函数是非常灵活的,可以定义单一返回值、多个返回值、可变参数和命名返回值等。
- Go 支持将函数作为值传递,允许匿名函数和闭包等特性。
- 函数调用时,可以直接使用返回值,也可以通过传递函数作为参数来进行更高阶的编程。
基础用法:
1 | package main |
1 |
|
多返回值:
1 | package main |
1 |
|
变参函数:
1 | package main |
1 | $ go run variadic-functions.go |
闭包:
1 | package main |
1 | $ go run closures.go |
递归:
1 | package main |
1 | $ go run recursion.go |
指针
在 Go 语言中,指针是存储变量内存地址的类型,它允许程序间接访问变量的值。指针可以非常方便地用于修改函数外部的变量值,以及在内存中操作数据。Go 的指针与 C 语言类似,但它不允许进行指针算术运算,这让它更加安全。
1. 定义和声明指针
在 Go 中,指针的声明方式是使用 *
表示指向某个类型的指针,类型前加 *
表示该变量是该类型的指针。
1 | var ptr *int |
这意味着 ptr
是一个指向 int
类型的指针,但它当前没有指向任何具体的内存地址。
2. 获取变量的地址(取地址操作符 &
)
要获取一个变量的内存地址,可以使用 &
操作符,它会返回变量的地址。
1 | x := 10 |
3. 解引用指针(取值操作符 *
)
通过指针访问它所指向的变量的值,使用 *
操作符。这个过程叫做“解引用”。
1 | x := 10 |
4. 指针与变量的关系
&
获取变量的地址。*
解引用指针,获取指针所指向的值。
示例:
1 | x := 10 |
5. 修改变量的值通过指针
指针允许通过间接访问修改原变量的值。通过解引用指针可以修改它指向的变量。
1 | x := 10 |
6. 指针作为函数参数
在 Go 中,函数参数是值传递的。如果希望函数修改传入的变量,可以传递变量的指针。这通常用于避免复制大量数据的成本,或允许函数修改传入的参数。
1 | func modifyValue(a *int) { |
7. 指针类型与结构体
结构体也可以使用指针,结构体指针常用于修改结构体的成员,避免复制结构体的副本。
1 | type Person struct { |
8. nil
指针
指针可以是 nil
,即它不指向任何有效的内存地址。你可以检查指针是否为 nil
来避免解引用空指针。
1 | var ptr *int |
9. 零值和指针
Go 中,指针的零值是 nil
,表示它没有指向任何有效的内存地址。当一个指针没有显式初始化时,它的默认值就是 nil
。
1 | var ptr *int // ptr 默认为 nil |
10. 指针的应用
- 修改函数外部的变量值:通过指针参数来修改传入的变量。
- 避免大数据的复制:对于大的数据结构(如数组或结构体),可以通过指针传递,避免复制整个数据结构。
- 链表、树等数据结构:指针广泛应用于实现链表、二叉树等动态数据结构。
总结
指针是 Go 语言中的一个重要特性,它让你能够间接操作内存,并通过引用传递修改变量的值。Go 语言中的指针有如下特点:
- 不允许指针算术运算,避免了 C 语言中的很多潜在错误。
- 使用
&
获取变量的地址,使用*
解引用指针。 - 可以通过指针修改变量的值,传递指针给函数可以改变函数外的变量。
指针在 Go 中的使用简单而强大,可以提高程序的效率并且节省内存空间。
1 | package main |
1 | $ go run pointers.go |
字符串和rune类型
一、字符串(string)
1. 字符串的定义
- 不可变性:字符串是不可变的,即一旦创建后,无法直接修改其中的内容。任何修改操作都会生成一个新的字符串。
- 底层结构:字符串底层是一个只读的字节序列(
[]byte
)。 - 编码:通常用来存储 UTF-8 编码的数据,可以包含 ASCII 字符、汉字、特殊符号等。
2. 字符串的常见操作
声明与初始化
- 使用双引号声明普通字符串。
- 使用反引号声明原始字符串,保留原格式,包括换行和特殊字符。
1 | s1 := "hello, world" // 普通字符串 |
获取字符串长度
- 使用
len()
获取字符串长度,返回的是字节数而非字符数。
1 | s := "你好" |
索引访问字符串
- 可以通过索引访问字符串中的字节:
1 | s := "hello" |
- 注意:通过索引访问的是单个字节,而不是字符。
字符串拼接
- 使用
+
或fmt.Sprintf()
拼接字符串。
1 | s1 := "hello" |
切片操作
- 支持通过切片截取子字符串,但结果是字节切片,可能导致截断多字节字符。
1 | s := "你好世界" |
3. Go标准库中的字符串方法
Go 提供了 strings
包来处理字符串,包括查找、替换、切分等操作。
常用函数
1 | import "strings" |
二、Rune
1. Rune的定义
rune
是 Go 的一个别名类型,等价于int32
,用于表示单个 Unicode 字符。- 每个
rune
占用4个字节,能够表示所有 Unicode 代码点。
2. Rune的用途
- 用于处理多字节字符(如汉字、emoji)或逐字符操作。
- 可以将字符串转换为
[]rune
,以逐字符处理,而不是逐字节。
3. Rune的常见操作
字符串与Rune的相互转换
将字符串转为
[]rune
:1
2runes := []rune("你好世界")
fmt.Println(runes) // [20320 22909 19990 30028]将
[]rune
转为字符串:1
2s := string([]rune{20320, 22909, 19990, 30028})
fmt.Println(s) // 你好世界
逐字符处理
使用
for
遍历[]rune
可以逐字符操作:1
2
3
4s := "hello, 世界"
for i, r := range []rune(s) {
fmt.Printf("第%d个字符:%c (Unicode: %U)\n", i, r, r)
}
修改字符串
字符串不可变,但可以通过
[]rune
修改后重新构造:1
2
3
4
5s := "hello"
runes := []rune(s)
runes[0] = 'H'
s = string(runes)
fmt.Println(s) // Hello
统计字符数量
使用
len()
获取的是字节数。若要获取字符数,需要将字符串转换为[]rune
:1
2
3s := "你好世界"
fmt.Println(len(s)) // 12(字节数)
fmt.Println(len([]rune(s))) // 4(字符数)
三、字符串与Rune的关系
特性 | 字符串(string) | rune |
---|---|---|
类型 | 不可变的字节序列 | 表示单个 Unicode 代码点 |
内存占用 | 每个字符占用 1~4 字节(UTF-8 编码) | 每个字符固定占用 4 字节(int32 类型) |
表示范围 | UTF-8 编码的字节序列 | Unicode 字符 |
适用场景 | 用于存储和操作整段文本 | 用于逐字符处理,支持多字节字符 |
索引操作 | 索引访问的是字节 | 可以通过 []rune 访问字符 |
四、使用场景与注意事项
- 字符串处理:
- 如果主要处理整段文本数据,使用
string
。 - 避免直接索引多字节字符,否则可能导致错误。
- 如果主要处理整段文本数据,使用
- 多字节字符处理:
- 如果需要逐字符操作,使用
[]rune
。 - 转换为
[]rune
后,才能安全地进行字符级别的索引和修改。
- 如果需要逐字符操作,使用
- 性能权衡:
- 操作
string
通常更高效,因为底层是只读的[]byte
。 - 使用
[]rune
需要额外的内存,适合复杂字符处理场景。
- 操作
五、总结
- 字符串(string):主要用于存储和处理文本,适合完整的文本操作。支持丰富的库函数,如查找、替换、切分等。
- Rune:用来处理单个 Unicode 字符,尤其适合多字节字符和逐字符遍历操作。
- 转换技巧:
[]rune
和string
的相互转换,是处理字符和文本的桥梁。
1 | package main |
1 | $ go run strings-and-runes.go |
结构体
1. 定义结构体
结构体使用 type
关键字定义,格式如下:
1 | type StructName struct { |
示例:
1 | type Person struct { |
2. 创建结构体实例
创建结构体实例的方式有以下几种:
2.1 使用结构体字面量
1 | p := Person{"Alice", 30} |
2.2 使用字段名初始化(推荐)
1 | p := Person{Name: "Bob", Age: 25} |
2.3 创建空结构体并赋值
1 | var p Person |
2.4 使用 new
创建结构体指针
1 | p := new(Person) |
3. 结构体的零值
结构体的零值是其所有字段的零值,例如:
1 | type Example struct { |
4. 结构体字段的访问与修改
结构体字段可以通过点操作符(.
)访问和修改:
1 | p := Person{Name: "Eve", Age: 28} |
5. 结构体的比较
两个结构体实例可以直接比较,但前提是它们的所有字段都支持比较操作。
1 | p1 := Person{Name: "Alice", Age: 30} |
6. 匿名字段
结构体支持匿名字段(嵌套结构体),可以通过类型名直接使用:
1 | type Address struct { |
7. 结构体的方法
Go 中的方法是绑定到特定类型(包括结构体)的函数。
7.1 定义方法
方法定义时,必须有一个接收者(receiver)。接收者可以是值类型或指针类型。
1 | func (p Person) SayHello() { |
7.2 调用方法
1 | p := Person{Name: "Grace", Age: 24} |
7.3 值接收者 vs 指针接收者
- 值接收者:方法操作的是结构体的副本,不会修改原始数据。
- 指针接收者:方法操作的是结构体的指针,可以修改原始数据。
示例:
1 | func (p Person) ChangeName(newName string) { |
8. 嵌套结构体
结构体可以嵌套另一个结构体以实现更复杂的数据结构:
1 | type Company struct { |
9. JSON 与结构体
结构体可以与 JSON 数据相互转换,需使用 encoding/json
包。
9.1 转换为 JSON
1 | import "encoding/json" |
9.2 从 JSON 转换为结构体
1 | jsonStr := `{"Name": "Mike", "Age": 29}` |
9.3 使用标签自定义 JSON 字段
1 | type Person struct { |
10. 结构体的内存布局
结构体的内存分配是连续的,字段的顺序会影响内存对齐和大小。
优化内存对齐
1 | type Optimized struct { |
Optimized
的字段顺序可以减少内存填充,提高内存使用效率。
1 | package main |
1 | $ go run structs.go |
方法
在Go语言中,结构体(struct
)是一种用户定义的数据类型,它由一组字段(fields
)组成,字段可以是不同类型的数据。结构体的方法(methods
)是与结构体相关的函数,它们能够操作结构体中的数据。
1. 定义结构体
首先,我们需要定义一个结构体。结构体的定义使用关键字 type
来创建,格式如下:
1 | type StructName struct { |
2. 结构体的方法
结构体方法是与结构体类型相关联的函数。Go 语言通过“方法接收器”(method receiver
)来将方法与结构体关联。方法接收器可以是结构体类型的指针或值。
方法接收器
- 值接收器:方法接收器是结构体的值拷贝。
- 指针接收器:方法接收器是结构体的指针,可以修改结构体的内容。
3. 定义结构体的方法
3.1 使用值接收器
值接收器的好处是可以避免修改原始结构体,适合那些不需要修改数据的方法。
1 | package main |
3.2 使用指针接收器
指针接收器的好处是可以直接修改结构体的内容,适合需要修改结构体数据的方法。
1 | package main |
4. 值接收器与指针接收器的区别
值接收器
:方法会接收结构体的副本,修改副本不会影响原始结构体。
- 适用于方法不需要修改结构体字段的情况。
- 如果结构体较小,传递副本开销不大。
指针接收器
:方法会接收结构体的指针,方法内部修改会影响到原始结构体。
- 适用于方法需要修改结构体字段的情况。
- 如果结构体较大,使用指针可以避免拷贝开销。
5. 示例:结构体和方法结合的复杂应用
1 | package main |
6. 方法重载与接口
Go语言不像C++或Java那样支持方法重载(即相同名字的多个方法),但可以通过接口来实现类似的效果。通过定义接口类型,可以让不同类型(即使它们有相同的方法签名)在同一方法中调用。
7. 总结
- 在Go语言中,结构体的方法可以通过值接收器和指针接收器来定义。
- 值接收器不会修改结构体本身,而指针接收器可以修改结构体。
- 通常推荐使用指针接收器,特别是在方法可能会修改结构体时,或者结构体较大时,以避免不必要的拷贝。
接口
Go 语言中的接口是一个非常重要的概念,它定义了一组方法的集合,但是并不包含方法的具体实现。接口类型非常灵活,Go 语言的接口使用起来比许多其他编程语言中的接口要简单和直观。下面详细讲解一下 Go 语言接口的各个方面。
1. 接口定义
Go 语言的接口是通过 interface
关键字来定义的。接口可以包含多个方法,但接口本身并不实现这些方法。接口的定义只是声明了“哪些方法应该被实现”,但没有提供具体的实现细节。
1 | package main |
在这个例子中:
Speaker
是一个接口,包含一个Speak()
方法。Person
类型实现了Speak()
方法,所以Person
就实现了Speaker
接口。
2. 隐式实现
Go 的接口与其他语言不同,不需要显式地声明某个类型实现了某个接口。只要某个类型实现了接口中的所有方法,这个类型就自动实现了这个接口。也就是说,你不需要写类似于 type Person implements Speaker
这样的代码。
1 | type Dog struct{} |
这里,Dog
类型同样实现了 Speak()
方法,所以它也隐式地实现了 Speaker
接口。
3. 空接口 (interface{}
)
空接口是 Go 中非常特殊的接口类型。空接口没有任何方法,因此所有的类型都实现了空接口。空接口可以用来表示任何类型的数据。
1 | func Print(v interface{}) { |
4. 接口的类型断言
Go 语言提供了类型断言功能,用于从接口类型转换回具体的类型。类型断言可以检查接口的实际类型,并根据检查结果进行相应的处理。
1 | package main |
在上面的例子中,我们使用了 i.(type)
来检查接口 i
的实际类型。switch
语句允许我们根据不同的类型执行不同的代码。
5. 接口的值
接口类型的变量实际上包含了两个信息:
- 一个指向具体数据的指针
- 一个指向该数据实现方法的表(即方法集)
如果你给接口赋值一个具体的类型实例,那么接口变量就包含了该类型实例的值和该类型的方法集。
1 | package main |
6. 空接口与反射
空接口 interface{}
通常用于接受任何类型的值,结合反射包 reflect
,可以对空接口中的值进行进一步的操作和检查。反射可以用来动态地获取类型信息并执行操作。
1 | package main |
7. 接口的组合
Go 语言中的接口可以通过组合其他接口来创建更复杂的接口。你可以将多个接口的集合组合在一起,形成一个新的接口。
1 | package main |
8. 接口的使用场景
接口通常用于以下几种场景:
- 多态性:不同类型实现相同接口,允许在不同类型之间进行操作,而不关心具体类型。
- 解耦:接口使得类型间的关系更加松散,允许通过接口而不是具体实现来设计和测试代码。
- 扩展性:通过定义接口,可以为现有系统添加新的功能而不修改已有代码。
总结
Go 语言的接口非常灵活,支持隐式实现、多态、组合等特性,可以使代码更加简洁、可扩展。通过空接口,我们能够处理不同类型的数据,结合反射等技术,Go 的接口在实际开发中非常有用。
1 | package main |
1 | $ go run interfaces.go |
Embedding
Go 语言中的 Embedding(嵌入)是指在一个类型中嵌入另一个类型,常常用于实现代码复用、继承和接口组合等功能。Go 语言没有传统的类继承机制,而是通过嵌入其他类型(通常是结构体或接口)来实现类似的功能。嵌入使得一个类型能够访问另一个类型的方法和字段,但它并不会强制要求显示继承关系,这使得 Go 在设计上更简洁和灵活。
1. 结构体嵌入
结构体嵌入是 Go 中最常见的嵌入方式。通过嵌入其他结构体,一个结构体可以拥有另一个结构体的字段和方法。嵌入的结构体不需要显式声明字段名称,这样可以直接访问被嵌入结构体的字段和方法。
示例:结构体嵌入
1 | package main |
在上面的例子中,Person
结构体嵌入了 Address
结构体,因此 Person
类型拥有了 Address
类型的所有字段(City
和 State
)。可以直接通过 p.City
和 p.State
访问嵌入的字段。
2. 方法的继承
嵌入不仅仅是让一个结构体继承另一个结构体的字段,还可以让一个结构体继承另一个结构体的方法。被嵌入的结构体的方法可以被外部结构体直接调用。
示例:方法继承
1 | package main |
在这个例子中,Person
结构体继承了 Address
的方法 FullAddress()
,因此你可以通过 p.FullAddress()
来调用 Address
结构体的方法。嵌入的结构体的所有方法都会自动被嵌入到外部结构体中,无需显式继承。
3. 方法重写
在 Go 语言中,你可以通过嵌入的方式覆盖(重写)嵌入结构体的某些方法。通过这种方式,你可以在外部结构体中实现自定义的行为。
示例:方法重写
1 | package main |
在这个例子中,Dog
结构体嵌入了 Animal
结构体,并且重写了 Speak()
方法。因此,调用 d.Speak()
时,输出的是 Woof!
而不是 Animal speaks
。
4. 接口的嵌入
接口类型也可以嵌套其他接口,从而形成更复杂的接口组合。通过接口嵌入,你可以组合多个接口的行为,形成更强大的抽象。
示例:接口嵌入
1 | package main |
在这个例子中,Human
接口嵌入了 Speaker
和 Eater
接口,因此任何实现了这两个接口的方法的类型(如 Person
)也自动实现了 Human
接口。这样,你可以组合多个接口,从而使得类型具有更多的行为。
5. 匿名字段
Go 语言中的嵌入可以使用匿名字段。匿名字段是指在结构体中直接嵌入另一个类型,而不需要为其起一个名称。这样做可以简化代码,使得代码更简洁。
示例:匿名字段
1 | package main |
在这个例子中,Address
作为匿名字段直接嵌入了 Person
结构体。Person
结构体可以直接访问 Address
的字段 City
和 State
。
6. 嵌入的使用场景
- 代码复用:通过嵌入类型,可以在不继承的情况下重用另一个类型的方法和字段。
- 实现接口:通过嵌入结构体或接口,可以实现多个接口,增强类型的灵活性。
- 接口组合:通过嵌套多个接口,能够实现更复杂的接口设计。
- 简化结构体设计:匿名字段和嵌入使得结构体设计更简洁,代码更易于维护。
总结
Go 语言的嵌入(Embedding)是实现代码复用、模拟继承以及接口组合的重要手段。通过嵌入,Go 不仅避免了传统继承模型中的复杂性,同时又能够灵活地组合和扩展现有类型的功能。嵌入可以显著提高代码的简洁性和可维护性,是 Go 语言设计中的一个重要特性。
1 | package main |
1 | $ go run embedding.go |
泛型
Go语言的泛型(Generics)是在 Go 1.18 版本引入的一个重要特性,允许你编写更具通用性和复用性的代码。在没有泛型之前,Go语言的类型系统要求程序员在许多情况下编写重复的代码,而泛型使得函数、数据结构和算法能够适应不同类型,而无需手动复制代码。
下面我会简略而全面地讲解 Go 语言泛型的核心知识点。
1. 泛型基础
泛型的核心思想是让函数、类型或数据结构在编写时不指定特定的类型,而是在调用时由用户指定类型。Go通过类型参数(type parameters)来实现这一点。
在Go语言中,泛型的语法是通过 type
关键字来声明类型参数。
2. 定义泛型函数
我们可以定义一个泛型函数,接受一个类型参数,并在函数体内根据类型参数进行操作。
1 | package main |
在这个例子中,T
是一个类型参数,它可以是任何类型(由 any
关键字表示,any
是 interface{}
的别名)。Print
函数接收任何类型的值并打印它。
3. 泛型类型约束
Go语言的泛型允许为类型参数添加约束,确保类型参数符合某些条件。通过接口来实现约束。例如,我们可以要求类型参数必须是数字类型。
1 | package main |
在这个例子中,Add
函数的类型参数 T
被约束为 int
、int64
或 float64
之一。
4. 泛型数据结构
泛型不仅可以用于函数,还可以用于数据结构(如数组、切片、映射等)。通过泛型数据结构,我们可以避免为每种类型编写重复的代码。
1 | package main |
5. any
和 interface{}
在 Go 泛型中,any
是一种通用类型,表示任何类型,它是 interface{}
的别名。在 Go 1.18 版本及之后,你可以用 any
来表示可以接受任何类型的类型参数。
1 | // 两者是等价的 |
6. 泛型约束类型的组合
Go 支持组合多个约束,可以将多个接口约束组合在一起,从而使得类型参数更为灵活。
1 | package main |
7. 类型推断
在调用泛型函数时,Go会自动推断类型参数。如果没有显式指定类型,Go会根据传入的参数类型推断出类型。
1 | package main |
8. 使用泛型接口
你也可以为接口定义泛型,使得接口更加灵活和可重用。
1 | package main |
9. 总结
Go 语言的泛型使得编写更加通用和可重用的代码成为可能,它通过类型参数和约束来实现这一点。泛型能够提高代码的灵活性和可维护性,减少代码重复,但同时也需要保持类型的安全性。
泛型的核心语法和概念包括:
- 使用
type
定义类型参数。 - 使用
any
或interface{}
来表示可以接受任何类型。 - 通过类型约束限制类型参数的范围。
- 使用泛型数据结构(如栈、队列等)来实现通用的数据结构。
你可以使用泛型来优化你的代码,避免编写大量的重复逻辑,特别是在需要处理多种类型时。