Golang入门
一、基本数据类型
1、整数类型
var num3 int = 3
var num4 int8 = 4 // 占1个字节 -2^7 ~ 2^7 - 1
var num5 int16 = 5 // 占2个字节 -2^15 ~ 2^15 - 1
var num6 int32 = 6 // 占4个字节 -2^31 ~ 2^31 - 1
var num7 int64 = 7 // 占8个字节 -2^63 ~ 2^63 - 12、浮点类型
var num1 float32 = 1
var num2 float64 = 23、字符类型
转义字符
fmt.Println("aaa\nbbb") // \n 换行
fmt.Println("aaa\bbbb") // \b 退格
fmt.Println("aaa\rbbbb") // \r 光标回到本行的开头,后续输入就会替换原有的字符
fmt.Println("aaaaa\tbbbbb") // -t 制表符 8位
输出:
aaa
bbb
aabbb
bbbb
aaaaa bbbbb4、布尔类型
5、字符串类型
var s1 string = "hello golang"
fmt.Println(s1)字符串是不可变的:指的是字符串一旦定义好,其中的字符的值不能改变。
6、基本数据类型的默认值
var num1 float32
var num2 float64
var num3 int
var num4 int8
var num5 int16
var num6 int32
var num7 int64
var num8 string
var num9 byte
var num10 bool
fmt.Println(num1)
fmt.Println("float32: ", unsafe.Sizeof(num1))
fmt.Println(num2)
fmt.Println("float64: ", unsafe.Sizeof(num2))
fmt.Println(num3)
fmt.Println("int: ", unsafe.Sizeof(num3))
fmt.Println(num4)
fmt.Println("int8: ", unsafe.Sizeof(num4))
fmt.Println(num5)
fmt.Println("int16: ", unsafe.Sizeof(num5))
fmt.Println(num6)
fmt.Println("int32: ", unsafe.Sizeof(num6))
fmt.Println(num7)
fmt.Println("int64: ", unsafe.Sizeof(num7))
fmt.Println(num8)
fmt.Println("string: ", unsafe.Sizeof(num8))
fmt.Println(num9)
fmt.Println("byte: ", unsafe.Sizeof(num9))
fmt.Println(num10)
fmt.Println("bool: ", unsafe.Sizeof(num10))
输出结果:
0
float32: 4
0
float64: 8
0
int: 8
0
int8: 1
0
int16: 2
0
int32: 4
0
int64: 8
string: 16
0
byte: 1
false
bool: 17、数据类型之间的转换
7.1、基本数据类型之间的转换
var n1 int = 100
// var n2 float32 = n1 // 在这里自动转换不好使,比如显式转换
var n2 float32 = float32(n1)
fmt.Println(n1)
fmt.Println(n2)
fmt.Printf("%T\n", n1) // int n1的类型还是int类型,只是将n1的值100转为了float32而已,n1还是int类型
// 将int64转为int8的时候,编译不会出错,但是会数据溢出
var n3 int64 = 888888
var n4 int8 = int8(n3)
fmt.Println(n4) //56
var n5 int32 = 12
var n6 int64 = int64(n5) + 30 // 一定要匹配 = 左右的数据类型
fmt.Println(n5)
fmt.Println(n6)
var n7 int64 = 12
var n8 int8 = int8(n7) + 127 // 编译通过,但是结果可能会溢出
// var n9 int8 = int8(n7) + 128 // 编译不会通过
fmt.Println(n8)
// fmt.Println(n9)7.2、基本数据类型和字符串类型之间的转换
方式一、Sprintf("", )

