go非基础向入门 | 豆包MarsCode AI刷题

118 阅读28分钟

在线文档

www.topgoer.com/%E5%B8%B8%E…


基本架构

//表示一个程序的入口
package main
//导入一些依赖包
import "fmt"
//主函数,大家都见过
func main() {
}

数据类型

布尔型

很简单的对(true)错(false)

整数类型

序号类型和描述
1uint8 无符号 8 位整型 (0 到 255)
2uint16 无符号 16 位整型 (0 到 65535)
3uint32 无符号 32 位整型 (0 到 4294967295)
4uint64 无符号 64 位整型 (0 到 18446744073709551615)
5int8 有符号 8 位整型 (-128 到 127)
6int16 有符号 16 位整型 (-32768 到 32767)
7int32 有符号 32 位整型 (-2147483648 到 2147483647)
8int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

浮点型

序号类型和描述
1float32 IEEE-754 32位浮点型数
2float64 IEEE-754 64位浮点型数
3complex64 32 位实数和虚数
4complex128 64 位实数和虚数

其他数字

序号类型和描述
1byte 类似 uint8
2rune 类似 int32
3uint 32 或 64 位
4int 与 uint 一样大小
5uintptr 无符号整型,用于存放一个指针

字符串

普普通通的字符串,采用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。(AB) 为 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
按位或运算符""是双目运算符。 其功能是参与运算的两数各对应的二进位相或(AB) 结果为 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 = C2

其他运算符

运算符描述实例
&返回变量存储地址&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) }

解析:

  1. 定义接口和结构体

    • Writer 接口定义了 Write 方法。
    • StringWriter 结构体实现了 Write 方法。
  2. 类型转换

    • StringWriter 实例赋值给 Writer 接口变量 w
    • 使用 w.(*StringWriter)Writer 接口类型转换为 StringWriter 类型。
  3. 访问字段

    • 修改 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 := 0go 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 := 0var 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 或许会更新?