在线文档
基本架构
//表示一个程序的入口
package main
//导入一些依赖包
import "fmt"
//主函数,大家都见过
func main() {
}
数据类型
布尔型
很简单的对(true)错(false)
整数类型
| 序号 | 类型和描述 |
|---|---|
| 1 | uint8 无符号 8 位整型 (0 到 255) |
| 2 | uint16 无符号 16 位整型 (0 到 65535) |
| 3 | uint32 无符号 32 位整型 (0 到 4294967295) |
| 4 | uint64 无符号 64 位整型 (0 到 18446744073709551615) |
| 5 | int8 有符号 8 位整型 (-128 到 127) |
| 6 | int16 有符号 16 位整型 (-32768 到 32767) |
| 7 | int32 有符号 32 位整型 (-2147483648 到 2147483647) |
| 8 | int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
浮点型
| 序号 | 类型和描述 |
|---|---|
| 1 | float32 IEEE-754 32位浮点型数 |
| 2 | float64 IEEE-754 64位浮点型数 |
| 3 | complex64 32 位实数和虚数 |
| 4 | complex128 64 位实数和虚数 |
其他数字
| 序号 | 类型和描述 |
|---|---|
| 1 | byte 类似 uint8 |
| 2 | rune 类似 int32 |
| 3 | uint 32 或 64 位 |
| 4 | int 与 uint 一样大小 |
| 5 | uintptr 无符号整型,用于存放一个指针 |
字符串
普普通通的字符串,采用Unicode编码类型
派生类型(引用类型?)
包括:
- (a) 指针类型(Pointer)
- (b) 数组类型
- (c) 结构化类型(struct)
- (d) Channel 类型
- (e) 函数类型
- (f) 切片类型
- (g) 接口类型(interface)
- (h) Map 类型
变量
这个声明好唐啊(
单声明
var name type
多声明
var name1, name2 type
海象
name := value
常量
定义
可以用const定义
懒得写了···
iota
可以理解为SQL中的自动增加
直接举例子!!!
const(
a = iota //0
b //1
c //2
)
const(
e = iota //0
f //1
g //2
)
运算符
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 其他运算符
这玩意太基础了,而且没有创新,所以直接抄菜鸟的
算术运算符
假定 A 值为 10,B 值为 20。
| 运算符 | 描述 | 实例 |
|---|---|---|
| + | 相加 | A + B 输出结果 30 |
| - | 相减 | A - B 输出结果 -10 |
| * | 相乘 | A * B 输出结果 200 |
| / | 相除 | B / A 输出结果 2 |
| % | 求余 | B % A 输出结果 0 |
| ++ | 自增 | A++ 输出结果 11 |
| -- | 自减 | A-- 输出结果 9 |
关系运算符
| 运算符 | 描述 | 实例 |
|---|---|---|
| == | 检查两个值是否相等,如果相等返回 True 否则返回 False。 | (A == B) 为 False |
| != | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 | (A != B) 为 True |
| 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 | (A > B) 为 False | |
| < | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 | (A < B) 为 True |
| >= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 | (A >= B) 为 False |
| <= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 | (A <= B) 为 True |
逻辑运算符
| 运算符 | 描述 | 实例 | ||||
|---|---|---|---|---|---|---|
| && | 逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。 | (A && B) 为 False | ||||
| 逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。 | (A | B) 为 True | ||||
| ! | 逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 |
位运算符
| p | q | p & q | p | q | p ^ q | | - | - | ----- | ------ | ----- | | 0 | 0 | 0 | 0 | 0 | | 0 | 1 | 0 | 1 | 1 | | 1 | 1 | 1 | 1 | 0 | | 1 | 0 | 0 | 1 | 1 |
| 运算符 | 描述 | 实例 | |||
|---|---|---|---|---|---|
| & | 按位与运算符"&"是双目运算符。 其功能是参与运算的两数各对应的二进位相与。 | (A & B) 结果为 12, 二进制为 0000 1100 | |||
| 按位或运算符" | "是双目运算符。 其功能是参与运算的两数各对应的二进位相或 | (A | B) 结果为 61, 二进制为 0011 1101 | ||
| 按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 | (A ^ B) 结果为 49, 二进制为 0011 0001 | ||||
| << | 左移运算符"<<"是双目运算符。左移n位就是乘以2的n次方。 其功能把"<<"左边的运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补0。 | A << 2 结果为 240 ,二进制为 1111 0000 | |||
| >> | 右移运算符">>"是双目运算符。右移n位就是除以2的n次方。 其功能是把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数。 |
赋值运算符
| 运算符 | 描述 | 实例 | |||
|---|---|---|---|---|---|
| = | 简单的赋值运算符,将一个表达式的值赋给一个左值 | C = A + B 将 A + B 表达式结果赋值给 C | |||
| += | 相加后再赋值 | C += A 等于 C = C + A | |||
| -= | 相减后再赋值 | C -= A 等于 C = C - A | |||
| *= | 相乘后再赋值 | C *= A 等于 C = C * A | |||
| /= | 相除后再赋值 | C /= A 等于 C = C / A | |||
| %= | 求余后再赋值 | C %= A 等于 C = C % A | |||
| <<= | 左移后赋值 | C <<= 2 等于 C = C << 2 | |||
| >>= | 右移后赋值 | C >>= 2 等于 C = C >> 2 | |||
| &= | 按位与后赋值 | C &= 2 等于 C = C & 2 | |||
| ^= | 按位异或后赋值 | C ^= 2 等于 C = C ^ 2 | |||
| = | 按位或后赋值 | C | = 2 等于 C = C | 2 |
其他运算符
| 运算符 | 描述 | 实例 |
|---|---|---|
| & | 返回变量存储地址 | &a; 将给出变量的实际地址。 |
| * | 指针变量。 | *a; 是一个指针变量 |
懒得写优先级
条件语句
哈哈,并没有三目运算符
if...else...
if 条件 {
代码块
}else{
代码块
}
关于 if 的条件:
if的条件可以写一条也可以写两条,如果写两条,则第一条一定会执行,第二条是条件
switch
switch [表达式]{
case 表达式 :
代码块
case 表达式 :
代码块
case 表达式 :
代码块
}
go的switch相对于别的语言做了些改动:
- switch并不会像管道一样滑下去,更贴近于匹配的概念
- switch的case中可以写逻辑表达式,如果为真,则进入该分支
select
select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
select {
case <- channel1:
// 执行的代码
case value := <- channel2:
// 执行的代码
case channel3 <- value:
// 执行的代码
// 你可以定义任意数量的 case
default:
// 所有通道都没有准备好,执行的代码
}
循环
for
for [初始;条件;执行]{
代码块
}
关于[初始;条件;执行] 如果不写任何语句,则为无限循环,写一个语句,这个语句就会默认为条件,写三个就正常
函数
这个牛逼了
func 函数名(形参名 数据类型,形参名 数据类型,形参名 数据类型) 返回值类型/ (返回值名称 返回值类型, 返回值名称 返回值类型) {
return a + b
}
go中依然采用了值传递和引用传递的写法
函数的超级用法
func check(s string) func(int, int) int {
if s == "+" {
return func(x, y int) int { return x + y }
} else {
return func(x, y int) int { return x - y }
}
}
怎么用呢?
check("-")(2, 1)
匿名函数做闭包
比较难理解,看看代码
func clock() func() int {
i := 0
return func() int {
i++
return i
}
}
这个玩意就相当于创建了个小玩意
每次执行会执行return的功能
很奇妙
C := clock()
fmt.Println(C())
fmt.Println(C())
fmt.Println(C())
1
2
3
函数还可以直接为结构体定义方法,非常强大
数组
声明
在声明时,如果没有初始化,就会被默认分配变量的初始值
var arrayName [size]dataType
var balance [10]float32
初始化数组
var numbers = [5]int{1, 2, 3, 4, 5}
numbers := [5]int{1, 2, 3, 4, 5}
如果数组长度不确定,还可以用...代替
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
或
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
初始化指定下标的元素
// 将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}
引用数组
很简单,很无聊,不想写
二(多)维数组
声明数组
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
var threedim [5][10][4]int
初始化二维数组
a := [3][4]int{
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}
引用二(多)维数组
不想写~~~
如何向函数传递数组呢?
一维
固定大小的
func myFunction(param [10]int) {
....
}
不固定大小的
func myFunction(param []int) {
....
}
二维
func myFunction(param [][]int) {
....
}
三维
当然没有三维,你在想什么?
指针
指针定义
var var_name *var-type
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
如何获取地址
当然是大名鼎鼎的取地址符 &
如何解引用地址
当然是小名鼎鼎的解引用符 *****
会有指针数组吗?
神说,会有的
var ptr [MAX]*int;
会有指指针吗?
神说,会有的
var ptr **int;
能向函数中传入指针吗?
神说,我不说
会有的,别打了
结构体
青出于蓝胜于蓝
结构体的定义
type struct_variable_type struct {
member definition
member definition
...
member definition
}
结构体的声明
variable_name := structure_variable_type {value1, value2...valuen}
或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
其中对应的是无参数构造和有参数构造(胡言乱语
成员的访问
使用 . 访问
结构体.成员名
能否作为函数的参数呢?
可可以的!!!!
可以创建结构体指针吗?
可以的!!!!!
关于之前提到过的结构体方法创建:
格式:
func (结构体形参 结构体名称) 方法名(传参名称 type) return_type {
方法体
}
首先构造结构体
type book struct {
title string
author string
year int
price float64
}
然后创建方法
func (b book) show() {
fmt.Println("title:", b.title)
fmt.Println("author:", b.author)
fmt.Println("year:", b.year)
fmt.Println("price:", b.price)
}
切片
切片可以理解诶为动态数组
创建:
用var创建:
var identifier []type
make创建
var slice1 []type = make([]type, len)
简写
slice1 := make([]type, len)
也可以指定容量和底层容量
make([]T, length, capacity)
capacity:可选参数,指底层容量
初始化:
s :=[] int {1,2,3 }
切片操作
可以进行切片操作!!!和py差不多
创建一个空切片:
s := arr[:]
其实就是s和arr同时引用这个数组,因为切片内没有写任何参数
切片的函数
获取长度:len()
底层容量:cap()
追加:append()
拷贝:copy()
range
功能
用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素
在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
举例子:
数组和切片
遍历简单的切片,2%d 的结果为 2 对应的次方数:
实例
package main
import "fmt"
*// 声明一个包含 2 的幂次方的切片*
**var** pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
*// 遍历 pow 切片,i 是索引,v 是值*
**for** i, v := range pow {
*// 打印 2 的 i 次方等于 v*
fmt.Printf("2%d = %d\n", i, v)
}
}
以上实例运行输出结果为:
2**0 = 1
2**1 = 2
2**2 = 4
2**3 = 8
2**4 = 16
2**5 = 32
2**6 = 64
2**7 = 128
字符串
range 迭代字符串时,返回每个字符的索引和 Unicode 代码点(rune)。
实例
package main
import "fmt"
func main() {
for i, c := range "hello" {
fmt.Printf("index: %d, char: %c\n", i, c)
}
}
以上实例运行输出结果为:
index: 0, char: h
index: 1, char: e
index: 2, char: l
index: 3, char: l
index: 4, char: o
映射(Map)
for 循环的 range 格式可以省略 key 和 value,如下实例:
实例
package main
import "fmt"
func main() {
*// 创建一个空的 map,key 是 int 类型,value 是 float32 类型*
map1 := make(map[int]float32)
*// 向 map1 中添加 key-value 对*
map1[1] = 1.0
map1[2] = 2.0
map1[3] = 3.0
map1[4] = 4.0
*// 遍历 map1,读取 key 和 value*
for key, value := range map1 {
*// 打印 key 和 value*
fmt.Printf("key is: %d - value is: %f\n", key, value)
}
*// 遍历 map1,只读取 key*
for key := range map1 {
*// 打印 key*
fmt.Printf("key is: %d**\n**", key)
}
*// 遍历 map1,只读取 value*
for _, value := range map1 {
*// 打印 value*
fmt.Printf("value is: %f\n", value)
}
}
以上实例运行输出结果为:
key is: 4 - value is: 4.000000
key is: 1 - value is: 1.000000
key is: 2 - value is: 2.000000
key is: 3 - value is: 3.000000
key is: 1
key is: 2
key is: 3
key is: 4
value is: 1.000000
value is: 2.000000
value is: 3.000000
value is: 4.000000
通道(Channel)
range 遍历从通道接收的值,直到通道关闭。
实例
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
****for**** v := range ch {
fmt.Println(v)
}
}
for v := range ch { fmt.Println(v) } }
以上实例运行输出结果为:
1
2
忽略值
在遍历时可以使用 _ 来忽略索引或值。
实例
package main
import "fmt"
func main() {
nums := []int{2, 3, 4}
*// 忽略索引*
for _, num := range nums {
fmt.Println("value:", num)
}
*// 忽略值*
****for**** i := range nums {
fmt.Println("index:", i)
}
}
以上实例运行输出结果为:
value: 2
value: 3
value: 4
index: 0
index: 1
index: 2
其他
range 遍历其他数据结构:
实例
package main
mport "fmt"
func main() {
*//这是我们使用 range 去求一个 slice 的和。使用数组跟这个很类似*
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
*//在数组上使用 range 将传入索引和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。*
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
*//range 也可以用在 map 的键值对上。*
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
*//range也可以用来枚举 Unicode 字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。*
for i, c := range "go" {
fmt.Println(i, c)
}
}
以上实例运行输出结果为:
sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111
Map
无序键值对
创建
/* 使用 make 函数 */
map_variable := make(map[KeyType]ValueType, initialCapacity)
示例
// 创建一个空的 Map
m := make(map[string]int)
// 创建一个初始容量为 10 的 Map
m := make(map[string]int, 10)
使用字面量创建:
// 使用字面量创建 Map
m := map[string]int{
"apple": 1,
"banana": 2,
"orange": 3,
}
获取元素
// 获取键值对
v1 := m["apple"]
v2, ok := m["pear"] // 如果键不存在,ok 的值为 false,v2 的值为该类型的零值
修改元素:
// 修改键值对
m["apple"] = 5
获取 Map 的长度:
// 获取 Map 的长度
len := len(m)
遍历 Map:
// 遍历 Map
for k, v := range m {
fmt.Printf("key=%s, value=%d\n", k, v)
}
删除元素:
// 删除键值对
delete(m, "banana")
Go 语言类型转换
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。
Go 语言类型转换基本格式如下:
type_name(expression)
type_name 为类型,expression 为表达式。
数值类型转换
将整型转换为浮点型:
var a int = 10
var b float64 = float64(a)
以下实例中将整型转化为浮点型,并计算结果,将结果赋值给浮点型变量:
实例
package main
import "fmt"
func main() {
var sum int = 17
var count int = 5
var mean float32
mean = float32(sum)/float32(count)
fmt.Printf("mean 的值为: %f\n",mean)
}
mean = float32(sum)/float32(count) fmt.Printf("mean 的值为: %f\n",mean) }
以上实例执行输出结果为:
mean 的值为: 3.400000
字符串类型转换
将一个字符串转换成另一个类型,可以使用以下语法:
var str string = "10"
var num int
num, _ = strconv.Atoi(str)
以上代码将字符串变量 str 转换为整型变量 num。
注意,strconv.Atoi 函数返回两个值,第一个是转换后的整型值,第二个是可能发生的错误,我们可以使用空白标识符 _ 来忽略这个错误
以下实例将字符串转换为整数
实例
package main
import (
"fmt"
"strconv"
)
func main() {
str := "123"
num, err := strconv.Atoi(str)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 '%s' 转换为整数为:%d\n", str, num)
}
}
以上实例执行输出结果为:
字符串 '123' 转换为整数为:123
以下实例将整数转换为字符串:
实例
package main
import (
"fmt"
"strconv"
)
func main() {
num := 123
str := strconv.Itoa(num)
fmt.Printf("整数 %d 转换为字符串为:'%s'\n", num, str)
}
以上实例执行输出结果为:
整数 123 转换为字符串为:'123'
以下实例将字符串转换为浮点数:
实例
package main
import (
"fmt"
"strconv"
)
func main() {
str := "3.14"
num, err := strconv.ParseFloat(str, 64)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 '%s' 转为浮点型为:%f\n", str, num)
}
}
以上实例执行输出结果为:
字符串 '3.14' 转为浮点型为:3.140000
以下实例将浮点数转换为字符串:
实例
package main
import (
"fmt"
"strconv"
)
func main() {
num := 3.14
str := strconv.FormatFloat(num, 'f', 2, 64)
fmt.Printf("浮点数 %f 转为字符串为:'%s'\n", num, str)
}
以上实例执行输出结果为:
浮点数 3.140000 转为字符串为:'3.14'
接口类型转换
接口类型转换有两种情况 :类型断言和类型转换。
类型断言
类型断言用于将接口类型转换为指定类型,其语法为:
value.(type)
或者
value.(T)
其中 value 是接口类型的变量,type 或 T 是要转换成的类型。
如果类型断言成功,它将返回转换后的值和一个布尔值,表示转换是否成功。
实例
package main
import "fmt"
func main() {
var i interface{} = "Hello, World"
str, ok := i.(string)
if ok {
fmt.Printf("'%s' is a string\n", str)
} else {
fmt.Println("conversion failed")
}
}
以上实例中,我们定义了一个接口类型变量 i,并将它赋值为字符串 "Hello, World"。然后,我们使用类型断言将 i 转换为字符串类型,并将转换后的值赋值给变量 str。最后,我们使用 ok 变量检查类型转换是否成功,如果成功,我们打印转换后的字符串;否则,我们打印转换失败的消息。
类型转换
类型转换用于将一个接口类型的值转换为另一个接口类型,其语法为:
T(value)
T 是目标接口类型,value 是要转换的值。
在类型转换中,我们必须保证要转换的值和目标接口类型之间是兼容的,否则编译器会报错。
实例
package main
import "fmt"
*// 定义一个接口 Writer*
type Writer interface {
Write([]byte) (int, error)
}
*// 实现 Writer 接口的结构体 StringWriter*
type StringWriter struct {
str string
}
*// 实现 Write 方法*
func (sw *StringWriter) Write(data []byte) (int, error) {
sw.str += string(data)
return len(data), nil
}
func main() {
*// 创建一个 StringWriter 实例并赋值给 Writer 接口变量*
var w Writer = &StringWriter{}
*// 将 Writer 接口类型转换为 StringWriter 类型*
sw := w.(*StringWriter)
*// 修改 StringWriter 的字段*
sw.str = "Hello, World"
*// 打印 StringWriter 的字段值*
fmt.Println(sw.str)
}
// 将 Writer 接口类型转换为 StringWriter 类型 sw := w.(*StringWriter)
// 修改 StringWriter 的字段 sw.str = "Hello, World"
// 打印 StringWriter 的字段值 fmt.Println(sw.str) }
解析:
-
定义接口和结构体:
Writer接口定义了Write方法。StringWriter结构体实现了Write方法。
-
类型转换:
- 将
StringWriter实例赋值给Writer接口变量w。 - 使用
w.(*StringWriter)将Writer接口类型转换为StringWriter类型。
- 将
-
访问字段:
- 修改
StringWriter的字段str,并打印其值。
- 修改
空接口类型
空接口 interface{} 可以持有任何类型的值。在实际应用中,空接口经常被用来处理多种类型的值。
实例
package main
import (
"fmt"
)
func printValue(v interface{}) {
switch v := v.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
default:
fmt.Println("Unknown type")
}
}
func main() {
printValue(42)
printValue("hello")
printValue(3.14)
}
在这个例子中,printValue 函数接受一个空接口类型的参数,并使用类型断言和类型选择来处理不同的类型。
init
初始化函数,会在执行main之前被执行
defer
会在函数结束前执行代码 相当于单开了一个栈,所有的defer后面的语句会在函数结束前执行 通常被用于释放资源,关闭句柄,数据库连接释放
new
接受一个类型参数,返回一个指向该内存的指针
make
make和new一样,都是用于内存分配 用于slice,make,chan的创建
并发
go从代码层面支持简单的并发,同时实现了垃圾回收机制
启用并发非常简单,只需要添加go关键字就好,并发通过goroutine特性完成,类似于线程,可以并行进行工作
goroutine由go进行调度完成工作,线程则是通过系统完成调度工作
对于多个goroutine之间的通信问题,go提供了channel进行通信,通过通信实现共享内存(CSP)
进程和线程
进程: 指程序在操作系统中执行一次的过程,是系统进行资源调度的独立单位
线程: 指的是进程执行时的实体,是CPU工作时进行调度资源的最小单位,
一个进程可以包含多个线程,同一个线程之间的进程可以并发执行。
并发和并行
并发: 指多线程在单核CPU上运行,在不同的时间把许多任务交给相同的核心处理,任务不会并行运行
并行: 指多线程在多核CPU上运行,在相同的时间把许多任务交给不同的核心处理,任务会同时运行
并发和并行本质并不相同,并发的任务会通过时间切片操作来实现“同时”运行,实际每次只能处理一个任务,并行指多个核心同时处理许多任务,能更大的发挥多核CPU的性能,
总结: 并发就是有很多事情要做,并行就是同时做很多事情。
协程和线程
协程: 享有独立的栈空间,共享堆空间,由用户控制调度,本质类似于用户级的线程,相对于线程,协程的开销非常小
线程: goroutine:一个线程上可能运行多个协程,协程比较轻量,
总结: 协程<线程<进程
协程-goroutine
go在工作时,goroutine是最小的单位,go会自动的将goroutine分给每个核心用于执行任务,
goroutine相当于线程,但是开销更小,go内置实现了goroutine的内存共享
使用go关键字就可以轻松地创建goroutine,函数是最小的goroutine单位,只需要在函数之前加上go就可以快速地创建一个基于这个函数的goroutine
main在执行时,会被go默认创建为一个goroutine
创建goroutine
goroutine是函数之间独立运行的能力,所以只能以函数的基础创建goroutine,一个goroutine对应一个函数
格式
go 函数()
函数的返回值会被忽略
实例
package main
import "fmt"
func show_hello() {
fmt.Println("hello world")
}
func main() {
go show_hello()
}
匿名函数创建goroutine
格式
go func(形参列表) {
函数体
}(实参列表)
实例
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Printf("%d\n", i)
}(i)
}
time.Sleep(time.Second)
}
注意: 当main执行结束时,在main中创建的goroutine会被强制结束,也就是说,在父协程结束后子协程会被迫结束。
通道-channel
介绍
单独的进行函数的并发是没有意义的,这只是加快了程序执行的效率,只有数据进行交换时,才能体现出并行的意义
数据交互虽然可以通过共享内存实现,但是共享内存对于多个goroutine会产生竟态问题,影像数据交互的安全性,可靠性,为了避免这种问题必须使用互斥锁堆内存进行加密,这种做法势必会产生性能问题。
go倡导的并发模型CSP,即通过通信实现共享内存而不是通过共享内存实现通信,
goroutine是go运行时的执行体,那么channel就是其中的链接
channel是一种特殊的数据类型,遵循队列的传输规则,保证收发的顺序,所以要为channel规定其传输的数据类型,也就是说,每个channel只能传输他所被规定的数据。
channel在进程内起到通信的作用,对于进程间的通信,则需要使用socket或者http等通信协议来完成
特性
在任何时候,一个channel只能有一个goroutine进行写入或者读取
遵循先入先出的传输规则
通道根据交换的行为,分为无缓冲通道和有缓冲通道,无缓冲通道用于同步的信息交换,有缓冲的通道用于异步信息的交换
channel创建
声明类型
var ch chan 数据类型
此时,只完成了声明工作,还没有对内部初始化,我们通过make对其初始化
ch = make(chan 数据类型【,缓冲大小】)
或者可以通过make快速创建
ch := make(chan 数据类型,缓冲大小
| /;'/
')fa
操作
支持发送,接收,关闭
发送
ch<-data
data的type要和channel的类型相同
如果发送的数据始终没有被接收,发送行为就一直处于阻塞状态,直到有goroutine接收数据
接受
data的type要和channel的类型相同
接收数据的∏种格式
会被阻塞的接收数据
data := <- ch
如果通道没有goroutine发送数据,接收行为将持续被阻塞
不会被阻塞的接收
data,ok := <- ch
如果通道中有数据,则data中会存放接受的数据了,OK则保存是否成功接收导数据,如果没有接收到数据,data中为null,OK则反之
这种方法会浪费大量的CPU资源,因此不常用,
接收数据,忽略数据
<-ch
执行语句会发生阻塞,直到有goroutine发送数据
循环接收
for data := range ch {
循环体
}
字面意思,for会一直保持接受状态,直到有数据写入channel,数据就会被读入到data中,并且会进入for循环一次,循环结束后,继续等待数据被写入channel
实例
package main
import (
"fmt"
rand2 "math/rand"
"math/rand/v2"
"time"
)
func main() {
ch := make(chan time.Time)
rand2.Seed(time.Now().UnixNano())
go func() {
for {
ch <- time.Now()
sleepDuration := time.Second * time.Duration(rand.IntN(6))
time.Sleep(sleepDuration)
}
}()
for data := range ch {
fmt.Println(data)
}
}
注意:
通道的收发操作必然在两个goroutine中进行,如果接收和发送在同一个goroutine中进行,接收和发送必然不能同时进行,如果不能同时进行,则必然会导致阻塞
单次操作,通道只能接收或者发送一个数据元素
通道发送数据的次序问题
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan time.Time)
go func() {
fmt.Println(1)
ch1 <- time.Now()
fmt.Println(2)
}()
go func() {
time.Sleep(1 * time.Second)
fmt.Println(3)
fmt.Println(<-ch1)
fmt.Println(4)
}()
time.Sleep(time.Second * 3)
}
只有当发送被接受时,两者才同时完成,并退出阻塞状态
关闭通道
可以使用close关键字关闭通道
close(ch)
通道不必要关闭,和文件对象不一样,文件对象不会被垃圾回收机制锁定,通道会,所以关闭通道的行为不是必须的
特点
对一个已经关闭的通道发送数据会导致panic
对一个已经关闭的通道接收数据会一直接受数据直到通道为空
对一个关闭且为空的通道执行接受操作会收到默认值
重复关闭通道会导致panic
如何判断通道被关闭,可以采用多重返回的方式,如果ok的值为f在,则说明通道被关闭
单向通道
单向通道只能用于读取或者写入,当然本质还是支持读取和写入的,否则根本没办法使用,所以这只是对于通道的一种使用限制,并不是真的只能使用其中一项功能
声明
写入通道
var ch chan<- 通道类型
读取通道
var ch <-chan 通道类型
初始化
ch = make(chan 数据类型,【缓冲区大小】)
当然,创建一个这样的通道是毫无意义的,对这样的通道进行违法操作也会报错,在一些包中,使用了这个性质来创建特殊的通道,有利于代码接口的严谨性
无缓冲通道
无缓冲通道指的是没有缓冲区的通道,这也就代表着通道想要被使用,必须同时进行发送和接受,否则先到的一方会被阻塞
太合理了
也就是说,无缓冲通道进行的收发操作必定是同步的
带缓冲通道
带缓冲通道中有着可以存储值的缓冲区,这也就代表着接收和发送不必要同时进行,阻塞的条件也会改变,这时,只有当缓冲区内没有数据时,接受行为会被阻塞,没有可用的缓冲区时,发送行为会被阻塞。
因为这种特性,收发操作并不会被保证以同步的方式进行。
类似于菜鸟驿站?
为什么要限制长度呢?
如果生产方写入数据的速度大于消费方读取数据的速度,通道的缓冲区的数据就会不断滞胀,最后导致崩溃
可以使用len获取通道内元素的数量,使用cap获取容量,虽然我们很少这么做,你会在意你的水管里面有多少水吗?
channel的超时机制
go并没有直接提供关于超时的处理机制,但是我们可以使用select来解决这一问题
select虽然并不是专门为处理超时准备的,但是却可以很好的解决这个问题,select特性是,只要有一个case可以执行,就会往下执行,而不会考虑其他case,
这种并不是很专业的处理方式会带来一些问题,比如慢速的机或者网络会出现结果不一致的现象,但是从根本来讲,解决死锁的价值要远远大于使用select带来的问题
select的用法与Switch非常相似,但也有限制,最大的限制就是每个case必须是一个io操作,结构如下
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
select {
case data := <-ch1:
fmt.Println(data)
case data := <-ch2:
fmt.Println(data)
default:fmt.Println(123)
}
}
在这个语句中,select会从头到尾对每个io进行评估,如果其中任意的io可以执行,那么就执行该case
如果没有io响应,则执行default,没有default就一直等待,直到至少有一个io响应
处理超时的实例
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
quit := make(chan int)
go func() {
for {
select {
case num := <-ch:
fmt.Println(num)
case <-time.After(time.Second * 3):
fmt.Println("timeout")
quit <- 0
}
}
}()
for i := 0; i < 100; i++ {
ch <- i
}
<-quit
fmt.Println("quit")
}
互斥锁&读写互斥锁(Mutex&RWMutex)
互斥锁
为了解决多个goroutine同时对一个资源进行操作所引发的竞争问题。
什么是竞争呢?
package main
import (
"fmt"
"time"
)
func main() {
aaa := 0
go func() {
for i := 0; i < 100; i++ {
go func() {
for j := 0; j < 100; j++ {
go func() {
aaa++
}()
}
}()
}
}()
time.Sleep(time.Second * 4)
fmt.Println(aaa)
}
诶,运行发现结果并不是一万,这就是竞争导致的数据与实际情况不符
怎么避免这种情况呢?
Mutex是最简单的类型,比较暴力,当一个goroutine获得了一块资源的Mutex之后,其他goroutine便不能访问这块资源,只能等待当前持有Mutex的goroutine释放Mutex,保证了资源同一时间只被一个goroutine访问
当多个goroutine面对一个被释放的Mutex时,唤醒的策略是随机的
创建和使用
创建:
var Mu sync.Mutex
使用:
加锁:
Mu.Lock()
解锁
Mu.Unlock()
实例:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
aaa := 0
var Mu sync.Mutex
go func() {
for i := 0; i < 100; i++ {
go func() {
for j := 0; j < 100; j++ {
go func() {
Mu.Lock()
aaa++
Mu.Unlock()
}()
}
}()
}
}()
time.Sleep(time.Second * 4)
fmt.Println(aaa)
}
这样就可以保证数据区数据的准确性
应用范围:单机系统的多个线程同时操作一个数据区,且不考虑性能
读写互斥锁
读写互斥锁稍微友好些,典型的单写多读模式,多个goroutine可以同时申请到读锁,但是只能有一个 goroutine持有写锁,并且在写锁被持有时,读锁不不开放。也就是说,当写锁被持有时,整个锁被改goroutine独占,
本质上读写互斥锁是组合了互斥锁
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount atomic.Int32 // number of pending readers
readerWait atomic.Int32 // number of departing readers
}
申请读锁和写锁
申请读锁
RWMu.RLock()
RWMu.RUnlock()
申请写锁
RWMu.Lock()
RWMu.Unlock()
无论是读锁还是写锁,都必须在操作完成后进行释放,否则会导致之后等待锁的goroutine一直处于饥饿状态,甚至可能导致死锁
ps 或许会更新?