var n1 int = 19
var s1 string = fmt.Sprintf("%d", n1)
fmt.Printf("s1对应的类型是: %T, s1 = %q \n", s1, s1)
var n2 float32 = 1.23
var s2 string = fmt.Sprintf("%f", n2)
fmt.Printf("s2对应的类型是: %T, s2 = %q \n", s2, s2)
var n3 bool = false
var s3 string = fmt.Sprintf("%t", n3)
fmt.Printf("s3对应的类型是: %T, s3 = %q \n", s3, s3)
var n4 byte = 'a'
var s4 string = fmt.Sprintf("%c", n4)
fmt.Printf("s4对应的类型是: %T, s4 = %q \n", s4, s4)方式二、strconv
二、复杂数据类型
1、指针
var num int = 18
// &符号+变量,就可以获取这个变量内存的地址
fmt.Println(&num)
// 定义一个指针变量:
// var代表要声明一个变量
// ptr 指针变量的名字
// ptr 对应的类型是:*int 是一个指针类型 ,可以理解为 指向int类型的指针
// &num 就是一个地址,是ptr变量的具体的值
var ptr *int = &num
fmt.Println(ptr)
fmt.Println("ptr本身的存储空间的值:", &ptr)
// 想获取ptr这个指针或者这个地址指向的那个数据
fmt.Printf("ptr指向的数值:%v \n", *ptr)
输出:
0xc000084048
0xc000084048
ptr本身的存储空间的值: 0xc00004c008
ptr指向的数值:18总结:
- & 取内存地址
- *根据地址取值
1.1、可以通过指针改变指向值
var num int = 10
fmt.Println(num)
var ptr *int = &num
*ptr = 20
fmt.Println(num)1.2、指针变量接收的一定是地址值
var ptr *int = num // 错误1.3、指针变量的地址不可以不匹配
var ptr *float32 = &num1.4、基本数据类型(又叫值类型)
都有对应的指针类型,形式为 * 数据类型,比如int对应的指针就是* int,float32对应的指针类型就是* float32,依次类推。
2、
三、运算符
四、流程控制
关键字
break
作用:
break用于立即终止循环(for、switch或select),并跳出当前的循环块或switch语句。- 在多层嵌套循环中,
break仅作用于它所在的最近一层循环。
使用场景:提前退出循环,如在查找某个满足条件的元素时。
例子:
for i := 0; i < 10; i++ {
if i == 5 {
break // 终止循环,当 i == 5 时跳出 for 循环
}
fmt.Println(i)
}
输出:
0
1
2
3
4continue
作用:
continue用于跳过当前循环的剩余部分,并立即进入下一次循环迭代。- 在
for循环中,当执行continue时,控制权会返回到循环的增量表达式(如果有的话)。
使用场景:跳过当前不符合条件的迭代,继续下一次迭代,例如跳过奇数或某些条件下的值。
例子:
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // 跳过本次循环的剩余部分,直接进入下一次循环
}
fmt.Println(i)
}
输出:
1
3
5
7
9goto
作用:
goto是一种无条件跳转语句,用于将控制转移到同一函数或方法中的某个标签位置。- 标签是由一个标识符加冒号 (
:) 构成的,可以放置在代码的任意位置。
使用场景:在 Go 中不推荐频繁使用 goto,因为它可能会让代码难以理解。但在某些情况下(如在嵌套循环中需要跳出多层循环),goto 可以作为一种简便的控制方式。
例子:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
goto end // 跳转到 end 标签,直接结束嵌套循环
}
fmt.Println(i, j)
}
}
end: // 标签位置
fmt.Println("跳出了嵌套循环")
输出:
0 0
0 1
0 2
1 0
跳出了嵌套循环return
作用:
return用于从函数中返回值,并结束该函数的执行。执行return后,函数不会再执行return语句后的任何代码。- 如果函数有返回值,
return语句后跟随返回的值。
使用场景:返回函数的计算结果或状态,或在特定条件下提前退出函数。
例子:
func add(a int, b int) int {
return a + b // 返回 a + b 的结果,并结束函数
}
func main() {
sum := add(3, 5)
fmt.Println(sum) // 输出: 8
}
输出:
8五、函数
1、简单函数
func 函数名 (形参列表) (返回值类型列表) {
执行语句...
return + 返回值列表
}例子:
func cal(a int, b int) int {
return a + b
}
func main() {
sum := cal(10, 20)
fmt.Println(sum)
}2、函数详解
func cal(a int, b int) (int, int) {
return a + b, a - b
}
func main() {
sum, res := cal(10, 20)
fmt.Println(sum, res)
}3、闭包
3.1、定义
闭包是指内部函数捕获并引用外部函数作用域中的变量。使用闭包时,这些外部变量的生命周期会延长,直到闭包不再引用它们为止。Go 的闭包主要通过匿名函数(func)实现。
3.2、例子
func getSum() func(int) int {
var sum int = 0
return func(num int) int {
sum += num
return sum
}
}
func main() {
f := getSum()
fmt.Println(f(1))
fmt.Println(f(2))
fmt.Println(f(3))
}3.3、原理
闭包能引用外部变量的原因在于,Go 语言会将这些变量的引用包含在闭包的环境中。闭包在创建时将变量的引用“捕获”在自身的上下文中,而不是将值复制进去。这种特性允许闭包函数在后续的调用中继续访问这些变量的最新值。
3.4、使用场景
4、defer关键字
在 Golang 中,defer 关键字用于在函数结束时执行一些代码。defer 语句的核心特点是无论函数如何结束——正常返回、遇到错误或是发生 panic——被 defer 的代码都会在函数退出前执行。因此,defer 常用于释放资源、关闭文件、解锁互斥锁等清理操作。
defer 的基本语法和用法
defer 语句在函数体内定义,需要跟随一个函数调用。当执行到 defer 语句时,Go 会记录该语句并推迟到当前函数返回时再执行。
语法:
defer functionName(args)示例:
package main
import "fmt"
func main() {
fmt.Println("Start")
defer fmt.Println("This is deferred")
fmt.Println("End")
}输出:
Start
End
This is deferred在这个例子中,defer fmt.Println("This is deferred") 被推迟到 main() 函数即将结束时执行,所以它最后输出。
defer 的执行顺序
如果在一个函数中有多个 defer 语句,这些 defer 调用会按照后进先出的顺序(LIFO)执行。这类似于栈结构的后入先出。
示例:
package main
import "fmt"
func main() {
defer fmt.Println("First")
defer fmt.Println("Second")
defer fmt.Println("Third")
}输出:
Third
Second
Firstdefer 会按顺序将所有的调用入栈,在函数退出时逆序执行这些 defer 语句。
defer 的常见用途
释放资源:在操作文件、数据库连接等资源时,
defer可以确保它们在函数结束前正确释放。gopackage main import ( "fmt" "os" ) func main() { file, err := os.Open("test.txt") if err != nil { fmt.Println("Error opening file:", err) return } defer file.Close() // 确保在函数结束时关闭文件 // 读取文件内容 // ... }解锁互斥锁:在多线程程序中使用
sync.Mutex或sync.RWMutex时,可以使用defer来确保锁在操作完成后正确解锁。govar mu sync.Mutex func criticalSection() { mu.Lock() defer mu.Unlock() // 确保在函数结束前解锁 // 执行一些需要锁保护的操作 }捕获并处理异常:在 Go 中,
defer配合recover可以捕获panic并处理,避免程序崩溃。gofunc safeDivision(a, b int) { defer func() { if r := recover(); r != nil { fmt.Println("Recovered from panic:", r) } }() fmt.Println("Result:", a/b) // 若 b 为 0,会引发 panic }
defer 的工作原理与参数评估
defer 语句会在它定义时计算参数值,但会推迟执行实际的函数调用。这意味着 defer 语句捕获的是当前的参数值。
示例:
package main
import "fmt"
func printValue(val int) {
fmt.Println("Deferred value:", val)
}
func main() {
x := 10
defer printValue(x) // 此时 x 的值是 10
x = 20
fmt.Println("Updated value:", x)
}输出:
Updated value: 20
Deferred value: 10尽管 x 在 defer 语句之后被更新为 20,但 defer 捕获的是当时的值(10),所以 printValue(10) 被推迟执行。
defer 在返回值中的应用
在有命名返回值的函数中,defer 可以修改返回值,因为 defer 语句会在返回前执行。
示例:
package main
import "fmt"
func modifyReturnValue() (result int) {
defer func() {
result += 10 // 修改命名返回值
}()
return 5
}
func main() {
fmt.Println(modifyReturnValue()) // 输出: 15
}在此例中,result 是命名返回值,函数返回值会在 defer 执行时被修改。
注意事项
- 性能:
defer虽然方便,但在高性能需求的代码中需谨慎使用。因为defer的实现相对开销较高,尤其在大量循环中使用时可能会影响性能。 - 不适合实时性要求的操作:由于
defer的延迟执行特点,不适合对实时性要求高的操作场景。 - 参数捕获机制:
defer在定义时捕获的参数可能与最终执行时的上下文不同,需注意避免误用。
总结
defer用于推迟代码执行至函数退出前,适合资源管理、异常处理等场景。defer语句遵循后进先出顺序执行。- 其参数在定义时评估,实际调用在函数退出前。
5、系统函数
5.1、字符串相关函数
5.1.1、字符串遍历
for - range
func main() {
str := "hello golang你好"
for i, value := range str {
fmt.Printf("下标:%d, 值:%c \n", i, value)
}
}- 切片:
r := []rune(str)
func main() {
str := "你好 Golang"
r := []rune(str)
for i := 0; i < len(r); i++ {
fmt.Printf("%c \n", r[i])
}
}5.1.2、字符串转整数
n, err := strconv.Atoi("66")5.1.3、整数转字符串
str = strconv.Itoa(8888)5.1.4、查找子串是否在指定的字符串中
strings.Contains("fadshjkfhakj", "fa")
例子:
flags := strings.Contains("fadshjkfhakj", "fdaa")
fmt.Println(flags)5.1.5、统计一个字符串有几个指定的子串
strings.Count("fdafadgadf","dfa")
例子:
count := string.Count("fdafag","a")
fmt.Println(count)5.1.6、不区分大小写的字符串比较
strings.EqualFold("hello", "HELLO")
例子:
flags := strings.EqualFold("hello", "HELLO")
fmt.Println(flags)5.1.7、返回子串在字符串第一次出现的索引值,如果没有返回-1
strings.Index("fdagagagag","ag")
例子:
index := strings.Index("fdagagagag","ag")
fmt.Println(index)5.1.8、字符串的替换
strings.Replace("fzdfadgadgaga", "ga", "qint", n)
例子:
str := strings.Replace("fzdfadgadgaga", "ga", "qint", -1) // 全部替换
str := strings.Replace("fzdfadgadgaga", "ga", "qint", 2) // 替换2个
fmt.Println(str)5.1.9、按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组
strings.Split("go-python-golang", "-")
例子:
arr := strings.Split("go-python-golang", "-")5.1.10、大小写切换
strings.ToLower("GO") // to 小写
strings.ToUpper("go") // to 大写5.1.11、将字符串左右两边的空格去掉
strings.TrimSpace(" go java cpp ")
例子:
str := strings.TrimSpace(" go java cpp ") // 不会去掉字符串中间的空格
fmt.Println(str)5.1.12、将左右指定的字符去掉
strings.Trim("--golang--", "-")
例子:
str := strings.Trim("--golang--", "-")
fmt.Println(str)5.1.13、判断字符串是否以指定的字符串开头
strings.HasPrefix("https://www.aqjszz.com/docs", "https")
例子
flags := strings.HasPrefix("https://www.aqjszz.com/docs", "https")
fmt.Println(flags)5.1.14、判断字符串是否以指定的字符串结尾
strings.HasSuffix("fadf.png", "jpg")5.2、日期和时间相关函数
now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05"))5.3、内置函数
5.3.1、len
len 用于获取容器类型的长度或大小。它适用于多种数据结构,包括数组、切片、字符串、映射(map)和通道(channel)。
语法:
len(v interface{}) int- 数组:返回数组的长度。
- 切片:返回切片当前包含的元素个数,而不是底层数组的容量。
- 字符串:返回字符串的字节数(注意:对于包含非 ASCII 字符的字符串,字节数可能不等于字符数)。
- 映射(map):返回映射中的键值对数量。
- 通道(channel):返回通道中排队等待接收的元素数量(适用于缓冲通道)。
示例:
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := []int{1, 2, 3}
str := "hello"
m := map[string]int{"one": 1, "two": 2}
ch := make(chan int, 5)
ch <- 1
ch <- 2
fmt.Println("数组长度:", len(arr)) // 输出: 5
fmt.Println("切片长度:", len(slice)) // 输出: 3
fmt.Println("字符串长度:", len(str)) // 输出: 5
fmt.Println("映射长度:", len(m)) // 输出: 2
fmt.Println("通道长度:", len(ch)) // 输出: 2
}5.3.2、new
new 用于分配内存,但只会返回类型的指针,并不会进行初始化。它适合用于一些简单的类型,例如数值类型、结构体等。new 返回的是指向类型的零值的指针。
语法:
new(Type) *Type- 用法:
new(T)会分配一个 T 类型的内存空间并返回指针,指针指向的值是类型 T 的零值。 new主要用于需要一个指针类型而不需要额外初始化的场景,适合少量分配和直接访问的情况。
示例:
package main
import "fmt"
func main() {
p := new(int) // 分配一个 int 类型的内存,初始值为 0
*p = 10 // 修改指针指向的值
fmt.Println(*p) // 输出: 10
s := new([]int) // 分配一个指向空切片的指针
fmt.Println(s) // 输出: &[]
}注意:new 分配的只是内存,没有初始化。对于结构体类型 new(T) 创建的指针等价于 &T{}。
5.3.3、make
make 用于初始化和分配引用类型(即:切片、映射和通道)的内存空间。make 会返回一个初始化后的值,而不是指针。
语法:
make(t Type, size ...IntegerType) Type- 切片(slice):
make([]T, len, cap)创建一个具有指定长度和容量的切片。 - 映射(map):
make(map[K]V, hint)创建一个指定键值对数量(容量提示)的映射。 - 通道(channel):
make(chan T, buffer)创建一个带缓冲区的通道。
示例:
package main
import "fmt"
func main() {
// 创建一个长度为 5 的整数切片
slice := make([]int, 5, 10)
fmt.Println("切片长度:", len(slice)) // 输出: 5
fmt.Println("切片容量:", cap(slice)) // 输出: 10
// 创建一个映射
m := make(map[string]int)
m["foo"] = 42
fmt.Println("映射:", m) // 输出: map[foo:42]
// 创建一个缓冲区大小为 3 的通道
ch := make(chan int, 3)
ch <- 1
ch <- 2
fmt.Println("通道长度:", len(ch)) // 输出: 2
}注意:make 仅用于这三种类型,因为它不仅仅是分配内存,还初始化底层数据结构的相关信息。
总结对比
| 函数 | 主要用途 | 返回值 | 适用类型 |
|---|---|---|---|
len | 获取长度或大小 | int | 数组、切片、字符串、映射、通道 |
new | 分配内存并返回指向零值的指针 | *T(类型的指针) | 值类型(基本类型、结构体等) |
make | 分配并初始化内存,返回初始化后的值 | T(初始化后的值) | 切片、映射、通道 |
六、错误处理
6.1、错误捕获机制
defer + recover
例子:
func main() {
test()
fmt.Println("main end")
}
func test() {
defer func() {
err := recover()
if err != nil {
fmt.Println("err = ", err)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println(res)
}
输出:
err = runtime error: integer divide by zero
main end6.2、自定义错误
需要调用errors包下的New函数:
func New
func New(text string) error使用字符串创建一个错误,请类比fmt包的Errorf方法,差不多可以认为是New(fmt.Sprintf(...))。
例子:
func main() {
err := test()
if err != nil {
fmt.Println("err = ", err)
}
fmt.Println("main end")
}
func test() (err error) {
num1 := 10
num2 := 0
if num2 == 0 {
return errors.New("除数不能为0")
} else {
res := num1 / num2
println("num1/num2 = ", res)
fmt.Println(res)
return nil
}
}
输出:
err = 除数不能为0
main end七、数组
7.1、命名
var 数组名 [数组大小]数组类型7.2、初始化
第一种:
var arr1 [3]int = [3]int{1,2,3}
第二种:
var arr2 = [3]int{4,5,6}
第三种:
var arr3 = [...]int{12,34,45,65,67}
第四种:
var arr4 = [...]int{2:22, 1:11, 3:33, 0:55}7.3、类型
数组长度属于数组类型。
7.4、二维数组
遍历
方式一:for循环遍历
方式二:for-range遍历
func main() {
var arr [3][3]int = [3][3]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
fmt.Println(arr)
// 方式一:for循环遍历
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ {
fmt.Printf("arr[%d][%d] = %d \t", i, j, arr[i][j])
}
fmt.Println()
}
fmt.Println("-----------------")
// 方式二:range循环遍历
for i, v := range arr {
for j, v2 := range v {
fmt.Printf("arr[%d][%d] = %d \t", i, j, v2)
}
fmt.Println()
}
}八、切片---左闭右开
切片是建立在数组之上
func main() {
var arr [6]int = [6]int{3, 5, 6, 7, 8, 9}
// 切片是构建在数组之上
var slice []int = arr[2:5]
fmt.Println(slice)
}切片(slice) 是一种动态数组。切片提供了便捷、灵活的数组操作方式,底层结构可以动态增长或缩小。
切片的底层结构
在 Go 中,切片的底层是基于数组实现的,其底层结构由一个 sliceHeader 管理,定义在 reflect 包中,包含以下三部分:
- 指针(Pointer):指向切片底层数组的起始位置。
- 长度(Length):表示切片当前的长度,即切片实际存储的元素个数。
- 容量(Capacity):表示从切片起始位置到底层数组末尾的最大可用空间。
因此,切片可以表示为以下结构:
type sliceHeader struct {
ptr *elementType // 指向底层数组的指针
length int // 当前切片长度
cap int // 当前切片的容量
}内存布局示意图
假设有以下代码:
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4]在这段代码中,arr 是一个长度为 5 的数组,而 s 是一个切片,表示 arr 中从索引 1 到 3 的部分。底层内存布局如下:
底层数组: [1, 2, 3, 4, 5]
↑ ↑
起始位置 长度为3
切片 s: 指向 arr[1](值为2),长度为3,容量为4(从arr[1]到arr[4])s.ptr指向arr[1]的位置。s.length为3,表示s包含的元素数量[2, 3, 4]。s.cap为4,因为从arr[1]开始到数组末尾一共有 4 个元素。
切片扩容
Go 的切片具有动态扩容能力,当切片使用 append 操作超出其容量时,Go 会分配一个新的、更大容量的底层数组,并将原切片的数据复制到新数组中。扩容机制一般遵循以下原则:
- 小切片(小于 1024 元素):每次扩容时,容量翻倍。
- 大切片(大于等于 1024 元素):每次扩容增加原来容量的 1/4 左右。
举例:
s := make([]int, 2, 2) // 创建一个长度为2,容量为2的切片
s = append(s, 1) // 超出容量,触发扩容在 append 操作中,由于超出容量,Go 会分配一个新的切片,长度为 3,容量为 4,并将数据 [0, 0, 1] 复制到新切片。
切片的共享与独立
在使用切片时要注意切片可能会共享同一个底层数组。例如:
arr := []int{1, 2, 3, 4, 5}
s1 := arr[1:4]
s2 := arr[2:5]
s1[1] = 10
fmt.Println(arr) // 输出:[1, 2, 10, 4, 5]在这里,s1 和 s2 都指向了 arr 的一部分,因此修改 s1 的值会影响 s2 和 arr。要独立地使用切片,可以使用 copy 函数创建切片的副本。
切片的拷贝
通过 copy 函数来实现对切片的拷贝,copy 会复制切片的内容,但不会共享底层数组。这意味着拷贝后的切片与原切片独立存储,修改其中一个切片不会影响另一个。
copy 函数的用法
copy 函数的语法为:
copy(dst, src []Type) intdst是目标切片(拷贝到的切片),src是源切片(被拷贝的切片)。copy返回复制的元素个数。它只会复制dst和src之间较小长度的部分。
示例代码
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
// 使用 copy 函数将 src 拷贝到 dst
count := copy(dst, src)
fmt.Println("拷贝的元素个数:", count) // 输出:拷贝的元素个数: 5
fmt.Println("源切片:", src) // 输出:源切片: [1 2 3 4 5]
fmt.Println("目标切片:", dst) // 输出:目标切片: [1 2 3 4 5]
// 修改目标切片
dst[0] = 10
fmt.Println("修改后的源切片:", src) // 输出:修改后的源切片: [1 2 3 4 5]
fmt.Println("修改后的目标切片:", dst) // 输出:修改后的目标切片: [10 2 3 4 5]
}结果说明
copy(dst, src)将src切片的内容逐一复制到dst切片中,两个切片的底层数据完全独立。- 修改
dst的元素不会影响到src,因为dst中的数据是新拷贝的副本。
九、映射map
Go 语言中,map 是一种内置的数据结构,用于实现键值对(key-value)存储。它可以高效地通过键快速查找、插入、和删除数据。map 是无序的,键的顺序在迭代时不保证固定。
基本语法与使用
声明和初始化
使用
make函数初始化一个map:gom := make(map[string]int) // 键是字符串类型,值是整数类型或者在声明的同时初始化:
gom := map[string]int{"apple": 1, "banana": 2, "cherry": 3}基本操作
添加或更新键值对:
gom["apple"] = 5 // 更新或添加键 "apple" 的值为 5访问元素:
govalue := m["apple"] // 获取键 "apple" 对应的值,若不存在则返回类型的零值删除键值对:
使用
delete函数从map中移除指定的键:godelete(m, "banana") // 删除键 "banana" 对应的键值对检测键是否存在:
在访问
map中的某个键时,可以使用双重赋值的形式来检查键是否存在:govalue, ok := m["apple"] if ok { fmt.Println("键存在,值为:", value) } else { fmt.Println("键不存在") }
map 的底层实现原理
Go 的 map 是基于哈希表实现的,通过哈希函数将键映射到特定的位置。每个位置可以存放一个或多个键值对,以应对哈希冲突。
哈希桶:
map将键值对分组存储在多个桶(bucket)中,每个桶中可以容纳若干个键值对。哈希冲突:
由于不同的键可能会被映射到相同的位置,Go 使用链表或开放寻址来解决冲突。在某些情况下,Go 会将冲突较多的桶再分成小桶进行分散存储。
动态扩容:
当
map中的键值对增加较多时,哈希表会自动扩容并重新分布桶,以保证查询和插入的性能。
特性与限制
无序性:
map中的元素是无序的,迭代时每次的顺序可能会不同,Go 不保证map中的键的顺序。并发安全性:
map不是并发安全的,在多线程环境下,多个 goroutine 同时读写map会导致竞态条件,通常需要使用sync.RWMutex进行加锁操作来保证安全。键的类型:
map的键可以是任何可比较的类型,比如int、string、struct(无切片、映射字段),但不能使用切片、映射、函数等不可比较的类型作为键。
示例代码
以下代码展示了 map 的基本使用:
package main
import "fmt"
func main() {
// 初始化一个 map
m := map[string]int{"apple": 1, "banana": 2, "cherry": 3}
// 添加或更新元素
m["date"] = 4
m["apple"] = 5
// 检查某个键是否存在
value, ok := m["banana"]
if ok {
fmt.Println("banana 存在,值为:", value)
} else {
fmt.Println("banana 不存在")
}
// 删除元素
delete(m, "cherry")
// 遍历 map
for key, value := range m {
fmt.Printf("%s -> %d\n", key, value)
}
}map 的零值和空 map
一个未初始化的
map的零值是nil,操作一个nilmap不会引发错误,但插入操作无效。govar m map[string]int fmt.Println(m == nil) // 输出: true
map 的使用场景
map 非常适合需要快速查找的数据结构,如:
- 统计字符或单词的出现频率
- 记录对象的映射关系(如用户 ID 到用户信息)
- 缓存查询结果以提高效率
十、面向对象
1、结构体
1. 结构体的定义与使用
结构体是用于将不同类型的数据字段组合在一起的一种数据类型,类似于其他语言的类。每个字段可以是不同的数据类型,结构体的实例可以包含所有字段的数据。
定义结构体
type Person struct {
Name string
Age int
Job string
}在上面的例子中,Person 是一个结构体类型,它有 Name、Age 和 Job 三个字段。
创建结构体实例
可以使用以下方法创建结构体的实例:
// 方式一:字段赋值
p1 := Person{Name: "Alice", Age: 30, Job: "Engineer"}
// 方式二:创建指针
p2 := &Person{Name: "Bob", Age: 25, Job: "Designer"}
// 方式三:省略字段名(不推荐)
// 这种方式取决于字段的顺序,容易出错
p3 := Person{"Charlie", 35, "Manager"}2. 访问和修改结构体字段
结构体的字段通过点运算符来访问和修改:
fmt.Println(p1.Name) // 输出:Alice
p1.Age = 31 // 修改年龄
fmt.Println(p1.Age) // 输出:313. 方法绑定与接收者
Go 没有类的概念,因此将方法绑定到结构体上来实现对象的行为。方法的接收者(receiver)定义了该方法的所属结构体。
定义方法
可以将一个方法绑定到结构体上,类似于类中的方法:
func (p Person) Greet() string {
return "Hello, my name is " + p.Name
}在这里,(p Person) 是接收者,表示该方法是 Person 结构体的一个方法。Greet 方法返回包含姓名的问候语。
使用方法
fmt.Println(p1.Greet()) // 输出: Hello, my name is Alice指针接收者 vs 值接收者
- 值接收者:方法接收结构体的副本,对副本的修改不会影响原始结构体。
- 指针接收者:方法接收结构体的指针,指针接收者可以修改结构体的原始数据。
通常,在需要修改结构体内部数据或提高性能时使用指针接收者:
func (p *Person) UpdateJob(newJob string) {
p.Job = newJob
}
p1.UpdateJob("Artist")
fmt.Println(p1.Job) // 输出: Artist4. 构造函数
Go 中没有类构造函数,但可以定义一个函数来初始化和返回结构体实例,作为构造函数的替代。
func NewPerson(name string, age int, job string) Person {
return Person{Name: name, Age: age, Job: job}
}
p4 := NewPerson("David", 28, "Developer")这里 NewPerson 函数就相当于一个构造函数,用于创建 Person 结构体实例。
5. 组合与继承
Go 不支持传统的继承,但通过组合(Composition)来实现类似的功能。结构体可以包含其他结构体,以共享字段和方法。
结构体嵌套
type Address struct {
City string
Country string
}
type Employee struct {
Person
Address
Position string
}在这里,Employee 结构体包含 Person 和 Address,可以访问这两个结构体的所有字段。
使用嵌套结构体
e := Employee{
Person: Person{Name: "Emma", Age: 26, Job: "Analyst"},
Address: Address{City: "New York", Country: "USA"},
Position: "Data Analyst",
}
fmt.Println(e.Name) // 输出: Emma
fmt.Println(e.City) // 输出: New York6. 面向对象特性总结
Go 实现面向对象的特性主要依靠结构体和方法组合实现:
- 封装:通过结构体定义属性,方法定义行为,将数据和操作封装在一个结构体中。
- 继承:使用组合而非继承来复用代码,结构体可以嵌套其他结构体。
- 多态:通过接口(
interface)来实现多态,允许不同的结构体实现相同的接口方法。
7. 内存布局与结构体
在内存中,结构体是一个连续的数据块,字段顺序与定义顺序一致。对于嵌套的结构体,内存布局也是按照组合的顺序来排列。
示例
一个内存示例如下:
type Person struct {
Name string // 8字节 (假设)
Age int // 8字节
}
type Employee struct {
Person // 嵌套的 Person,占用16字节
Position string // 8字节
}结构体 Employee 在内存中会以字段排列的顺序存储,Person 的字段在 Employee 的前半部分存储,紧接着存储 Position 字段。
2、方法
1. 什么是方法
- 方法是绑定到某个类型(通常是结构体)的函数。
- 方法通过**接收者(receiver)**来访问和操作绑定类型的实例数据。
方法的语法格式:
func (receiver Type) MethodName(parameters) returnType {
// 方法体
}receiver:方法绑定的类型的实例(可以是值类型或指针类型)。Type:绑定方法的类型。MethodName:方法名称。parameters:方法参数。returnType:返回值类型。
2. 定义方法
示例:绑定到结构体的值接收者的方法
package main
import "fmt"
// 定义结构体
type Person struct {
Name string
Age int
}
// 定义方法
func (p Person) Greet() string {
return "Hello, my name is " + p.Name
}
func main() {
p := Person{Name: "Alice", Age: 30}
fmt.Println(p.Greet()) // 输出: Hello, my name is Alice
}3. 接收者类型:值接收者 vs 指针接收者
(1) 值接收者
值接收者方法会接收结构体的一个副本,在方法中修改字段不会影响原始结构体。
func (p Person) IncrementAge() {
p.Age += 1
}
func main() {
p := Person{Name: "Alice", Age: 30}
p.IncrementAge()
fmt.Println(p.Age) // 输出: 30,值未改变
}p.Age未改变,因为IncrementAge操作的是p的副本。
(2) 指针接收者
指针接收者方法接收结构体的地址,可以修改原始结构体的字段。
func (p *Person) IncrementAge() {
p.Age += 1
}
func main() {
p := Person{Name: "Alice", Age: 30}
p.IncrementAge()
fmt.Println(p.Age) // 输出: 31
}- 这里使用指针接收者,
p.Age被正确修改。
总结
- 值接收者:不会修改原始数据,适用于只读操作或数据较小的情况。
- 指针接收者:允许修改原始数据,适用于需要修改数据或结构体较大的情况。
4. 方法与普通函数的区别
| 特性 | 方法 | 普通函数 |
|---|---|---|
| 绑定类型 | 是,绑定到特定类型 | 否,无需绑定特定类型 |
| 接收者(receiver) | 必须有 | 无 |
| 调用方式 | instance.MethodName() | FunctionName() |
示例:方法 vs 普通函数
// 方法
func (p Person) Greet() string {
return "Hello, " + p.Name
}
// 普通函数
func GreetPerson(p Person) string {
return "Hello, " + p.Name
}调用区别:
fmt.Println(p.Greet()) // 调用方法
fmt.Println(GreetPerson(p)) // 调用函数5. 方法的多态(通过接口实现)
Go 没有传统的类和继承,但通过接口(interface)实现了方法的多态特性。
示例:接口与多态
type Speaker interface {
Speak() string
}
type Cat struct{}
func (c Cat) Speak() string {
return "Meow"
}
type Dog struct{}
func (d Dog) Speak() string {
return "Woof"
}
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
c := Cat{}
d := Dog{}
MakeSound(c) // 输出: Meow
MakeSound(d) // 输出: Woof
}在这里,不同的类型 Cat 和 Dog 实现了 Speaker 接口,可以在 MakeSound 函数中使用,实现了多态。
6. 方法的重用与继承
Go 中没有传统的继承,但可以通过嵌套结构体复用方法。
示例:方法重用
type Animal struct{}
func (a Animal) Speak() string {
return "I am an animal"
}
type Bird struct {
Animal
}
func main() {
b := Bird{}
fmt.Println(b.Speak()) // 输出: I am an animal
}Bird 通过嵌套 Animal 结构体继承了 Speak 方法。
7. 方法的特殊使用:值和接口
- 方法的值接收者可以自动接受值或指针实例调用。
- 方法的指针接收者只能接受指针实例调用。
示例
func (p Person) Greet() {
fmt.Println("Hello, I am", p.Name)
}
func (p *Person) UpdateName(newName string) {
p.Name = newName
}
func main() {
p := Person{Name: "Alice"}
p.Greet() // 自动使用值调用
(&p).Greet() // 指针也可以调用值接收者的方法
(&p).UpdateName("Bob")
fmt.Println(p.Name) // 输出: Bob
}8. 方法的内存布局
- 值接收者:方法的调用会复制一份结构体,分配新的内存,方法内的修改不会影响原始值。
- 指针接收者:方法接收结构体的指针,直接操作原始内存,方法内的修改会反映到原始值上。
3、跨包访问
大写字母开头无问题,解决小写字母结构体:工厂模式
1. 包的结构与导入(Import)
Go 的代码结构是由多个包(package)组成的。每个包都是独立的代码单元,包含了相关的函数、类型和变量。Go 使用 import 关键字来引用其他包,从而实现跨包的访问。
1.1 包的结构
在 Go 中,每个源代码文件都属于某个包。每个包有一个名字,通常与包所在目录的名称相同。例如:
// file: mathutils/square.go
package mathutils
func Square(x int) int {
return x * x
}这个包的名字是 mathutils,它包含了一个名为 Square 的函数。
1.2 导入包
通过 import 关键字,Go 代码可以引入其他包,以便访问包内的内容:
// file: main.go
package main
import (
"fmt"
"path/to/mathutils" // 导入自定义的包 mathutils
)
func main() {
result := mathutils.Square(5) // 调用 mathutils 包中的 Square 函数
fmt.Println(result) // 输出: 25
}在这个例子中,main 包通过 import 引入了 mathutils 包,并且调用了 mathutils 包中的 Square 函数。
2. 可见性规则:大写字母与小写字母
Go 语言有一个非常简单的访问控制机制:大写字母开头的标识符(如函数、变量、类型)是公开的(exported),可以被其他包访问;小写字母开头的标识符是私有的(unexported),只能在包内部访问。
2.1 公共和私有标识符
- 公开标识符:以大写字母开头的标识符可以被其他包访问。
- 私有标识符:以小写字母开头的标识符只能在定义它的包内访问,其他包无法访问。
示例:公共与私有标识符
// file: mathutils/mathutils.go
package mathutils
// Public function: 可以跨包访问
func Add(a, b int) int {
return a + b
}
// Private function: 只能在 mathutils 包内访问
func subtract(a, b int) int {
return a - b
}
go复制代码// file: main.go
package main
import (
"fmt"
"path/to/mathutils"
)
func main() {
result := mathutils.Add(3, 4) // 访问 Add 函数,合法
fmt.Println(result) // 输出 7
// result := mathutils.subtract(5, 2) // 编译错误,无法访问私有函数 subtract
// fmt.Println(result)
}在上面的代码中:
Add是公开的函数,main包可以访问它。subtract是私有的函数,main包不能访问它。
3. 跨包访问类型
除了函数和变量,Go 的包还可以导出类型。对于类型的导出规则,Go 的行为与函数、变量的导出规则是一样的,都是通过首字母的大小写来判断是否可访问。
3.1 导出结构体类型
// file: person/person.go
package person
// 公共结构体类型
type Person struct {
Name string
Age int
}
// 私有字段,不能跨包访问
func (p *Person) SetName(name string) {
p.Name = name
}
go复制代码// file: main.go
package main
import (
"fmt"
"path/to/person"
)
func main() {
p := person.Person{Name: "John", Age: 30} // 访问公开的结构体
fmt.Println(p.Name, p.Age) // 输出 John 30
// p.SetName("Alice") // 访问 Person 类型的方法,合法
// p.Name = "Doe" // 编译错误,不能直接修改私有字段
}3.2 导出接口类型
Go 允许导出接口类型,跨包访问接口中的方法。例如:
// file: animal/animal.go
package animal
// 公共接口
type Animal interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct{}
func (c Cat) Speak() string {
return "Meow!"
}
go复制代码// file: main.go
package main
import (
"fmt"
"path/to/animal"
)
func main() {
var a animal.Animal
a = animal.Dog{}
fmt.Println(a.Speak()) // 输出 Woof!
a = animal.Cat{}
fmt.Println(a.Speak()) // 输出 Meow!
}4. 跨包访问常量和变量
Go 也允许在一个包中定义常量和变量,并通过 import 进行跨包访问。
// file: constants/constants.go
package constants
// 公共常量
const Pi = 3.14
// 私有常量
const privateConst = 42
go复制代码// file: main.go
package main
import (
"fmt"
"path/to/constants"
)
func main() {
fmt.Println(constants.Pi) // 输出 3.14
// fmt.Println(constants.privateConst) // 编译错误,私有常量不能访问
}5. 跨包访问的路径与模块管理
跨包访问不仅依赖于包内部的可见性规则,还依赖于 Go 模块(go mod)的管理。随着 Go 1.11 版本引入模块支持,Go 使用模块(go.mod)来管理跨包的依赖关系。通过 go.mod 文件,Go 可以确保依赖项的版本一致性,并允许对外部包的访问。
示例:go.mod
module myproject
go 1.20
require (
github.com/some/package v1.2.3
)在 go.mod 文件中指定需要的外部包以及版本号,Go 会确保所有的依赖都能正确解析并进行跨包访问。
6. 总结
Go 的跨包访问遵循以下规则:
- 包的导入:使用
import语句导入其他包。 - 访问规则:首字母大写的标识符(如函数、变量、类型)是公开的,其他包可以访问;首字母小写的标识符是私有的,只能在定义它的包中访问。
- 结构体和接口:结构体和接口类型的访问规则与函数和变量类似。
- 模块化:Go 使用模块(
go mod)管理依赖,可以轻松管理跨包依赖关系。
4、封装
1. Go 中的封装实现机制
Go 的封装主要依靠 包 和 标识符的可见性 规则来实现。具体来说,封装是通过以下几种方式实现的:
- 包:Go 的包(
package)是封装的主要单元,一个包是 Go 代码的基本组织方式,包内的数据和方法可以对外提供接口,也可以限制对外部的访问。 - 首字母大小写规则:Go 语言并没有像其他面向对象语言一样提供访问修饰符(如
public、private等)。Go 使用 首字母大写 来表示一个标识符(变量、函数、类型等)是公开的(exported),即可以被外部包访问;而 首字母小写 则表示该标识符是私有的(unexported),即只能在包内访问。
2. Go 中封装的具体应用
2.1 公开与私有标识符
在 Go 中,标识符的首字母决定了它的可见性:
- 首字母大写:标识符是公开的,其他包可以访问。
- 首字母小写:标识符是私有的,只能在定义它的包内访问。
示例:公开和私有变量、方法
// file: person/person.go
package person
// Person 结构体是公开的,因为首字母大写
type Person struct {
Name string // 公共字段,首字母大写,外部可以访问
age int // 私有字段,首字母小写,外部不能访问
}
// NewPerson 是一个工厂函数,创建并返回一个 Person 对象
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
age: age,
}
}
// SetAge 方法是公开的,允许修改 age 字段
func (p *Person) SetAge(age int) {
p.age = age
}
// GetAge 方法是公开的,允许访问 age 字段
func (p *Person) GetAge() int {
return p.age
}
// file: main.go
package main
import (
"fmt"
"path/to/person" // 引入 person 包
)
func main() {
p := person.NewPerson("John", 30)
fmt.Println(p.Name) // 输出: John (公开字段)
// fmt.Println(p.age) // 编译错误,不能直接访问 age,因为它是私有的
p.SetAge(35) // 修改 age 的值
fmt.Println(p.GetAge()) // 输出: 35 (通过公共方法访问私有字段)
}2.2 公开和私有方法
Go 通过首字母的大小写来决定方法的可见性。如果方法的首字母大写,那么它就能被包外访问;如果首字母小写,那么它只能在包内使用。
// file: car/car.go
package car
// Car 结构体
type Car struct {
brand string // 私有字段
year int // 私有字段
}
// NewCar 是公开的构造函数
func NewCar(brand string, year int) *Car {
return &Car{brand: brand, year: year}
}
// Start 是公开方法,外部包可以调用
func (c *Car) Start() string {
return "Starting " + c.brand
}
// stop 是私有方法,外部包不能调用
func (c *Car) stop() {
// 只能在包内部调用
fmt.Println("Stopping the car")
}在上面的代码中,NewCar 和 Start 是公开的,可以被其他包访问,而 stop 是私有的,只能在 car 包内部调用。
2.3 使用工厂方法和访问方法
由于 Go 没有传统的构造函数和访问控制方法(如 Java 中的 getter/setter),通常我们通过工厂方法来创建实例,并通过公共方法来访问或修改私有字段。
// file: car/car.go
package car
type Car struct {
brand string
year int
}
// NewCar 工厂方法,返回一个 Car 对象
func NewCar(brand string, year int) *Car {
return &Car{brand: brand, year: year}
}
// GetBrand 获取 brand
func (c *Car) GetBrand() string {
return c.brand
}在上面的例子中,NewCar 是公开的工厂方法,返回一个 Car 实例,外部通过公共的 GetBrand 方法来访问私有的 brand 字段。
2.4 数据封装与访问控制
封装的关键点在于 控制数据的访问。Go 的封装机制通过限制包外对私有字段和私有方法的访问来保护数据的安全性,并通过公开的接口提供必要的操作。
例如,在 Go 中,如果我们想保护某个字段的修改权限,通常会通过私有字段配合公开的修改方法(如 SetAge)来实现:
// file: person/person.go
package person
type Person struct {
name string
age int
}
// NewPerson 创建并返回一个 Person 实例
func NewPerson(name string, age int) *Person {
return &Person{name: name, age: age}
}
// SetAge 用来修改私有字段 age 的值
func (p *Person) SetAge(age int) {
if age > 0 {
p.age = age
}
}
// GetAge 用来访问私有字段 age 的值
func (p *Person) GetAge() int {
return p.age
}通过这种方式,age 字段是私有的,外部无法直接访问或修改它,只能通过 SetAge 方法进行操作,保证了数据的封装性和安全性。
3. Go 中的封装优缺点
优点
- 简洁:Go 的封装机制非常简洁。通过首字母的大小写控制访问权限,避免了复杂的访问修饰符(如 Java 中的
public、private、protected)。 - 灵活性:Go 的封装机制为数据提供了良好的保护,但也不妨碍必要的外部访问,尤其是通过公开的方法。
- 组合优于继承:Go 提倡通过组合来实现复用,封装与组合相结合,使得 Go 在实现复杂功能时更加灵活。
缺点
- 没有传统的访问修饰符:Go 中的封装机制虽然简洁,但在一些复杂场景中可能会显得不够细粒度。例如,无法像 Java 一样使用
private、protected、public来细致控制访问权限。
4. 总结
在 Go 语言中,封装是通过包和标识符的可见性来实现的:
- 包:包是代码的组织单元,可以封装一组相关的函数、类型和变量。
- 可见性:通过标识符首字母的大小写决定访问权限,首字母大写表示公开,首字母小写表示私有。
- 方法和字段的封装:通过公开的接口(如方法)来操作私有数据,确保数据的安全性和一致性。
5、继承
1. Go 中的继承(组合)
Go 的设计哲学是 组合优于继承,这意味着在 Go 中,你不会直接创建一个类来继承另一个类,而是将一个结构体嵌套(组合)到另一个结构体中,从而实现类似继承的功能。
Go 语言通过 结构体嵌套(struct embedding) 来实现类似继承的效果。通过嵌套一个类型到另一个类型的结构体中,外部结构体可以访问嵌套结构体的方法和字段。
2. Go 的继承(组合)示例
假设我们有一个 Animal 结构体,它有一个方法 Speak,然后我们定义一个 Dog 结构体,它“继承”了 Animal 的功能。
示例:通过组合实现继承
package main
import "fmt"
// Animal 结构体,定义了一个方法
type Animal struct {
Name string
}
// Speak 方法是 Animal 类型的方法
func (a *Animal) Speak() {
fmt.Println(a.Name + " makes a sound")
}
// Dog 结构体,嵌入 Animal 结构体
type Dog struct {
Animal // 通过嵌入 Animal 类型,Dog "继承" 了 Animal 的字段和方法
Breed string
}
// Dog 的 Speak 方法覆盖了 Animal 的 Speak 方法
func (d *Dog) Speak() {
fmt.Println(d.Name + " barks")
}
func main() {
dog := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
}
// Dog 调用自己的 Speak 方法
dog.Speak() // 输出: Buddy barks
// 访问 Animal 中的字段和方法
fmt.Println(dog.Name) // 输出: Buddy
}3. 解释
在上面的示例中,Dog 结构体嵌套了 Animal 结构体,从而使 Dog 获得了 Animal 结构体的所有字段和方法。这就是 Go 语言中常说的 结构体嵌套 或 组合,它让 Dog 结构体“继承”了 Animal 的行为。
Animal结构体包含字段Name和方法Speak。Dog结构体通过嵌套Animal类型来实现继承,从而可以访问Animal的字段和方法。Dog自己也有一个Speak方法,这覆盖了Animal的Speak方法,使得Dog有自己独特的Speak行为。
4. 嵌入式类型的行为
当一个结构体嵌入另一个结构体时,嵌入的结构体的字段和方法会变得可访问。这种方式在 Go 中被称为 结构体的匿名字段(anonymous field)。
- 如果嵌入的结构体是一个 匿名字段(即只传递类型而没有显式指定字段名),外部结构体可以直接访问嵌入结构体的方法和字段,而不需要通过字段名来访问。
示例:匿名字段
package main
import "fmt"
// Animal 结构体
type Animal struct {
Name string
}
// Speak 方法是 Animal 类型的方法
func (a *Animal) Speak() {
fmt.Println(a.Name + " makes a sound")
}
// Dog 结构体,Animal 作为匿名字段
type Dog struct {
Animal // Animal 被作为匿名字段嵌入
Breed string
}
func main() {
dog := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
}
// 直接访问 Animal 的方法
dog.Speak() // 输出: Buddy makes a sound
}在这个例子中,Dog 结构体通过匿名字段 Animal 继承了 Animal 的 Name 和 Speak 方法。你可以直接通过 dog.Speak() 来调用 Animal 的方法,而不需要通过 dog.Animal.Speak()。
5. 方法覆盖(Override)与多态
Go 支持通过 方法覆盖(method overriding)来实现不同类型的多态。当子类型(例如 Dog)定义了与父类型(例如 Animal)相同名称和签名的方法时,子类型的方法会覆盖父类型的方法。
示例:方法覆盖
package main
import "fmt"
// Animal 结构体
type Animal struct {
Name string
}
// Speak 方法是 Animal 类型的方法
func (a *Animal) Speak() {
fmt.Println(a.Name + " makes a sound")
}
// Dog 结构体,嵌入 Animal 结构体
type Dog struct {
Animal
Breed string
}
// Dog 的 Speak 方法覆盖了 Animal 的 Speak 方法
func (d *Dog) Speak() {
fmt.Println(d.Name + " barks")
}
func main() {
animal := Animal{Name: "Generic Animal"}
dog := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
}
animal.Speak() // 输出: Generic Animal makes a sound
dog.Speak() // 输出: Buddy barks
}在上面的代码中,Dog 结构体覆盖了 Animal 的 Speak 方法。因此,在调用 dog.Speak() 时,会执行 Dog 类型的 Speak 方法,而不是 Animal 类型的 Speak 方法。
6. Go 的继承与传统 OOP 语言的比较
- 没有显式的继承关键字:在传统的面向对象编程语言中,继承是通过关键字(如
extends或implements)来明确声明的,而 Go 语言通过结构体嵌套和方法覆盖来实现继承的效果,没有传统的继承关键字。 - 组合优于继承:Go 更提倡使用组合来实现复用和扩展,而不是通过继承来扩展功能。通过组合,你可以更灵活地将多个结构体结合在一起,而不必担心复杂的继承层次结构。
- 多态:Go 语言通过接口(
interface)和方法覆盖来支持多态,而不像传统的 OOP 语言那样通过继承来实现多态。
注意事项:
6、接口
1. 接口的定义
在 Go 中,接口定义了一组方法的集合。当某个类型实现了这些方法时,Go 语言认为该类型实现了接口。
1.1 基本接口定义
package main
import "fmt"
// 定义一个接口
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
type Cat struct {
Name string
}
// Dog 实现了 Speaker 接口的 Speak 方法
func (d Dog) Speak() string {
return d.Name + " barks"
}
// Cat 实现了 Speaker 接口的 Speak 方法
func (c Cat) Speak() string {
return c.Name + " meows"
}
func main() {
var s Speaker
// 创建 Dog 和 Cat 类型的实例
s = Dog{Name: "Buddy"}
fmt.Println(s.Speak()) // 输出: Buddy barks
s = Cat{Name: "Kitty"}
fmt.Println(s.Speak()) // 输出: Kitty meows
}在上面的代码中,我们定义了一个 Speaker 接口,该接口要求实现一个 Speak() 方法。Dog 和 Cat 类型分别实现了 Speak() 方法,因此它们都隐式实现了 Speaker 接口。我们可以将 Dog 或 Cat 类型的实例赋值给 Speaker 类型的变量,从而使用它们的 Speak() 方法。
2. 接口的隐式实现
Go 语言的接口与其他面向对象语言(如 Java 或 C++)不同,它是 隐式实现 的。也就是说,你不需要显式地声明某个类型实现了接口,只要类型实现了接口中规定的所有方法,Go 就认为该类型实现了这个接口。
2.1 示例:隐式实现接口
package main
import "fmt"
// 定义接口
type Speaker interface {
Speak() string
}
// 定义一个类型 Dog
type Dog struct {
Name string
}
// Dog 实现了 Speak 方法
func (d Dog) Speak() string {
return d.Name + " barks"
}
func main() {
var s Speaker
// Dog 类型实例实现了 Speaker 接口
s = Dog{Name: "Buddy"}
fmt.Println(s.Speak()) // 输出: Buddy barks
}这里,Dog 类型实现了 Speak() 方法,所以它隐式地实现了 Speaker 接口,Go 编译器会自动推断出这个实现。因此,我们不需要显式声明 Dog 实现了 Speaker 接口。
3. 空接口(interface{})
Go 语言的 空接口(interface{})是没有任何方法的接口,任何类型都可以实现空接口。空接口可以用来接收任何类型的值,常用于不确定类型的场景,比如容器、JSON 解析等。
3.1 空接口示例
package main
import "fmt"
// 定义空接口
func printValue(i interface{}) {
fmt.Println(i)
}
func main() {
printValue(42) // 输出: 42
printValue("Hello Go") // 输出: Hello Go
printValue([]int{1, 2}) // 输出: [1 2]
}在上面的例子中,interface{} 是一个空接口,它可以接受任何类型的参数,因此 printValue() 可以接收任何类型的值并打印。
4. 类型断言
Go 语言的接口支持类型断言,它可以从一个接口类型的变量中提取出具体的动态类型。类型断言用于确定接口类型的实际值类型,并将其转换为该类型。
4.1 类型断言示例
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " barks"
}
func main() {
var s Speaker = Dog{Name: "Buddy"}
// 类型断言
if d, ok := s.(Dog); ok {
fmt.Println("Dog:", d.Name) // 输出: Dog: Buddy
} else {
fmt.Println("Not a Dog")
}
// 尝试断言为一个不同的类型
if c, ok := s.(string); ok {
fmt.Println("String:", c)
} else {
fmt.Println("Not a String") // 输出: Not a String
}
}在这个示例中,我们将 s 断言为 Dog 类型并进行检查。如果 s 实际上是 Dog 类型,那么 d 会是 Dog 类型的值,否则断言失败,ok 会是 false。
5. 接口的零值
接口类型的零值是 nil,这意味着如果一个接口没有被赋值(即没有类型和方法实现),它的值为 nil。如果你尝试调用一个为 nil 的接口的某个方法,会导致运行时错误。
5.1 接口零值示例
package main
import "fmt"
type Speaker interface {
Speak() string
}
func main() {
var s Speaker // 这里 s 是一个空接口,值为 nil
// 会导致运行时错误,因为 s 是 nil
fmt.Println(s.Speak()) // panic: runtime error: invalid memory address or nil pointer dereference
}在这个示例中,接口 s 的零值是 nil,如果调用其方法 Speak(),会发生运行时错误,因为接口没有绑定任何类型的实现。
6. 接口的多态
接口的多态性体现在同一个接口类型可以持有不同类型的实例,并且可以通过该接口调用它们的实现方法。Go 语言通过接口的这种方式实现了多态。
6.1 接口多态示例
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " barks"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return c.Name + " meows"
}
func introduce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
d := Dog{Name: "Buddy"}
c := Cat{Name: "Kitty"}
introduce(d) // 输出: Buddy barks
introduce(c) // 输出: Kitty meows
}在这个示例中,Dog 和 Cat 类型都实现了 Speaker 接口。函数 introduce 接受一个 Speaker 接口类型的参数,我们可以传递 Dog 或 Cat 类型的实例,Go 会根据实际的类型调用相应的 Speak 方法,这就是接口的多态性。
7、多态
细节:多态数组
1. 什么是多态?
多态(Polymorphism)是指不同类型的对象对相同消息作出响应的能力。在面向对象编程中,多态通常指通过父类引用调用子类的方法。Go 语言中的多态并不依赖于继承,而是通过接口来实现。
具体来说,多态的基本含义是同一个接口类型,可以接受不同类型的实例,并通过接口调用不同类型的具体实现。这种特性使得程序具有更多的灵活性和扩展性。
2. Go 中如何实现多态?
Go 语言的多态是通过 接口(interface) 来实现的。接口定义了一个或多个方法的集合,任何类型只要实现了接口中的所有方法,就被认为是实现了该接口。
Go 语言与传统面向对象语言(如 Java、C++)不同,它没有显式的继承机制,而是通过接口的隐式实现来实现多态性。
3. Go 中接口的作用
接口定义了方法的集合,而任何实现了该接口的类型都可以被视为该接口的类型。关键在于 隐式实现,只要一个类型实现了接口所要求的所有方法,Go 语言就会自动认为这个类型实现了该接口。
3.1 接口的定义
package main
import "fmt"
// 定义一个接口
type Speaker interface {
Speak() string
}
// 定义两个类型 Dog 和 Cat
type Dog struct {
Name string
}
type Cat struct {
Name string
}
// Dog 类型实现了 Speaker 接口的 Speak 方法
func (d Dog) Speak() string {
return d.Name + " barks"
}
// Cat 类型实现了 Speaker 接口的 Speak 方法
func (c Cat) Speak() string {
return c.Name + " meows"
}
func main() {
var s Speaker
// Dog 类型实例实现了 Speaker 接口
s = Dog{Name: "Buddy"}
fmt.Println(s.Speak()) // 输出: Buddy barks
// Cat 类型实例也实现了 Speaker 接口
s = Cat{Name: "Kitty"}
fmt.Println(s.Speak()) // 输出: Kitty meows
}在这个例子中,Dog 和 Cat 都实现了 Speak() 方法,因此它们都隐式实现了 Speaker 接口。在 main 函数中,变量 s 是 Speaker 类型,可以保存任何实现了 Speak() 方法的类型实例。这里,s 先被赋值为 Dog 类型的实例,再赋值为 Cat 类型的实例。无论 s 存储的是 Dog 还是 Cat,都能调用 Speak() 方法,实现了接口的多态。
4. Go 中接口的多态性
通过接口,可以在运行时通过同一个接口类型的变量来操作不同类型的实例,从而实现多态。不同类型(如 Dog 和 Cat)通过实现相同的接口方法,使得同一个接口类型的变量可以存储不同类型的值,并且可以通过调用接口方法来执行不同的操作。
4.1 通过接口实现多态
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
type Cat struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " barks"
}
func (c Cat) Speak() string {
return c.Name + " meows"
}
func introduce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
d := Dog{Name: "Buddy"}
c := Cat{Name: "Kitty"}
// 通过同一个接口类型变量 s 来操作不同类型的实例
introduce(d) // 输出: Buddy barks
introduce(c) // 输出: Kitty meows
}在上述代码中,introduce 函数接收一个 Speaker 类型的接口作为参数,Dog 和 Cat 都实现了 Speaker 接口,因此我们可以将 Dog 或 Cat 类型的实例传递给 introduce 函数。introduce 函数通过接口调用不同类型的 Speak 方法,实现了多态性。
5. Go 中的空接口(interface{})与多态
Go 中的空接口 interface{} 是一个没有任何方法签名的接口。由于所有类型都至少实现了空接口,所以空接口可以接受任何类型的值。空接口通常用于处理未知类型的数据,比如在通用的函数中使用。
5.1 空接口的多态示例
package main
import "fmt"
func printValue(i interface{}) {
fmt.Println(i)
}
func main() {
printValue(42) // 输出: 42
printValue("Hello Go") // 输出: Hello Go
printValue([]int{1, 2}) // 输出: [1 2]
}在这个例子中,printValue 函数接受一个 interface{} 类型的参数,可以传递任何类型的值给该函数,因此实现了广泛的多态性。无论传入的是 int、string 还是 slice,printValue 都能接受并打印出来。
6. 接口与类型断言
Go 的接口提供了类型断言功能,允许我们在运行时将接口类型转换为具体的类型。通过类型断言,我们可以提取出接口变量中的具体类型,并对其进行操作。
6.1 类型断言示例
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " barks"
}
func main() {
var s Speaker = Dog{Name: "Buddy"}
// 类型断言:将 Speaker 类型转换为 Dog 类型
if d, ok := s.(Dog); ok {
fmt.Println("Dog name:", d.Name) // 输出: Dog name: Buddy
} else {
fmt.Println("Not a Dog")
}
}在这个示例中,类型断言 s.(Dog) 尝试将 Speaker 接口变量 s 转换为 Dog 类型。如果 s 是 Dog 类型,断言成功,ok 为 true,我们可以访问 Dog 类型的字段;否则,ok 为 false,表示类型转换失败。
8、断言
在 Go 语言中,断言(Assertion)主要是指 类型断言(Type Assertion),它允许我们在运行时检查和转换一个接口类型的值为具体的类型。
Go 语言中的类型断言用于获取接口类型的具体值或确认接口类型的具体类型。它提供了一种方式,让我们可以从接口类型中“提取”出具体类型的值。
1. 类型断言的语法
类型断言的基本语法是:
v := x.(T)其中:
x是一个接口类型的值(它可以是任何实现了某个接口的类型)。T是你期望从接口类型x中提取出来的具体类型。v是你断言的结果,类型为T。
这意味着你试图将 x 这个接口类型的值断言为 T 类型,如果成功,v 就是 T 类型的值。如果失败,将会发生 panic。
2. 类型断言的两种形式
Go 中的类型断言有两种形式:
2.1 单值形式(直接断言)
这种方式直接获取接口中的具体值,如果断言失败会导致程序抛出 panic。
var x interface{} = 42 // 这里 x 是一个 interface{}
v := x.(int) // 断言 x 是 int 类型
fmt.Println(v) // 输出: 42如果类型不匹配,会触发 panic:
var x interface{} = 42
v := x.(string) // panic: interface conversion: interface {} is int, not string
fmt.Println(v)2.2 双值形式(带检查的断言)
这种方式在进行类型断言时,除了返回断言的结果值,还会返回一个 bool 值,表示类型断言是否成功。
var x interface{} = 42
v, ok := x.(int) // ok 为 true,表示 x 是 int 类型
fmt.Println(v, ok) // 输出: 42 true
v2, ok := x.(string) // ok 为 false,表示 x 不是 string 类型
fmt.Println(v2, ok) // 输出: 0 false这种方式不会发生 panic,而是通过 ok 来表示断言是否成功。
3. 类型断言的使用场景
3.1 断言接口类型
最常见的应用是判断一个接口值的实际类型,然后对它进行具体操作。假设你有一个 interface{} 类型的变量,它可以持有任何类型的值。你可以通过类型断言将它转换为特定类型。
package main
import "fmt"
func printValue(i interface{}) {
switch v := i.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
case bool:
fmt.Println("bool:", v)
default:
fmt.Println("unknown type")
}
}
func main() {
printValue(42) // 输出: int: 42
printValue("Hello") // 输出: string: Hello
printValue(true) // 输出: bool: true
printValue(3.14) // 输出: unknown type
}在上面的代码中,使用了 Go 的 type switch 来对接口类型进行断言。通过 switch 可以方便地检查接口类型并进行相应的处理。
3.2 判断接口是否实现了某个方法
接口断言也可以用来判断某个类型是否实现了某个接口。
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " barks"
}
func main() {
var s Speaker
s = Dog{Name: "Buddy"}
// 判断 s 是否实现了 Speaker 接口
if _, ok := s.(Speaker); ok {
fmt.Println("s implements Speaker")
} else {
fmt.Println("s does not implement Speaker")
}
}这里通过类型断言 s.(Speaker) 来检查 s 是否实现了 Speaker 接口。
3.3 处理空接口
Go 的空接口 interface{} 可以接受任何类型的值。空接口常用于函数中接受任何类型的数据,但为了对数据进行处理,通常需要使用类型断言来提取具体的类型。
package main
import "fmt"
func printDetails(i interface{}) {
switch v := i.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
case bool:
fmt.Println("Boolean:", v)
default:
fmt.Println("Unknown type")
}
}
func main() {
printDetails(100) // 输出: Integer: 100
printDetails("Hello") // 输出: String: Hello
printDetails(true) // 输出: Boolean: true
printDetails(3.14) // 输出: Unknown type
}4. 类型断言的注意事项
4.1 类型不匹配时的 panic
如果你直接使用单值形式的类型断言,而且接口中的实际值类型与断言类型不匹配,程序会触发 panic。
var x interface{} = "hello"
v := x.(int) // panic: interface conversion: interface {} is string, not int为了避免这种情况,通常使用双值形式的类型断言来进行检查。
4.2 类型断言与 nil 值
对于接口值为 nil 的情况,类型断言有一些特别的行为:
- 如果接口值本身是
nil,则断言任何类型都会失败,并且ok为false。 - 如果接口值不为
nil,但它持有的具体值是nil,类型断言成功,但它返回的是具体类型的nil值。
var x interface{} = nil
v, ok := x.(int)
fmt.Println(v, ok) // 输出: 0 false
var y interface{} = (*int)(nil) // y 持有一个指向 int 的 nil 指针
v2, ok2 := y.(*int)
fmt.Println(v2, ok2) // 输出: <nil> true5. 总结
- 类型断言 是 Go 语言的一种机制,用于在运行时将一个接口类型的变量转换为具体类型的值。
- 类型断言有两种形式:单值形式(直接断言)和双值形式(带有
ok的断言,安全地检查断言是否成功)。 - 通过类型断言,Go 提供了类似于传统面向对象语言中的多态和动态类型的能力,可以在运行时检查和操作不同的类型。
- 使用
type switch可以简化对多种类型的判断和处理。 - 类型断言是 Go 语言强大的接口机制的重要组成部分,尤其在处理空接口和不同类型数据时非常有用。
十一、文件操作
十二、协程和管道
基本概念
- 并发(Concurrency):并发是指程序中多个操作可以同时进行。在Go中,goroutine使得并发执行变得简单。
- 并行(Parallelism):并行是指程序中多个操作同时在多个CPU核心上执行。并发不一定意味着并行,但并行是并发的一种形式。
- goroutine:goroutine是Go语言中的并发执行单元,它比传统的线程更轻量级,开销更小。
1、协程
Goroutine 是 Go 语言中的一种轻量级线程,由 Go 运行时管理。每个 Goroutine 都由 Go 运行时的调度器(scheduler)调度执行,而不像操作系统的线程一样由操作系统直接调度。Goroutine 允许我们在程序中并发地执行多个任务。
1.1、协程的创建
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("Hello!")
}
}
func main() {
go sayHello() // 启动一个协程
for i := 0; i < 5; i++ {
time.Sleep(150 * time.Millisecond)
fmt.Println("Hi!")
}
}1.2、Goroutine 的特性
- 轻量性:相比于操作系统线程,Goroutine 更加轻量。每个 Goroutine 的栈空间非常小,通常只有 2KB 左右,而操作系统线程的栈通常需要几 MB 的内存。Goroutine 会在运行时动态扩展和收缩栈空间,因此可以更高效地使用内存。
- 并发性:Goroutine 通过 Go 运行时的调度器进行调度,多个 Goroutine 可能被映射到少数几个操作系统线程上执行。调度器负责将 Goroutine 分配到可用的 CPU 核心上执行,实现高效的并发。
- 非阻塞执行:当启动一个 Goroutine 后,主程序会继续执行,而不会等待该 Goroutine 执行完毕。换句话说,
go关键字会让指定的函数以并发的方式执行,不会阻塞主程序。 - 调度机制:Go 运行时的调度器使用的是 M:N 调度模型(即多个 Goroutine 运行在多个操作系统线程上)。Go 调度器通过抢占式调度和协作式调度来管理 Goroutine 的执行。这个调度模型是 Go 并发模型的核心。
示例:启动多个 Goroutine
通过 go 关键字,我们可以启动多个 Goroutine 来并发执行多个任务。以下是一个例子:
package main
import "fmt"
func task(id int) {
fmt.Printf("Task %d is running\n", id)
}
func main() {
// 启动多个 Goroutine
for i := 1; i <= 3; i++ {
go task(i)
}
// 为了让 Goroutine 执行完毕,主线程等待
fmt.Scanln() // 阻塞主线程,等待输入
}在这个例子中,main 函数启动了三个 Goroutine,每个 Goroutine 执行 task 函数。fmt.Scanln() 用来阻塞主线程,确保所有的 Goroutine 都有机会执行。
为什么 fmt.Scanln() 是必要的?
fmt.Scanln() 用来阻塞主程序,保证 Goroutine 有足够的时间执行。在实际应用中,Goroutine 通常会与其他同步机制配合使用(例如,使用 Channel 或 WaitGroup)来等待所有任务完成。
1.3、 协程的调度机制
M:N调度模型
Go运行时采用M:N调度模型,其中:
- M 表示操作系统线程。
- N 表示协程(goroutine)。
这种模型允许多个协程在少数几个操作系统线程上多路复用,从而提高资源利用率和并发性能。当某个协程阻塞时,调度器会将其挂起,并将其他就绪的协程调度到当前线程上执行,从而避免线程阻塞。
调度器的工作原理
Go的调度器负责将协程分配到可用的操作系统线程上执行。调度器会根据协程的状态(如就绪、运行、阻塞等)进行动态调度。当某个协程阻塞时(例如等待I/O操作),调度器会将其挂起,并将其他就绪的协程调度到当前线程上执行。
1.4、主死从随
在 Go 中,协程(goroutine)是非常轻量的线程,主程序中的主协程并不会因为启动了其他协程而阻塞。主协程结束后,程序就会退出,无论其他协程是否执行完毕。因此,通常我们需要确保所有协程都执行完成后,主程序才能结束。
主协程死亡后行为的随机性
当主协程提前结束时,其他协程的行为会有一定的 随机性,主要体现在以下几个方面:
- 主协程提前退出:如果主协程(通常是
main函数)没有等到其他协程完成就退出,Go 程序就会结束,所有其他正在运行的协程会被强制终止。 - 依赖调度器的随机性:Go 调度器(runtime scheduler)会调度并运行所有的协程,但调度顺序和执行时长并不能完全预知。主协程结束后,由于协程调度是并发进行的,可能会在不同的时间点终止,造成某些协程可能还没有完全执行完就被中止。
- 内存和资源的随机释放:如果主协程过早结束,它也可能会导致程序退出时对资源的释放不完全。具体来说,由于内存和资源的管理可能依赖于某些延迟释放机制(如垃圾回收、文件关闭等),所以如果主程序过早退出,有些资源可能还没有被正确释放,造成某些协程的执行受到影响。
示例:主协程死亡导致子协程随机执行
考虑以下代码:
package main
import (
"fmt"
"time"
)
func task(id int) {
fmt.Printf("Task %d started\n", id)
time.Sleep(time.Second * 2)
fmt.Printf("Task %d finished\n", id)
}
func main() {
// 启动多个 Goroutine
go task(1)
go task(2)
go task(3)
// 主协程提前退出
fmt.Println("Main goroutine is exiting!")
}运行结果(每次可能不同):
Main goroutine is exiting!
Task 1 started
Task 3 started
Task 2 started或者:
Main goroutine is exiting!
Task 1 started
Task 2 started
Task 3 started
Task 1 finished
Task 2 finished
Task 3 finished分析:
- 随机性:在这个例子中,由于
main协程提前退出,其他协程的执行顺序是不确定的,而且有时它们甚至没有完全执行完。Go 语言的调度器会在main协程退出后随机地调度正在执行的其他协程,这种行为可能在每次运行时表现出不同的结果。 - 提前退出:在没有同步机制(如
WaitGroup或Channel)的情况下,主协程提前退出时,其他协程仍然可以继续执行,但如果主协程在其还没有完成之前就退出了,程序会强制退出,导致所有正在运行的协程被中止。
解决方案:使用 sync.WaitGroup
为了确保主协程在所有子协程执行完毕后退出,我们可以使用 sync.WaitGroup 来同步所有 Goroutine 的执行。
package main
import (
"fmt"
"sync"
"time"
)
func task(id int, wg *sync.WaitGroup) {
defer wg.Done() // 在函数退出时调用 Done 来通知 WaitGroup
fmt.Printf("Task %d started\n", id)
time.Sleep(time.Second * 2)
fmt.Printf("Task %d finished\n", id)
}
func main() {
var wg sync.WaitGroup
// 启动多个 Goroutine
for i := 1; i <= 3; i++ {
wg.Add(1) // 每启动一个协程,增加一个计数
go task(i, &wg)
}
// 等待所有 Goroutine 完成
wg.Wait()
fmt.Println("Main goroutine is exiting!")
}运行结果(确保子协程都执行完毕):
Task 1 started
Task 2 started
Task 3 started
Task 1 finished
Task 2 finished
Task 3 finished
Main goroutine is exiting!分析:
- 在这个改进后的版本中,
sync.WaitGroup保证了main函数在所有协程执行完毕之前不会退出。main函数会在调用wg.Wait()时阻塞,直到所有 Goroutine 调用wg.Done()表示它们已经执行完成。 - 这样,无论主协程何时退出,所有的子协程都能正确地执行完毕。
1.5、锁
1.5.1、互斥锁
互斥锁是最常见的锁机制之一,通常用来保护共享资源,防止多个协程同时访问这些资源,造成数据竞争或不一致的问题。
sync.Mutex
- 作用:
sync.Mutex提供了一个基本的锁机制,通过Lock和Unlock方法来显式地加锁和解锁。 - 应用场景:当多个协程需要对共享资源进行修改时,可以使用互斥锁来保证每次只有一个协程能够访问该资源。
示例:使用 sync.Mutex 保护共享资源
package main
import (
"fmt"
"sync"
)
var (
counter int
mutex sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mutex.Lock() // 获取锁
counter++
mutex.Unlock() // 释放锁
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait() // 等待所有 goroutine 执行完
fmt.Println("Final counter:", counter)
}解释:
mutex.Lock()和mutex.Unlock()用来保护对counter变量的访问,确保每次只有一个协程能修改它。sync.WaitGroup用来等待所有的协程完成。
1.5.2、读写锁
读写锁允许多个读操作并发执行,但写操作必须是独占的。这意味着,如果有一个写操作,所有的读操作都会被阻塞,直到写操作完成。
sync.RWMutex
- 作用:
sync.RWMutex提供了RLock(读锁)和Lock(写锁)两种锁操作。RLock:允许多个协程同时读取共享资源,但写操作时会阻塞。Lock:写锁会阻塞所有读操作和其他写操作。
- 应用场景:适用于读多写少的场景,例如缓存、配置管理等。
示例:使用 sync.RWMutex
package main
import (
"fmt"
"sync"
)
var (
data int
rwMutex sync.RWMutex
)
func read(wg *sync.WaitGroup) {
defer wg.Done()
rwMutex.RLock() // 获取读锁
fmt.Println("Reading:", data)
rwMutex.RUnlock() // 释放读锁
}
func write(wg *sync.WaitGroup, value int) {
defer wg.Done()
rwMutex.Lock() // 获取写锁
data = value
fmt.Println("Writing:", value)
rwMutex.Unlock() // 释放写锁
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go read(&wg)
}
for i := 0; i < 5; i++ {
wg.Add(1)
go write(&wg, i)
}
wg.Wait()
}解释:
rwMutex.RLock()用于获取读锁,允许多个协程同时读取数据。rwMutex.Lock()用于获取写锁,写操作会阻塞所有读操作和其他写操作。sync.WaitGroup用来等待所有的协程完成。
1.5.3、原子操作(Atomic Operations)
Go 提供了 sync/atomic 包,允许我们对整数值等进行原子操作,而无需使用互斥锁。这对于性能要求较高的场景非常有用,因为原子操作是无锁的。
示例:使用原子操作增加计数
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var counter int64
func increment(wg *sync.WaitGroup) {
defer wg.Done()
atomic.AddInt64(&counter, 1) // 原子增加
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final counter:", counter)
}解释:
atomic.AddInt64是一个原子操作,它直接对counter进行加1操作,避免了使用锁的开销。sync.WaitGroup用来等待所有的协程完成。
1.5.3、 锁的竞争与死锁
在 Go 中,锁竞争(Lock Contention)和死锁(Deadlock)是并发编程中常见的问题。
- 锁竞争:多个协程争夺同一个锁时,可能会影响程序性能。如果多个协程频繁获取和释放锁,可能会导致上下文切换和 CPU 资源浪费。
- 死锁:多个协程在等待彼此释放锁时,程序就会进入死锁状态,导致所有协程无法继续执行。死锁通常是由于锁获取顺序不一致或者锁的使用不当引起的。
示例:死锁示例
package main
import "sync"
var mutex1, mutex2 sync.Mutex
func deadlock() {
mutex1.Lock()
mutex2.Lock() // 死锁:两个锁获取顺序相反
}
func main() {
go deadlock()
mutex1.Lock()
mutex2.Lock()
}解决死锁的方法:
- 避免嵌套锁:尽量减少多个锁的嵌套,可以考虑使用
sync.RWMutex或其他更高效的同步机制。 - 锁的获取顺序:确保所有协程获取锁的顺序一致,避免循环等待。
2、管道
2.1、基础
Go 语言提供了 Channel 来在 Goroutine 之间传递数据,从而实现 Goroutine 之间的通信和同步。Channel 是 Go 并发编程的核心工具。
创建 Channel
Channel 是通过 make 函数创建的,指定 Channel 的类型和容量:
ch := make(chan int) // 创建一个 int 类型的 Channel向 Channel 发送数据
使用 <- 运算符向 Channel 发送数据:
ch <- 42 // 向 Channel 发送数据 42从 Channel 接收数据
使用 <- 运算符从 Channel 接收数据:
value := <-ch // 从 Channel 接收数据使用 Channel 进行同步
Channel 不仅用于传输数据,也可以用于 Goroutine 之间的同步。例如,我们可以使用 Channel 等待多个 Goroutine 执行完毕:
package main
import "fmt"
func task(ch chan bool, id int) {
fmt.Printf("Task %d is running\n", id)
ch <- true // 向 Channel 发送信号,表示任务完成
}
func main() {
ch := make(chan bool)
// 启动多个 Goroutine
for i := 1; i <= 3; i++ {
go task(ch, i)
}
// 等待所有 Goroutine 完成
for i := 1; i <= 3; i++ {
<-ch // 从 Channel 接收信号,等待任务完成
}
fmt.Println("All tasks completed")
}输出:
Task 1 is running
Task 2 is running
Task 3 is running
All tasks completedChannel 的关闭
当我们不再需要向 Channel 发送数据时,可以使用 close() 来关闭 Channel:
close(ch)关闭的 Channel 不能再发送数据,但可以继续从中接收数据。通常,关闭 Channel 用于告知接收方所有数据已经发送完毕。
2.2、遍历
Go 语言中的 range 可以用来遍历一个管道中的数据,直到管道关闭。遍历操作会从管道中依次接收元素,直到管道被关闭并且没有更多的数据可以接收为止。
示例:遍历管道中的数据
package main
import "fmt"
func sendData(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i // 将数据发送到管道
}
close(ch) // 发送数据完成后关闭管道
}
func main() {
ch := make(chan int)
// 启动协程发送数据
go sendData(ch)
// 遍历管道中的数据
for data := range ch {
fmt.Println(data) // 打印从管道接收到的数据
}
}解释:
range ch用来遍历ch管道中的数据。- 当管道关闭且所有数据被接收时,
range循环结束。 - 如果没有关闭管道且管道没有数据可接收,
range会阻塞。
2.3、无缓冲的channel
无缓冲的管道(unbuffered channel)是最基本的管道类型,它的特性是:
- 发送操作 会被阻塞,直到有一个接收操作准备好接收数据。
- 接收操作 会被阻塞,直到有一个发送操作向管道中发送数据。
这种类型的管道用于需要严格同步的场景,因为发送者和接收者必须保持同步。
示例:无缓冲管道
package main
import "fmt"
func sendData(ch chan int) {
fmt.Println("Sending data...")
ch <- 1 // 发送数据到管道,阻塞直到有接收者接收
fmt.Println("Data sent.")
}
func main() {
ch := make(chan int) // 无缓冲管道
go sendData(ch)
fmt.Println("Receiving data...")
data := <-ch // 从管道接收数据,阻塞直到数据到达
fmt.Println("Data received:", data)
}解释:
- 当
sendData协程中的ch <- 1执行时,程序会在ch <- 1处阻塞,直到主协程执行data := <-ch接收数据。
2.4、有缓冲的channel
有缓冲的管道(buffered channel)允许在管道中存储一定数量的数据,发送操作不会立即阻塞,直到缓冲区满时才会阻塞。接收操作也类似,只有在管道为空时才会阻塞。
有缓冲的管道非常适合于解耦生产者和消费者的速度差异。
示例:有缓冲管道
package main
import "fmt"
func sendData(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i // 向管道发送数据,只有缓冲区未满时才会发送
fmt.Println("Sent:", i)
}
close(ch) // 完成数据发送后关闭管道
}
func main() {
ch := make(chan int, 3) // 有缓冲的管道,缓冲区大小为3
go sendData(ch)
// 接收数据
for data := range ch {
fmt.Println("Received:", data)
}
}解释:
- 管道
ch := make(chan int, 3)创建了一个缓冲区大小为 3 的管道。 - 在
sendData中,数据发送到管道,直到缓冲区满为止才会阻塞。 - 缓冲区满时,
sendData会阻塞,直到有接收者从管道中取出数据。
2.5、channel与range
在 Go 中,我们可以使用 range 来遍历管道中的数据。与普通的 range 遍历数组、切片或映射不同,遍历管道会持续接收数据,直到管道关闭。
示例:使用 range 遍历管道
package main
import "fmt"
func generateNumbers(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 完成数据发送,关闭管道
}
func main() {
ch := make(chan int)
go generateNumbers(ch)
// 使用 range 遍历管道
for num := range ch {
fmt.Println(num) // 接收并打印数据
}
}解释:
range ch会不断从管道中接收数据,直到管道关闭并且所有数据都被接收。close(ch)用来关闭管道,表示没有更多数据会被发送。
2.6、channel与select
select 语句用于在多个管道操作之间进行选择,它类似于 switch 语句,能够同时监听多个管道的发送和接收操作,并且能够处理多个通道的阻塞。
示例:使用 select 同时处理多个管道
package main
import "fmt"
func sendData(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go sendData(ch1)
go sendData(ch2)
// 使用 select 监听多个管道
for i := 0; i < 10; i++ {
select {
case data := <-ch1:
fmt.Println("Received from ch1:", data)
case data := <-ch2:
fmt.Println("Received from ch2:", data)
}
}
}解释:
select语句会阻塞,直到它能够从其中一个管道接收到数据。- 在
select中,如果多个管道都可以操作,Go 会随机选择一个管道进行处理。 - 如果所有管道都没有准备好操作,
select会一直阻塞直到至少一个管道准备就绪。
示例:超时机制与 select
package main
import (
"fmt"
"time"
)
func sendData(ch chan int) {
time.Sleep(2 * time.Second)
ch <- 1
}
func main() {
ch := make(chan int)
go sendData(ch)
// 使用 select 设置超时
select {
case data := <-ch:
fmt.Println("Received data:", data)
case <-time.After(1 * time.Second): // 设置 1 秒超时
fmt.Println("Timeout!")
}
}解释:
time.After(1 * time.Second)会返回一个在 1 秒后发送数据的管道,因此如果管道在 1 秒内没有数据可接收,select会执行超时分支。
十三、网络编程
1、
十四、反射
Go 语言中的反射(Reflection)是一个非常强大且灵活的特性,它允许程序在运行时检查类型、获取值并动态操作对象。通过反射,Go 程序可以检查对象的类型、修改对象的属性以及调用对象的方法,这在某些场景下非常有用,例如实现通用的库、序列化与反序列化、ORM(对象关系映射)等。
1、反射基础
反射的核心在于 reflect 包,该包提供了操作类型和对象值的工具。要理解 Go 中的反射,我们首先需要了解以下两个概念:
- Type:表示一个对象的类型。
- Value:表示一个对象的值。
Go 语言中的类型和对象值是分开存储的。反射通过 reflect.Type 和 reflect.Value 这两个类型来提供对对象的访问。
2、基本概念
reflect.Type用于表示对象的类型信息。reflect.Value用于表示对象的值。
示例:获取类型和值
package main
import (
"fmt"
"reflect"
)
func main() {
var x int = 42
v := reflect.ValueOf(x) // 获取值的反射对象
t := reflect.TypeOf(x) // 获取类型的反射对象
// 输出值
fmt.Println("Value:", v)
// 输出类型
fmt.Println("Type:", t)
// 输出类型名称和类型种类
fmt.Println("Type Name:", t.Name()) // int
fmt.Println("Kind:", v.Kind()) // int
}输出:
Value: 42
Type: int
Type Name: int
Kind: intValueOf获取值的反射对象,返回reflect.Value。TypeOf获取类型的反射对象,返回reflect.Type。Kind表示对象的具体类型,比如int、struct、slice等。
3、reflect.Value 类型
reflect.Value 是 Go 反射操作的核心。通过 reflect.Value,我们可以:
- 获取变量的值。
- 获取变量的类型。
- 修改变量的值(前提是它是指针类型)。
- 调用对象的方法。
示例:获取变量的值
package main
import (
"fmt"
"reflect"
)
func main() {
var x int = 10
v := reflect.ValueOf(x)
fmt.Println("Value:", v) // 输出: Value: 10
fmt.Println("Type:", v.Type()) // 输出: Type: int
}示例:通过反射修改值
如果我们要修改值,必须要传递指向该值的指针。
package main
import (
"fmt"
"reflect"
)
func main() {
var x int = 10
v := reflect.ValueOf(&x) // 传递指针
v.Elem().SetInt(20) // 通过Elem()获取指向的值并修改
fmt.Println("Modified Value:", x) // 输出: Modified Value: 20
}Elem()用于获取指针指向的值,进而修改值。
4、反射与结构体
Go 的反射非常适合与结构体一起使用,它允许你动态地操作结构体的字段,访问或修改字段值。
示例:访问结构体字段
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{"Alice", 30}
v := reflect.ValueOf(p)
// 获取结构体字段
nameField := v.FieldByName("Name")
ageField := v.FieldByName("Age")
// 打印字段值
fmt.Println("Name:", nameField) // 输出: Name: Alice
fmt.Println("Age:", ageField) // 输出: Age: 30
}FieldByName可以通过字段名获取结构体的字段值。- 需要注意的是,
reflect.ValueOf(p)会返回结构体的值。如果要修改字段,应该传递结构体的指针。
示例:修改结构体字段
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{"Alice", 30}
v := reflect.ValueOf(&p) // 传递指针
v.Elem().FieldByName("Name").SetString("Bob") // 修改 Name 字段
v.Elem().FieldByName("Age").SetInt(40) // 修改 Age 字段
fmt.Println("Modified Person:", p) // 输出: Modified Person: {Bob 40}
}Elem()返回指针指向的值。SetString()和SetInt()分别用于修改字符串类型和整型字段。
5、反射与接口
在 Go 中,接口类型也是反射操作的重要部分。通过 reflect.Value,我们可以动态地获取接口值的类型和动态调用接口方法。
示例:处理接口
package main
import (
"fmt"
"reflect"
)
type Speaker interface {
Speak() string
}
type Person struct {
Name string
}
func (p Person) Speak() string {
return "Hello, my name is " + p.Name
}
func main() {
var s Speaker = Person{Name: "Alice"}
v := reflect.ValueOf(s)
method := v.MethodByName("Speak")
result := method.Call([]reflect.Value{})
fmt.Println(result[0].String()) // 输出: Hello, my name is Alice
}MethodByName获取方法的反射对象。Call用于调用方法,返回一个reflect.Value类型的切片。
6、反射的性能
反射是一种动态机制,相比静态类型,性能会稍差一些。尤其是在频繁操作时,反射可能会导致程序的性能下降。因此,反射应该谨慎使用,特别是在对性能要求较高的场合。
十五、结构体标签
Go 语言中的结构体标签(Struct Tags)是 Go 语言中一种非常强大的特性,它允许你在结构体的字段上添加额外的元数据。结构体标签通常用于标注一些与字段相关的额外信息,这些信息可以在运行时通过反射(reflect 包)来访问和处理。
1. 结构体标签的基本概念
结构体标签本质上是一种附加的元数据,通常用于与字段相关的额外描述。在 Go 语言中,标签的语法类似于字段名后面的字符串,它们被包裹在反引号()中。结构体标签没有固定的格式,开发者可以根据需求自由定义。
示例:简单的结构体标签
package main
import "fmt"
type Person struct {
Name string `json:"name"` // JSON 序列化时,使用 "name" 字段名
Age int `json:"age"` // JSON 序列化时,使用 "age" 字段名
}
func main() {
p := Person{"Alice", 30}
fmt.Println(p)
}在这个例子中,json:"name" 和 json:"age" 就是结构体的标签。它们表示当 Person 结构体被序列化为 JSON 时,字段 Name 和 Age 会分别被映射为 name 和 age。
2. 结构体标签的语法
Go 的结构体标签是由一个或多个键值对组成的字符串。每个键值对的格式是 key:"value",多个键值对之间用空格分隔。标签本身是一个字符串,但可以包含多个键值对,并且这些键值对的格式不一定是固定的。
示例:多个标签
type Person struct {
Name string `json:"name" bson:"name"`
Age int `json:"age" bson:"age"`
Email string `json:"email,omitempty"`
}在这个例子中,字段 Name 和 Age 分别有两个标签,一个用于 JSON(json)序列化,一个用于 MongoDB(bson)序列化。Email 字段的标签还包括 omitempty,这意味着如果该字段的值为空(零值),它将被省略在 JSON 输出中。
3. 结构体标签的常见用法
结构体标签的使用场景非常广泛,以下是一些常见的应用场景。
(1) JSON 序列化与反序列化
在 Go 中,encoding/json 包使用结构体标签来指定如何将结构体转换为 JSON 格式,或者如何从 JSON 数据中恢复结构体。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
p := Person{"Alice", 30}
// 序列化成 JSON 字符串
data, _ := json.Marshal(p)
fmt.Println(string(data)) // 输出: {"name":"Alice","age":30}
// 反序列化回结构体
var p2 Person
json.Unmarshal(data, &p2)
fmt.Println(p2) // 输出: {Alice 30}
}json:"name"告诉encoding/json序列化和反序列化时使用name字段名。
(2) BSON 序列化与反序列化
bson 是 MongoDB 使用的二进制 JSON 格式,Go 中的 go.mongodb.org/mongo-driver/bson 包使用类似的标签来指定如何将结构体字段映射为 BSON 字段。
package main
import (
"go.mongodb.org/mongo-driver/bson"
"fmt"
)
type Person struct {
Name string `bson:"name"`
Age int `bson:"age"`
}
func main() {
p := Person{"Alice", 30}
// 序列化成 BSON 字节切片
data, _ := bson.Marshal(p)
fmt.Println(data)
// 反序列化 BSON 数据回结构体
var p2 Person
bson.Unmarshal(data, &p2)
fmt.Println(p2) // 输出: {Alice 30}
}bson:"name"标签告诉 MongoDB 序列化和反序列化时应该将name字段映射为 BSON 数据中的name字段。
(3) 数据库 ORM
许多 ORM(对象关系映射)库,如 gorm,使用结构体标签来指定数据库列的名称、字段约束等信息。
package main
import (
"fmt"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100"`
Age int `gorm:"not null"`
}
func main() {
// 使用 GORM 操作数据库,结构体标签指定数据库字段映射
fmt.Println("User struct with gorm tags")
}gorm:"primaryKey"表示该字段是主键。gorm:"size:100"表示该字段的最大长度为 100。gorm:"not null"表示该字段不能为null。
(4) 表单绑定与验证
在 Web 开发中,结构体标签也用于将 HTTP 请求表单或 JSON 数据绑定到结构体字段,或者进行字段验证。常见的 Web 框架如 Gin、Echo、Beego 等都利用结构体标签来实现这一功能。
例如,在 Gin 框架中,结构体标签可以用来定义表单字段的名称或验证规则。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type User struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func main() {
r := gin.Default()
r.POST("/login", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"message": "Login successful!"})
})
r.Run()
}binding:"required"用于指定字段为必填项。
4. 访问结构体标签
通过反射,Go 语言允许我们在运行时读取结构体标签。这在很多库中非常有用,尤其是需要动态行为的场景(例如 ORM、序列化库等)。
示例:使用反射读取结构体标签
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
t := reflect.TypeOf(Person{})
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Println(field.Name, field.Tag)
}
}输出:
Name json:"name"
Age json:"age"reflect.TypeOf(p)获取结构体类型的反射对象。t.NumField()获取结构体字段的数量。t.Field(i)获取结构体字段的反射对象,并可以访问Tag字段来获取标签值。
