Golang M 2023 7 * theory
Posted 知其黑、受其白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang M 2023 7 * theory相关的知识,希望对你有一定的参考价值。
阅读目录
- 1 特性篇
- 什么是协程(Goroutine)
- 进程、线程、协程
- Golang 使用什么数据类型?
- 字符串的小问题
- 数组定义问题
- 对 rune 字面量的理解和数组的语法
- 内存四区
- Go 支持什么形式的类型转换?
- 空结构体的作用
- 单引号,双引号,反引号的区别?
- * 如何停止一个 Goroutine?
- Go 语言中 cap 函数可以作用于哪些内容?
- Printf(),Sprintf(),FprintF() 都是格式化输出,有什么不同?
- *golang 中 make 和 new 的区别?
- for-range 切片的时候,它的地址会发生变化么?
- context 使用场景和用途?
- 常量计数器 iota
- defer 特性相关
- defer 遇见 panic
- 介绍下 rune 类型
- 介绍一下 interface
- go 语言如何实现面对对象编程
- go的结构体能不能比较?
- waitGroup对象,可以实现同一时间启动n个协程
- 切片与数组的区别
- 切片的创建
- 为什么map的遍历是无需的?
- nil map 和空 map 有何不同?
- map 中删除一个 key,它的内存会释放么?
- Student 结构值运行下面程序发生什么?
- 2 channel 篇
- 3 内存逃逸篇
- Golang GC、三色标记、混合写屏障机制
1 特性篇
什么是协程(Goroutine)
协程是用户态轻量级线程,是线程调度的基本单位。
通常在函数前加上go关键字就能实现并发。
一个Goroutine会以一个很小的栈启动2KB或4KB,当遇到栈空间不足时,栈会自动伸缩, 因此可以轻易实现成千上万个goroutine同时启动。
进程、线程、协程
进程是一个程序的数据集合。
线程是进程的一个最小单位。
协程是用户控制的轻量级线程,它是一种特殊的线程,可以在单个线程中实现多任务的并发处理。
goroutine 是轻量级的线程,占用资源很少,但如果一直得不到释放并且还在不断创建新协程,毫无疑问是有问题的,并且是要在程序运行几天,甚至更长的时间才能发现的问题。
Golang 使用什么数据类型?
- 布尔型
- 数值型(整型、浮点型)
- 字符串
- 指针
- 数组
- 结构体
- 切片
- map
- chan
- 接口
- 函数
字符串的小问题
- ①可以用
==
比较。 - ②不可以通过下标的方式改变某个字符,字符串是只读的。
- ③不能和
nil
比较。
数组定义问题
数组是可以以指定下标的方式定义的,例如:
表示 array[9]==34 则 len(array) 就是 10
array := [...]int1,2,3,9:34
import "fmt"
func main()
array := [...]int1, 2, 3, 9: 34
fmt.Printf("值:%#v len%d", array, len(array))
PS E:\\TEXT\\test_go\\test\\case> go run .\\case.go
值:[10]int1, 2, 3, 0, 0, 0, 0, 0, 0, 34 len10
PS E:\\TEXT\\test_go\\test\\case>
对 rune 字面量的理解和数组的语法
package main
import (
"fmt"
)
func main()
m := [...]int
'a': 1,
'b': 2,
'c': 3,
m['a'] = 3
fmt.Println(len(m)) // 输出:100
原因:
以下标的方式定义数组内的元素,'c’的ascll为99,故长度为100。
内存四区
- 代码区:存放代码
- 全局区:常量+全局变量。
最终在进程退出时,由操作系统回收。 - 堆区:空间充裕,数据存放时间较久。
一般由开发者分配,启动 Golang 的 GC 由 GC 清除机制自动回收。 - 栈区:空间较小,要求数据读写性能高,数据存放时间较短暂。由编译器自动分配和释放,存放函数的参数值、局部变量、返回值等、局部变量等(局部变量如果产生逃逸现象,可能会挂在在堆区)
Go 支持什么形式的类型转换?
Go支持显示类型的转换,以满足严格的类型要求。
空结构体的作用
不包含任何字段的结构体叫做空结构体 struct
。
定义:
var et struct
et := struct
type ets struct
et := ets
var et ets
特性:
所有的空结构体的地址都是同一地址,都是 zerobase
的地址,且大小为 0。
使用场景:
1
用于保存不重复的元素的集合,Go 的 map 的 key 是不允许重复的,用空结构体作为 value,不占用额外空间。
2
用于channel 中信号传输,当我们不在乎传输的信号的内容的时候,只是说只要用信号过来,通知到了就行的时候,用空结构体作为 channel 的类型。
3
作为方法的接收者,然后该空结构体内嵌到其他结构体,实现继承。
单引号,双引号,反引号的区别?
- 单引号,表示 byte 或者 rune 类型,对应 uint8 和 int32 类型;默认直接赋值的话是 rune 类型。
- 双引号,字符串类型,不允许修改。实际上是字符数组,可以用下标索引其中的某个字节。
- 反引号,表示字符串字面量,反引号中的字符不支持任何转义,写什么就是什么。
* 如何停止一个 Goroutine?
- ① for - select 方法,采用通道,通知协程退出。
- ②采用 context 包。
Go 语言中 cap 函数可以作用于哪些内容?
- 数组
- 切片
- 通道
Printf(),Sprintf(),FprintF() 都是格式化输出,有什么不同?
- Printf():是标准输出,一般用于打印。
- Sprintf():把格式化字符串输出到字符串,并返回。
- FprintF():把格式化字符串输出到实现了 io.witer方法的类型,比如文件,写入文件。
*golang 中 make 和 new 的区别?
共同点:都会分配内存空间(堆上)
不同点:
- ① 作用变量不同,new 可以为任意类型分配内存;但是 make 只能给,切片、map、chan分配内存。
- ② 返回类型不同,new 返回的是指向变量的指针;make 返回的是上边三种变量类型本身。
- ③ new 对分配的内存清零;make会根据你的设定进行初始化,比如在设置长度、容量的时候。
for-range 切片的时候,它的地址会发生变化么?
在 for a,b := range slice
的遍历中,a 和 b 内存中的地址只有一份,每次循环遍历的时候,都会对其进行覆盖,其地址始终不会改变。
对于切片遍历的话,b 是复制的切片中的元素,改变 b,并不会影响切片中的元素。
context 使用场景和用途?
context 的主要作用:
协调多个 groutine 中的代码执行 “取消” 操作,并且可以存储键值对。最重要的是它是并发安全的。
① 可以存储键值对,供上下文(协程间)读取【建议不要使用】
② 优雅的主动取消协程(Cancel)。主动取消子协程运行,用不到子协程了,回收资源。比如一个http请求,客户端突然断开了,就直接cancel,停止后续的操作;
③ 超时退出协程(Timeout),比如如果三秒之内没有执行结束,直接退出该协程;
④ 截止时间退出协程(Deadline),如果一个业务,2点到4点为业务活动期,4点截止结束任务(协程)
常量计数器 iota
iota 常量计数器,具有自增的特点,可以简化有关于数字增长的常量的定义。
特点:
① iota只能出现在const代码块中。
② 不同 const 代码块中的 iota 互不影响。
③ 从第一行开始算,ioat 出现在第几行,它的值就是第几行减一 【所有注释行和空白行忽略;_
代表一行,不能忽略;这一行即使没有iota 也算一行。】
④ 没有表达式的常量定义复用上一行的表达式。
defer 特性相关
1
defer 的作用:
defer为延迟函数
,为防止开发人员,在函数退出的时候,忘记释放资源或者执行某些收尾工作;比如,解锁、关闭文件、异常捕获等操作;
2
defer 的执行顺序:
每个defer对应一个实例,多个defer,也就是多个实例,使用指针连接成一个单链表,每次写一个defer实例,就插入到这个单链表的头部,函数结束的时候,从头部依次取出,并执行defer。可以类比“栈”的先进后出方式。
3
defer 与 return 先后顺序:
return 后的语句先执行,defer 后的语句后执行。
4
具名返回值遇到defer的情况(看下面的例子)
return虽先执行,但是defer中有改变具名返回值的操作,导致返回值发生了改变(至于为什么,只能说Go就是这样定义的)
package main
import "fmt"
// t 初始化 0, 并且作用域为该函数全域
func returnButDefer() (t int)
defer func()
t = t * 10
()
return 1
func main()
fmt.Println(returnButDefer()) //输出 10
defer 遇见 panic
遇见 return(或函数体到末尾
)和遇见 panic 都会触发 defer。
① defer遇见panic,但是并不捕获异常的情况。
和 return 一样,只不过 panic 前面的 defer 执行完之后,跳出函数,直接报异常。
② defer遇见panic,并捕获异常。
和上述不同的是,当运行的defer中捕获异常,并恢复之后,跳出函数,不会报异常,会继续执行。
但是需要注意的是,在发生恐慌的函数内,panic之后的程序都不会被执行。
package main
import "fmt"
func DeferFunc1(i int) (t int)
t = i
defer func()
t += 3
()
return t
func DeferFunc2(i int) int
t := i
defer func()
t += 3
()
return t
func DeferFunc3(i int) (t int)
defer func()
t += i
()
return 2
func DeferFunc4() (t int)
//传入的实参t,将defer放入链表时的t,并不是执行defe时候的t
defer func(i int)
fmt.Println(i)
fmt.Println(t)
(t) //传入实参t
t = 1
return 2
func main()
fmt.Println(DeferFunc1(1))
fmt.Println(DeferFunc2(1))
fmt.Println(DeferFunc3(1))
DeferFunc4()
输出:
这类题目记住,return 返回值的时候,是赋值操作,并没指针。
PS E:\\TEXT\\test_go\\test\\case> go run .\\case.go
4
1
3
0
2
PS E:\\TEXT\\test_go\\test\\case>
介绍下 rune 类型
rune 是 int32 的别名,等同于int32,常用来处理 unicode 或 utf-8 字符,用来区分字符值和整数值。
这里和 byte 进行对比,byte是uint8,常用来处理 ascii 字符。
那么有什么不同呢?
举个例子
package main
import (
"fmt"
"unicode/utf8"
)
func main()
var str = "hello 世界"
/*
golang中string底层是通过byte数组实现的,
直接求len 实际是在按字节长度计算,
所以一个汉字占3个字节算了3个长度。
*/
fmt.Println("len(str):", len(str))
//以下两种都可以得到str的字符串长度
//golang中的unicode/utf8包提供了用utf-8获取长度的方法
fmt.Println("RuneCountInString:", utf8.RuneCountInString(str))
//通过rune类型处理unicode字符
fmt.Println("rune:", len([]rune(str)))
PS E:\\TEXT\\test_go\\test\\case> go run .\\case.go
len(str): 12
RuneCountInString: 8
rune: 8
PS E:\\TEXT\\test_go\\test\\case>
package main
import (
"fmt"
)
func main()
var str = "hello 世界"
fmt.Println(string([]rune(str)[7:]))
//就能取出‘界’
介绍一下 interface
interface 特性,
- interface 是 method 方法的集合。
- interface是一种类型,并且是指针类型
- 实现统一的接口(成为接口类型的数据)
- 利用统一的接口各干各的事(方法的不同实现方式)
- interface的更重要的作用在于多态实现
interface 使用
-
接口的使用不仅仅针对结构体,自定义类型、变量等等都可以实现接口。
-
如果一个接口没有任何方法,我们称为空接口,由于空接口没有方法,所以任何类型都实现了空接口。
-
要实现一个接口,必须实现该接口里面的所有方法。
接口的类型检查① 断言
package main
import (
"fmt"
)
type Student struct
Name string
Age int
func main()
stu := &Student
Name: "小有",
Age: 22,
var i interface = stu
// 断言成功,s1为*Student类型 不安全断言
s1 := i.(*Student)
fmt.Println(s1)
//断言失败,ok为false 安全型断言
s2, ok := i.(Student)
if ok
fmt.Println("success:", s2)
fmt.Println("failed:", s2)
② 如果接口类型可能有多种情况的话,采用 Type Switch 方法。
func typeCheck(v interface)
// switch v.(type) //只用判断类型,不需要值
switch msg := v.(type) //值和判断类型都需要
case int :
...
case string:
...
case Student:
...
case *Student:
...
default:
...
go 语言如何实现面对对象编程
面对对象编程的三个基本特征:
- 封装
- 继承
- 多态
go 通过结构体实现:
- 封装
- 继承
- 通过接口实现多态
go的结构体能不能比较?
结构体中含有不能比较的类型时,不能比较;
声明两个比较值的结构体的名字不同,即使字段名、类型、顺序相同,也不能比较(强转类型可以比较),说白了,必须用同一个结构体类型声明的值,才能比较;
sn1 := struct
age int
name string
age: 11, name: "qq"
sn2 := struct
age int
name string
age: 11, name: "qq"
//这种情况是可以比较的
if sn1 == sn2
fmt.Println("sn1 == sn2")
type s1 struct
age int
name string
type s2 struct
age int
name string
sn1 := s1age: 11, name: "qq"
sn2 := s2age: 11, name: "qq"
//这种情况,直接编译失败
if sn1 == sn2
fmt.Println("sn1 == sn2")
waitGroup对象,可以实现同一时间启动n个协程
一个waitGroup对象,可以实现同一时间启动n个协程,并发执行,等n个协程全部执行结束后,在继续往下执行的一个功能。
通过 Add() 方法设置启动了多少个协程,在每一个协程结束的时候调用 Done() 方法,计数减一,同时使用 wait() 方法阻塞主协程,等待全部的协程执行结束。
切片与数组的区别
共同点:
① 都是存储一系列相同类型的数据结构。
② 都可以通过下标来访问。
③ 都有 len 和 cap 这种概念。
不同点:
① 数组是定长的,且大小不能更改,是值类型。比如在函数参数传入的时候,形参和实参类型必须一模一样的。
② 切片是不定长的,容量是可以自动扩容的。切片传入函数的时候是值类型。
package main
import "fmt"
func main()
//创建一个长度和容量均为3的切片
arr := []int1, 2, 3
fmt.Println(arr) // [1 2 3]
//-------
addNum(arr)
//-------
fmt.Println(arr) // [1,2,3]
func addNum(sli []int)
//使用appedn添加"4"
sli = append(sli, 4)
fmt.Println(sli) // [1,2,3,4]
sli[0] = 666
fmt.Println(sli) // [666 2 3 4]
实际上,切片的传参是使用值传递。
函数能够对切片进行修改,是因为在函数中,拷贝切片所指的数组发生了变化,因此原切片的结果也发生变化。
func addNum(sli []int)
//使用appedn添加"4"
// sli = append(sli, 4)
// fmt.Println(sli) // [1,2,3,4]
sli[0] = 666
fmt.Println(sli) // [666 2 3 4]
PS E:\\TEXT\\test_go\\test\\case> go run .\\case.go
[1 2 3]
[666 2 3]
[666 2 3]
PS E:\\TEXT\\test_go\\test\\case>
答:一个方法就是用指针。
func main()
arr := []int1, 2, 3, 4
fmt.Println(arr) //[1 2 3 4]
// -----
addNum(&arr)
// -----
fmt.Println(arr) //[1 2 3 4 5]
func addNum(sli *[]int)
*sli = append(*sli, 5)
fmt.Println(*sli) //[1 2 3 4 5]
切片的创建
序号 | 方式 | 示例 |
---|---|---|
1 | 直接声明 | var slice []int |
2 | new | slice := *new([]int) |
3 | 字面量 | slice := []int1,2,3,4,5 |
4 | make | slice := make([]int,10) |
5 | 从切片或数组截取 | slice := array[:5] 或 slice := souceSlice[2:4] |
直接声明
这里重点说一下 nil 切片和空切片。
nil 切片
var slice []int
slice := *new([]int) //new前的*是解引用
空切片
slice := []int
slice := make([]int)
这两种方式的 len 和 cap 均为 0。
但是不同的是:
- nil 切片和 nil 的比较结果是 true。
- 空切片和 nil 的比较结果是 false,且同一程序里面,任何类型的空切片的底层数组指针的都指向同一地址。
为什么map的遍历是无需的?
① 遍历的起始位置每次都是随机的。
② 由于扩容,会导致 key 所处的桶发生变化。
map 的删除
key,value 清零。
对应位置的 tophash 置为 Empty。
1、所有Go版本通用方法
package main
import "fmt"
func main()
a := make(map[string]int)
a["a"] = 1
a["b"] = 2
fmt.Println(a)
// clear all
a = make(map[string]int)
fmt.Println(a)
PS E:\\TEXT\\test_go\\test\\case> go run .\\case.go
map[a:1 b:2]
map[]
PS E:\\TEXT\\test_go\\test\\case>
func TestMapDelete(t *testing.T)
var data = map[string]string
"name": "Elliot",
// "Elliot"
fmt.Println(data["name"])
delete(data, "name")
// 无任何输出
fmt.Println(data["name"])
nil map 和空 map 有何不同?
nil map表示未初始化的map,等同于 var m map[string]int
。
空 map 表示 map 已经被初始化,只是长度为 0,还并未赋于键值对。
- ① 直接读取
nil map:m[“a”]
并不会报错,会返回默认类型的空值。 - ② 直接给 nil map 赋值:
m[“a”] = 1
直接报错。 - ③ 需要通过 map == nil 来判断,是否为
nil map
。
map 中删除一个 key,它的内存会释放么?
① 如果删除的键值对都是值类型(int,float,bool,string以及数组和struct),map的内存不会自动释放。
② 如果删除的键值对中有(指针,slice,map,chan等),且该引用未被程序的其他位置使用,则该引用的内存会被释放,但是map中为存放这个类型而申请的内存不会被释放。
上述两种情况,map为存储键值所申请的空间,均不会被立即释放。等待GC到来,才会被释放。
③ 将map设置为nil后,内存被回收。
Student 结构值运行下面程序发生什么?
package main
import "fmt"
type Student struct
Name string
var list map[string]Student
func main()
list = make(map[string]Student)
student := Student"Aceld"
list["student"] = student
list["student"].Name = "LDB"
fmt.Println(list["student"])
编译失败:
map[string]Student
的 value 是一个 Student
结构值,所以当list[“student”] = student
,是一个值拷贝过程。
而 list[“student”]
则是一个值引用。
那么值引用的特点是只读。
所以对 list[“student”].Name = "LDB"
的修改是不允许的。
package main
import "fmt"
type Student struct
Name string
var list map[string]*Student
func main()
list = make(map[string]*Student)
student := Student"Aceld"
list["student"] = &student
list["student"].Name = "LDB"
fmt.Println(list["student"])
fmt.Println(list["student"].Name)
PS E:\\TEXT\\test_go\\test\\case> go run .\\case.go
&LDB
LDB
PS E:\\TEXT\\test_go\\test\\case>
2 channel 篇
并行与并发、进程与线程与协程
并行指物理上同时执行,并发指能够让多个任务在逻辑上交织执行的程序设计。
Go 缓冲通道与无缓冲通道的区别
1
无缓冲通道,在初始化的时候,不用添加缓冲区大小;
2
无缓冲通道的发送与接收(或者接收与发送)是同步的;
3
发送者的发送操作将被阻塞,直到有接收者接收数据;
接收者的接受操作将被阻塞,直到有发送者发送数据。
1
有缓冲通道,在初始化的时候,需要指定缓冲区大小;
2
有缓冲通道的发送与接收(或者接收与发送)是不同步的,也就是异步的;
3
有缓冲通道,缓冲区满后,再继续执行发送操作,会被阻塞。(其实无缓冲通道可以想象为是一个一直满的通道)
GMP 调度模型篇
- G代表协程;
- P代表协程处理器;
- M代表内核级线程。
什么是GMP模型?
当我们写一个并发程序,操作系统会对其进行调度,线程是操作系统调度的最小单位,而不是协程,所以GMP模型就是想办法将用户创建的众多协程分配到线程上的这么一个过程。
调度器有哪些设计策略?
复用线程:避免重复的创建、销毁线程,而是对线程的复用(work stealing机制和hand off机制)
抢占机制:一个协程占用cpu的时长是有时间限制的,当该协程运行超时之后,会被其他协程抢占,防止其他协程被饿死,
3 内存逃逸篇
什么是内存逃逸,为什么需要内存逃逸?
定义:
一个在栈区存储的变量,
因为被堆区的变量引用,
使得该变量会从栈区逃逸到堆区;
原因:
go 语言并不需要程序员像使用c/c++那样,需要自己去释放内存,go做了自动化处理。
所以go申请的局部变量(无论是 var 还是 new 申请的),只要没有超过一定大小,都被先分配到栈上,但是如果该变量被堆上的变量引用了得话,该变量必须逃逸到堆上,防止栈区的内存会被系统全部自动释放掉,从而导致被引用的变量丢失,产生野指针。
逃逸的堆区的变量,在需要被回收的时候,会被 GC 进行回收。
如何打印逃逸分析信息
逃逸分析在编译阶段进行,由编译器完成。
go run -gcflags "-m -l"<以上是关于Golang M 2023 7 * theory的主要内容,如果未能解决你的问题,请参考以下文章