目标:
整理go知识,主要包含如下内容:
1、基础
2、goroutine/channel/select
3、重要知识
4、kubernetes operator
第一部分 基础
1 main包作用是什么?包的特点是什么?包导入中的_作用是什么?包导入变量的可见性如何区分?
1)main函数保存在名为main的包中,如果不在,据不会生成可执行文件
2)go语言每个代码文件都属于一个包。所有处于同一个文件夹里的代码文件,必须使用同一个包名。
包和文件夹同名。
3) _ "github.com/goinaction/code/chapter2/sample/matchers"中
让 Go 语言对包做初始化操作,但是并不使用包里的标识符。为了让程序的
可读性更强,Go 编译器不允许声明导入某个包却不使用。下划线让编译器接受这类导入,并且
调用对应包内的所有代码文件里定义的 init 函数。
4)包导入变量
标识符以大写字母开头。以小写字母开头的
标识符是不公开的,不能被其他包中的代码直接访问。
2 如何跟踪所有goroutine?如何使用?
1) 使用 sync 包的 WaitGroup 跟踪所有启动的 goroutine。 WaitGroup 是一个计数信号量,判断是否所有的
goroutine都完成了工作。
2)
feeds, err := RetrieveFeeds()
// 创建一个无缓冲的通道,接收匹配后的结果
results := make(chan *Result)
// 构造一个 waitGroup,以便处理所有的数据源
var waitGroup sync.WaitGroup
// 设置需要等待处理
// 每个数据源的 goroutine 的数量
waitGroup.Add(len(feeds))
// 为每个数据源启动一个 goroutine 来查找结果
for _, feed := range feeds {
// 获取一个匹配器用于查找
matcher, exists := matchers[feed.Type]
if !exists {
matcher = matchers["default"]
}
// 启动一个 goroutine 来执行搜索
go func(matcher Matcher, feed *Feed) {
Match(matcher, feed, searchTerm, results)
waitGroup.Done()
}(matcher, feed)
}
// 启动一个 goroutine 来监控是否所有的工作都做完了
go func() {
// 等候所有任务完成
waitGroup.Wait()
// 用关闭通道的方式,通知 Display 函数
// 可以退出程序了
close(results)
}()
// 启动函数,显示返回的结果,并且
// 在最后一个结果显示
3 json解析
type Feed struct {
Name string `json:"site"`
URI string `json:"link"`
Type string `json:"type"`
}
每个字段的声明最后 ` 引号里的部分被称作标记(tag)
。这个标记里描述了 JSON 解码的元数据,
用于创建 Feed 类型值的切片。每个标记将结构类型里字段对应到 JSON 文档里指定名字的字段。
4 defer的作用?多个defer的解析顺序是怎么样的?
1) defer file.Close()
关键字 defer 会安排随后的函数调用在函数返回时才执行。在使用完文件后,需要主动关
闭文件。使用关键字 defer 来安排调用 Close 方法,可以保证这个函数一定会被调用。哪怕函
数意外崩溃终止,也能保证关键字 defer 安排调用的函数会被执行。关键字 defer 可以缩短打
开文件和关闭文件之间间隔的代码行数,有助提高代码可读性,减少错误。
2)后进先出顺序。
package main
import "fmt"
func main(){
defer fmt.Println("aaa")
defer fmt.Println("bbb")
}
输出结果为:
bbb
aaa
5 指针作用是什么?
使用指针可以在函数间或者 goroutine 间共享数据。
6 goroutine作用是什么?
通过启动 goroutine 和使用通道完成并发和同步。
7 什么是包?包和目录的关系?main包有什么作用?找包的过程?
1)每组文件被称为一个包
2)每个包在单独目录中。同一个目录的所有.go文件必须声明同一个包名。
3)将main包编译为二进制可执行文件。
可执行程序必须有main包。同时必须提供main函数。
4)编译器会首先查找 Go 的安装目录,
然后才会按顺序查找 GOPATH 变量里列出的目录。
8 远程导入是什么?命名导入是什么?
1)例如:
import "github.com/spf13/viper"
go get 将获取任意指定的 URL 的包,或者一个已经导入的包所依赖的其
他包。
2)命名导入是
指,在 import 语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。
样例:
import (
"fmt"
myfmt "mylib/fmt"
)
9 init函数作用是什么?如何导入和并使用一个包?
1)init 函数在 main 函数之前执行。用于初始化。
样例:
package postgres
import (
"database/sql"
)
func init() {
sql.Register("postgres", new(PostgresDriver))
}
2)使用空白标识符重命名这个导入可以让 init
函数发现并被调度运行,让编译器不会因为包未被使用而产生错误。
package main
使用空白标识符导入
包,避免编译错误。
import (
"database/sql"
_ "github.com/goinaction/code/chapter3/dbdriver/postgres"
)
func main() {
sql.Open("postgres", "mydb")
}
调用 sql 包提供的 Open 方法。该方法能
工作的关键在于 postgres 驱动通过自
己的 init 函数将自身注册到了 sql 包。
10 常用go命令有哪些?
1) go build
编译go文件,生成可执行二进制文件
样例:
go build hello.go
go build github.com/goinaction/code/chapter3/wordcount
编译所有包:
go build github.com/goinaction/code/chapter3/wordcount/...
2) go clean
删除编译生成的可执行文件
样例:
go clean hello.go
3) go run
11 go vender的原理?go bu工具原理?
1)其思想是把所有的依赖包复制到工程代码库中的目录里,然后使用工程内部的依赖包
所在目录来重写所有的导入路径。
2)gb 工具首先会在 $PROJECT/src/ 目录中查找代码,如果找不到,会在 $PROJECT/vender/src/
目录里查找。与工程相关的整个源代码都会在同一个代码库里。自己写的代码在工程目录的 src/
目录中,第三方依赖代码在工程目录的 vender/src 子目录中。这样,不需要配合重写导入路
径也可以完成整个构建过程
gb build all
12 数组特点是什么?如何声明?如何使用?多维数组如何使用?如何在函数间传递数组?数组的原理是什么?
1)长度固定的数据类型
2)
var array [5]int
array := [5]int{10, 20, 30, 40, 50}
// 容量由初始化值的数量决定
array := [...]int{10, 20, 30, 40, 50}
// 声明一个有 5 个元素的数组
// 用具体值初始化索引为 1 和 2 的元素
// 其余元素保持零值
array := [5]int{1: 10, 2: 20}
3)
// 声明包含 5 个元素的指向整数的数组
// 用整型指针初始化索引为 0 和 1 的数组元素
array := [5]*int{0: new(int), 1: new(int)}
// 为索引为 0 和 1 的元素赋值
*array[0] = 10
*array[1] = 20
4)
复制数组指针,只会复制指针的值,而不会复制指针所指向的值
// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化外层数组中索引为 1 个和 3 的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}
// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将外层数组的索引为 1、内层数组的索引为 0 的整型值复制到新的整型变量里
var value int = array1[1][0]
5)
使用指针在函数间传递大数组
// 分配一个需要 8 MB 的数组
var array [1e6]int
// 将数组的地址传递给函数 foo
foo(&array)
// 函数 foo 接受一个指向 100 万个整型值的数组的指针
func foo(array *[1e6]int) {
...
}
这次函数 foo 接受一个指向 100 万个整型值的数组的指针。现在将数组的地址传入函数,
只需要在栈上分配 8 字节的内存给指针就可以。
13 什么是切片?切片由哪些部分组成?如何使用?切片与数组的区别?创建nil切片?使用切片?如何计算长度和容量?两个切片的关系是什么?
1)切片是围绕动态数组的概念
构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。
2)
切片包含指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长
到的元素个数(即容量)
3)
// 创建一个字符串切片
// 其长度和容量都是 5 个元素
slice := make([]string, 5)
// 创建一个整型切片
// 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
4)
// 创建字符串切片
// 使用空字符串初始化第 100 个元素
slice := []string{99: ""}
记住,如果在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片。只有不指定值
的时候,才会创建切片,
/ 创建有 3 个元素的整型数组
array := [3]int{10, 20, 30}
// 创建长度和容量都是 3 的整型切片
slice := []int{10, 20, 30}
5)nil切片指针为空,长度和容量都为0
创建 nil 切片
// 创建 nil 整型切片
var slice []int
// 使用 make 创建空的整型切片
slice := make([]int, 0)
// 使用切片字面量创建
6)
// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
7)
对底层数组容量是 k 的切片 slice[i:j]来说
长度: j - i
容量: k - i
举例:
对底层数组容量是 5 的切片 slice[1:3]来说
长度: 3 - 1 = 2
容量: 5 - 1 = 4
注意:切片中最后索引对应的元素取不到,例如
slice[1:3],则元素slice[3]无法取到。
8)
两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享
部分,另一个切片也能感知到
14 切片如何增长?append同时增加切片长度和容量的原理?创建切片时的第3个索引作用是什么?如何计算长度和容量?有什么陷阱?如何追加切片?
1)使用 append ,需要一个被操作的切片和一个要追加的值,返回新切片。
append 总是会增加新切片的长
度,而容量有可能会改变,也可能不会改变
样例:
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice = append(newSlice, 60)
2)
切片的容量小于 1000 个元素时,总是
会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25%
的容量
样例:
使用 append 同时增加切片的长度和容量
// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}// 向切片追加一个新元素
// 将新元素赋值为 50
3)
第三个索引可以用来控制
新切片的容量。
newSlice := append(slice, 50)
// 将第三个元素切片,并限制容量
// 其长度为 1 个元素,容量为 2 个元素
slice := source[2:3:4]
4)
如何计算长度和容量
对于 slice[i:j:k] 或 [2:3:4]长度: j – i 或 3 - 2 = 1
容量: k – i 或 4 - 2 = 2
和之前一样,第一个值表示新切片开始的元素的索引位置,这个例子中是 2。第二个值表示
开始的索引位置(2)加上希望包括的元素的个数(1)
,2+1 的结果是 3,所以第二个值就是 3。为
了设置容量,从索引位置 2 开始,加上希望容量中包含的元素的个数(2)
,就得到了第三个值 4。
5)
append 会首先使用可用容量。一旦没有可用容量,会分配一个
新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。
导致对切片修改影响其他切片。
解决方法:
强制让新切片的第一个 append 操作
创建新的底层数组,与原有的底层数组分离。
样例:
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 对第三个元素做切片,并限制容量
// 其长度和容量都是 1 个元素
slice := source[2:3:3]
// 向 slice 追加新字符串
slice = append(slice, "Kiwi")
6)使用 ... 运算符,可以将一个切片的所有元素追加到另一个切片
样例:
s1 := []int{1, 2}
s2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果
fmt.Printf("%v\n", append(s1, s2...))
15 如何迭代切片?len函数和cap函数作用?多维切片如何声明?函数间如何传递切片?
1)用range迭代,返回索引和对应元素值的副本。
slice := []int{10, 20, 30, 40}// 迭代每一个元素,并显示其值
for index, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
注意:
如果使用该值变量的地址作为指向每个元素的指针,就会造成错误。
// 从第三个元素开始迭代每个元素
for index := 2; index < len(slice); index++ {
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
2)函数 len返回切片的长度,函数 cap 返回切片的容量。
3)
// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为 20 的元素
slice[0] = append(slice[0], 20)
原理:
先增长切片,再将新的整型切片赋值
给外层切片的第一个元素。
4) 在函数间传递切片就是要在函数间以值的方式传递切片。
// 分配包含 100 万个整型值的切片
slice := make([]int, 1e6)
// 将 slice 传递到函数 foo
slice = foo(slice)
// 函数 foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
...
return slice
}
一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量字段分别需要 8 字节。由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片
复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底
层数组
16 映射是什么? 内部如何实现的?如何使用?如何在函数间传递映射?
1)存储键值对的无序集合
2) 底层使用了三列表。
无法预测键值对的返回顺序。
映射的散列表包含一组桶。在存储、删除或者查找键值对的时候,所有操作都要先选择一个
桶。把操作映射时指定的键传给映射的散列函数,就能选中对应的桶。这个散列函数的目的是生
成一个索引,这个索引最终将键值对分布到所有可用的桶里。
字符串会转换为一个数值(散列值)
。这个数值落在映射已有桶的序号范围内表示一个可以用于存储的桶的序号。之后,这个数值就被用于
选择桶,用于存储或者查找指定的键值对。对 Go 语言的映射来说,生成的散列键的一部分,具
体来说是低位(LOB),被用来选择桶。映射使用两个数据结构来存储数据。第一
个数据结构是一个数组,内部存储的是用于选择桶的散列键的高八位值。这个数组用于区分每个
键值对要存在哪个桶里。第二个数据结构是一个字节数组,用于存储键值对。该字节数组先依次存储了这个桶里所有的键,之后依次存储了这个桶里所有的值。
实现这种键值对的存储方式目的在于减少每个桶所需的内存。
3)
dict := make(map[string]int)
// 删除键为 Coral 的键值对
delete(colors, "Coral")
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}
//判断是否存在
if actionRef, ok := action["ref"]; ok {
id := actionRef.(string)
ruleMap[id] = rule
}
4)
在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对
这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改
// removeColor 将指定映射里的键删除
func removeColor(colors map[string]string, key string) {
delete(colors, key)
}
17 Go语言为什么是一种静态类型的编程语言?如何声明用户定义的类型?关键字 var作用?如何基于 int64 声明一个新类型?
1)编译器需要在编译时知晓程序里每个值的
类型。
2)用struct
type user struct {
name string
email string
}
3)更明确地表示一个变量被设置为零值
4) 给不同类型的变量赋值会产生编译错误
type Duration int64
package main
type Duration int64
func main() {
var dur Duration
dur = int64(1000)
}
上述会产生错误。
18 什么是方法?如何使用?
1)方法能给用户定义的类型添加新的行为
在关键字func 和方法名之间增加了一个参数。
2)
样例:
type user struct {
name string
email string
}
// changeEmail 使用指针接收者实现了一个方法
func (u *user) changeEmail(email string) {
u.email = email
}
19 什么是接受者?接收者有哪些类型?直接收者与指针接收者的区别在哪里?
1)关键字 func 和函数名之间的
参数被称作接收者,将函数与接收者的类型绑在一起。如果一个函数有接收者,这个函数就被称
为方法。
2)值接收者和指针接收者。
3)
值接收者: 使用值的副本来调用方法。如果结构体很大,占据空间,且不能修改结构体中成员变量。
指针接收者: 使用实际值来调用方法。适用于修改结构体中成员变量。
20 内置类型有哪些?引用类型有哪些?什么是标头值?
1)值类型
种类: 数值类型、字符串类型和布尔类型。
这些类型本质上是原始的类型。因此,当对这些值进行增加或者删除的时候,会创
建一个新值。
2)引用类型
种类: 切片、映射、通道、接口和函数类型
3) 引用类型创建的变量叫做标头值。
每个引用类型创建的标头值是包含一个指向底层数据结构的指针。每个引用类型还包含一组独特
的字段,用于管理底层数据结构。因为标头值是为复制而设计的,所以永远不需要共享一个引用
类型的值。标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是
在共享底层数据结构。
21 接口是什么?接口如何实现?接口值内部原理是什么?接口如何使用?
1)接口是声明了一组行为并支持多态的类型。
2)这些被定义的行为不由接口直接实现,而是通过方法由用户
定义的类型实现。
3)接口变量的值由:
类型 + 值
指向内部表的指针iTable(包含所存储的值的类型信息) + 指向所存储值的指针
样例:
var n notifier
n = user('chao')
4)
// notifier 是一个定义了
// 通知类行为的接口
type notifier interface {
notify()
}
// user 在程序里定义一个用户类型
type user struct {
name string
email string
}
// notify 是使用指针接收者实现的方法
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}
// main 是应用程序的入口
func main() {
// 创建一个 user 类型的值,并发送通知30
u := user{"Bill", "bill@email.com"}
sendNotification(&u)
// ./listing36.go:32: 不能将 u(类型是 user)作为
//
sendNotification 的参数类型 notifier:
// user 类型并没有实现 notifier
//
(notify 方法使用指针接收者声明)
}
// sendNotification 接受一个实现了 notifier 接口的值
// 并发送通知
func sendNotification(n notifier) {
n.notify()
}
22 什么是方法集?
1)方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决
定了这个方法是关联到值,还是关联到指针,还是两个都关联。
2)T 类型的值的方法集只包含值
接收者声明的方法。而指向 T 类型的指针的方法集既包含值接收者声明的方法,也包含指针接收
者声明的方法。
Methods Receivers Values
-----------------------------------------------
(t T) T and *T
(t *T) *T
代码清单 5-43 展示了同样的规则,只不过换成了接收者的视角。这个规则说,如果使用指
针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值
接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。
23 定义接口 + 定义基类 + 子类内嵌承基类,并实现接口 + 定义使用类中的某个成员变量为该接口类型
具体如下:
type ResourceIF interface {
GetUrlPathName() string
}
type ResourceManager struct {
Resource ResourceIF
Endpoint string
}
type Resource struct {
UrlPath string
Attributes []string
}
type ActionResource struct {
Resource
}
func (r *ActionResource) GetUrlPathName() string {
if r.UrlPath == "" {
r.UrlPath = "actions"
}
return r.UrlPath
}
type RuleResource struct {
Resource
}
func (r *RuleResource) GetUrlPathName() string {
if r.UrlPath == "" {
r.UrlPath = "rules"
}
return r.UrlPath
}
func (r ResourceManager) GetById(id string, kwargs map[string]interface{}) (string, error) {
url := "/" + r.Resource.GetUrlPathName() + "/" + id
pathParams, realKwargs := r.getPathAndKwargParams(kwargs)
result, err := r.Client.Get(url, pathParams, realKwargs)
return result, err
}
24 什么是嵌入类型?
1) 将已有的类型直接声明在新的结构类型中。
嵌入类型提供了扩展类型的能力,而无需使用继承。
起到的作用有点类似于继承获得了父类的方法或成员变量。
类似:
type Resource struct {
UrlPath string
Attributes []string
}
type ActionResource struct {
Resource //嵌入类型
}
要嵌入一个类型,只需要声明这个类型的名字就可以了
第二部分 goroutine/channel/select
1 go并发是什么意思?goroutine原理是什么?并发同步模型是什么?goroutine如何调度?
Go线程实现模型MPG是什么?阻塞时如何处理?如何均衡处理工作?
1)能让某个函数独立于其他函数运行的能力。
2)一个函数创建为 goroutine时,Go 会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。
用协程在应用层模拟线程,避免上下文切换。内部调用操作系统异步io函数例如epoll,
当返回busy或blocking,会拉取另一个协程代码执行。
3)通信顺序进程(Communicating Sequential Processes, CSP)
的范型(paradigm)
。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是
对数据进行加锁来实现同步访问。
4)Go 语言的运行时会在逻辑处理器上调度
goroutine来运行。每个逻辑处理器都分别绑定到单个操作系统线程。
如果创建一个 goroutine 并准备运行,这个 goroutine 就会被放到调度器的全局运行队列中。之后,调度器就
将这些队列中的 goroutine 分配给一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列
中。本地运行队列中的 goroutine 会一直等待直到自己被分配的逻辑处理器执行。
总结:
goroutine被放入调度器全局队列->
给goroutine分配逻辑处理器->
放入逻辑处理器对应本地运行队列。
goutine等待知道自己被分配的逻辑处理器执行。
5) MPG模型中,
M:machine,对应内核线程
P:processor,代表M的上下文环境,将待执行的G与M对接。P的数量由GOMAXPROCS决定。
G:goroutine,本质是量项籍线程,包括:调用栈和调度信息。
模型如下:
KSE KSE
|_________|___________ 内核空间
M M 用户空间
| |
P-----G P-----G
| | | |
G G G G
MPG模型处理过程:
runqueue队列假如goroutine,一旦可调度,从runqueue队列中
取出goroutine,设置堆栈和指令指针运行goroutine。
6) 通过上下文processor,当遇到内核阻塞时,会
放开对应线程。放入Groutine到全局runqueue中。
P周期性检查全局runqueue中的goroutine。
7) P处理完本身的goroutine会从其他P中获取一半
的goroutine。
参考:
https://blog.csdn.net/qq_34147021/article/details/85654879
https://segmentfault.com/a/1190000018150987
2 goroutine基本调度模型?阻塞调度模型?网络I/O调度模型?调度策略是什么?
1)基本调度模型
goroutine被调度到逻辑处理器,逻辑处理器绑定线程,当goroutine可以运行的时候,
会被放入到该逻辑处理器的执行队列中。
线程M2
|
逻辑处理器PO----- G5
| |
正在运行的G4 G6
|
G7
2)阻塞调度模型
当goroutine执行阻塞调用时,调度器将这个线程与处理器分离,并创建一个新线程来运行
这个处理器上提供的服务。
线程M3
|
逻辑处理器P0----- G6 线程M2
| | |
正在运行的G5 G7 被阻塞的G4
3)网络I/O调度模型
如果需要执行网络I/O调用,goroutine会和逻辑处理器分离,
并放入网络轮询器中,若可读或科协,则goroutine重新分配到逻辑处理器完成操作。
4)调度策略
正在运行的goroutine结束前可被停止并重新调度,防止长时间
占据逻辑处理器
3 什么是用户态与内核态?用户态与内核态是如何切换的?为什么要有用户态与内核态?
1)内核态控制计算机硬件资源例如cpu,内存,I/O资源,提供上层应用运行环境,运行在高级别特权。
用户态是上层应用程序的活动空间,运行在低特权级别,必须通过系统调用等来获取内核空间资源。
2)用户态切换到内核态,主要是通过:
系统调用,异常,外围设备中断。
系统调用是申请使用操作系统提供的服务;
异常触发进入到处理异常的内核中;
外围设备中断是完成用户请求向cpu发出中断信号,暂停执行下一条指令而去
执行中断对应的处理程序。
3)限制不同程序访问能力。
参考:
https://www.cnblogs.com/maxigang/p/9041080.html
https://www.nowcoder.com/questionTerminal/e72c6b33bb874d27883b5254e58aa403?orderByHotValue=1&page=1&onlyReference=false
4 为什么不用线程而使用goroutine?goroutine具体是操作内核态还是用户态?goroutine线程模型是什么?协程与线程的主要区别是什么?
1)goroutine占据内存比线程少:goroutine 2KB, 线程: 8MB。
避免频繁切换上下文的开销。
2)goroutine操作是用户态。
线程切换需要先进入内核,然后进行上下文切换;
协程在用户态,代价少。协程切换是由调度器决定,线程切换是系统内核决定。
3)
现在的线程往往是用户态线程,和操作系统本身内核态线程kse有区别。
goroutine采用多对多模型(M:N),即多个用户线程映射到多个内核线程。
4)线程被内核调度,协程被程序自身调度。所以需要golang调度器。
进程是分配资源的基本单位,隔离。
线程是独立运行与调度的基本单位。
协程是用户态轻量级线程。
coroutine: 是执行序列,有独立栈和局部变量,与其他线程共享全局变量。非阻塞。执行顺序由程序控制。
goroutine:与其他goroutine并发运行在同一地址空间的Go函数或方法。
参考:
https://blog.csdn.net/qq_34147021/article/details/85654879
https://segmentfault.com/a/1190000018150987
https://blog.csdn.net/qingyuanluofeng/article/details/83118899
5 请编写一个goroutine?runtime库用法?
1)
package main
import (
"fmt"
"runtime"
"sync"
)
func main(){
//分配一个逻辑处理器给调度器使用
runtime.GOMAXPROCS(1)
// WaitGroup等待程序完成,本质时计数器,任务完成则计数器减一
// Add会让计数器+2,表示要等待两个goroutine
var wg sync.WaitGroup
wg.Add(2)
fmt.Println("############# Begin goroutine")
// 声明匿名函数,并创建goroutine
go func(){
// 在函数退出时执行Done通知main函数工作完成
defer wg.Done()
fmt.Println("Task 1 done.")
}()
go func(){
// 在函数退出时执行Done通知main函数工作完成
defer wg.Done()
fmt.Println("Task 2 done.")
}()
fmt.Println("Wait to finish.")
wg.Wait()
fmt.Println("############# End goroutine")
}
解释:
sync.WaitGroup是计数信号量,记录并维护运行的goroutine。
若值大于0,则阻塞。
2)runtime 包里有一个名为 GOMAXPROCS 的函数,
通过它可以指定调度器可用的逻辑处理器的数量。用这个函数,可以给每个可用的物理处理器在
运行的时候分配一个逻辑处理器。
// 给每个可用的核心分配一个逻辑处理器
runtime.GOMAXPROCS(runtime.NumCPU())
6 什么是竞争状态?如何检测?
1)多个goroutine读写共享资源
2)用go build -race
7 什么是通道?channel的内部结构是什么?
1)用于在 goroutine 之间同步和传递数据的关键数据类型
2)
每个channel内部实现都有三个队列
接收消息的协程队列。这个队列的结构是一个限定最大长度的链表,所有阻塞在channel的接收操作的协程都会被放在这个队列里。
发送消息的协程队列。这个队列的结构也是一个限定最大长度的链表。所有阻塞在channel的发送操作的协程也都会被放在这个队列里。
环形数据缓冲队列。这个环形数组的大小就是channel的容量。如果数组装满了,就表示channel满了,如果数组里一个值也没有,就表示channel是空的。对于一个阻塞型channel来说,它总是同时处于即满又空的状态。
8 什么是主线程?
1)每个进程至少包含一个线程,每个进程的初始线程被称作主线程。因为执行
这个线程的空间是应用程序的本身的空间,所以当主线程终止时,应用程序也会终止。操作系统
将线程调度到某个处理器上运行,这个处理器并不一定是进程所在的处理器。
9 什么是原子函数?如何使用?
1) 原子函数用加锁来访问变量。
2)
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
waitGroup sync.WaitGroup
)
func main(){
waitGroup.Add(2)
go incCounter(1)
go incCounter(2)
waitGroup.Wait()
fmt.Println("Counter: ", counter)
}
func incCounter(id int){
defer waitGroup.Done()
for i := 0; i < id; i++{
atomic.AddInt64(&counter, 1)
//当前goroutine从线程退出,并放回到队列
//runtime.Gosched(),用于让出CPU时间片,让出当前goroutine的执行权限,
//调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。
//这就像跑接力赛,A跑了一会碰到代码runtime.Gosched()就把接力棒交给B了,A歇着了,B继续跑。
runtime.Gosched()
}
}
LoadInt64 和 StoreInt64 。这两个函数提供了一种安全地读
和写一个整型值的方式
10 什么是互斥锁?如何使用?
1)互斥锁在代码创建临界区,保证统一时间只有一个goroutine
可以执行。
2)
import (
"fmt"
"runtime"
"sync"
)
var (
wGroup sync.WaitGroup
mutex sync.Mutex
count int64
)
func main(){
wGroup.Add(2)
go increment(1)
go increment(2)
wGroup.Wait()
fmt.Println("Count: ", count)
}
func increment(num int){
defer wGroup.Done()
for i:=0; i < num; i++{
mutex.Lock()
{
value := count
runtime.Gosched()
value++
count = value
}
mutex.Unlock()
}
}
11 通道作用?有哪些种类的通道?如何使用?
1) 通过发送和接收共享资源,在goroutine之间同步
2)无缓冲通道,有缓冲通道
unbuffer := make(chan int)
buffer := make(chan string, 10)
无缓冲通道: 发送goroutine和接收goroutine同时准本好。本质是同步
3)创建管道
make(chan <数据类型>, 缓冲区大小)
channel操作:
发送: 将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine
接收:
发送和接收两个操作都使用<- 运算符。
发送语句中: <- 运算符分割channel和要发送的值,发送语句中channel在前
ch <- value, 发送必定含有值
接收语句中: <- 运算符写在channel对象之前
x = <-ch 覆值
总结: channel在最左边是发送,在最右边是接收。
左发右接
12 请编写一个无缓冲通道。
import (
"fmt"
"math/rand"
"sync"
"time"
)
func init(){
rand.Seed(time.Now().UnixNano())
}
func main(){
var wg sync.WaitGroup
wg.Add(2)
court := make(chan int)
go play(&wg, court, "ma")
go play(&wg, court, "yang")
round := 0
court <- round
wg.Wait()
}
func play(wg* sync.WaitGroup, court chan int, name string){
defer wg.Done()
for {
round, ok := <- court
if !ok{
fmt.Printf("%s win\n", name)
return
}
n := rand.Intn(10)
if n % 9 == 0{
close(court)
return
}
round++
fmt.Printf("%s play round: %d\n", name, round)
court <- round
}
}
13 什么是有缓冲通道?与无缓冲通道区别?请编写一个有缓冲通道。
1) 有缓冲通道在被接收前能存储一个或多个值。
2) 无缓冲通道保证发送和接收goroutine在同一时间进行数据交换;
有缓冲通道没有这种保证。
3)
import (
"fmt"
"math/rand"
"sync"
"time"
)
func init(){
rand.Seed(time.Now().Unix())
}
func main(){
var wg sync.WaitGroup
goroutineNum := 3
taskNum := 8
wg.Add(goroutineNum)
taskChannel := make(chan string, taskNum)
for i:= 0; i < goroutineNum; i++{
go worker(&wg, taskChannel, i)
}
for i:=0 ;i < taskNum; i++{
taskChannel <- fmt.Sprintf("Task %d", i)
}
//当所有工作处理完成时关闭通道
close(taskChannel)
wg.Wait()
}
func worker(wg *sync.WaitGroup, taskChannel chan string, workerId int){
defer wg.Done()
for{
task, ok := <- taskChannel
if !ok{
fmt.Printf("Worker %d task finished\n", workerId)
return
}
fmt.Printf("Worker: %d, begin task: %s\n", workerId, task)
sleep := rand.Int63n(100)
time.Sleep(time.Duration(sleep) * time.Millisecond)
fmt.Printf("Worker: %d, finish task: %s\n", workerId, task)
}
}
14 请编写一个可以并发执行并返回结果的代码。
package main
import (
"fmt"
"sync"
)
func main(){
nums := []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
dataLen := len(nums)
resultChan := make(chan Result, dataLen)
processMain(nums, resultChan)
result := getResult(resultChan)
fmt.Println(result)
}
type Result struct{
result int
err error
}
//等待全部goroutine执行完成以后,才关闭通道
func processMain(nums []int, resultChan chan Result){
var wg sync.WaitGroup
for _, value:= range nums{
wg.Add(1)
go processOne(value, resultChan, &wg)
}
wg.Wait()
close(resultChan)
}
func processOne(num int, resultChan chan Result, wg *sync.WaitGroup){
defer wg.Done()
result := num + 1
resultObj := Result{result: result, err: nil}
resultChan <- resultObj
}
func getResult(resultChan chan Result)[]int{
fmt.Println("###### Begin getResult")
results := []int{}
for result := range resultChan{
fmt.Printf("value: %d\n", result)
realResult := result.result
err := result.err
if err == nil{
results = append(results, realResult)
}
}
fmt.Println("###### End getResult")
return results
}
关键:
1)并发获取结果的主要逻辑:
主处理函数中设置sync.WaitGroup类型的wg,每开启一个goroutine之前wg加1,
然后执行goroutine时,将处理后的结果塞入通道,wg减1,等待wg完成,此时关闭通道。
另外一个方法则从通道中获取值。
第三部分 重要知识
1 go struct能不能比较?
go是强类型语言,不通类型结构不能比较,同一类型实例可以比较,
实例不可比较,因为是指针类型。
2 go defer顺序是什么?
后进先出
3 select可以用于什么?原理是什么?
1)用于goroutine的完美退出
2) select监听I/O操作,每个case对应channel的I/O操作
4 context包的作用是什么?
context即上下文,是存在上下层的传递。
5 主协程如何等其余协程完成?
使用channel通信,context,select。
6 slice,len,cap,共享,扩容?
slice底层数据结构是len, cap, 数组组成,append扩容时
如果容量cap足够就直接添加;否则,就重新生成一个大的数组。
7 map如何顺序读取?
map不能顺序读取,若要顺序读取,先把key放入切片变成有序,
遍历切片,获取值。
8 实现set
type Set struct{
m map[interface{}]bool
s sync.RWMutex
}
//返回Set指针,New方法没有入参
func New() *Set{
dataMap := make(map[interface{}]bool)
set = Set{m: dataMap}
return &set
}
func Add(s *Set)(key interface{}){
s.Lock()
defer s.Unlock()
s.m[key] = true
}
9 如何实现消息队列(多生产者,多消费者)?
使用切片加锁实现。
10 大文件排序
归并排序,拆分为小文件后排序。
11 http get跟head的区别是什么?http 400 401,403分别是什么?
http keep-alive是什么?http能不能一次连接多次请求,不等后端返回?
1) head仅仅是http头信息。
2) 400是请求语法错误;
401请求未认证;
403请求被禁止。
3) client在head添加Connection:keep-alive,server在response中指定该字段,
告诉client能提供keep-alive服务。
4) http本质是使用socket连接,发送请求写入tcp缓冲。
12 什么是interface?什么是空接口?Go反射原理是什么?
接口类型的底层实现是什么?Go的反射包怎么找到对应的方法?
1)一个接口类型代表了一组固定的方法。
一个接口变量可以存储任意合适的值,只要这个值实现了这个接口的所有方法。
2)空接口类型interface{}。空接口类型的方法集合为空集,意味着任意类型都实现了空接口,
也就是说空接口类型的变量能够存储任意类型的值。
3)动态地获取空接口类型变量的实际类型,并更改它的值,来实现我们的反射机制。
4)接口类型的变量,它其实是由两部分组成,被赋的值的拷贝,和被赋的值的类型描述器。例如上面代码中的i变量,我们可以用这样一个二元组来表示: (c, Cat),代表了变量c和它的类型Cat。
5)接口变量是由值和值类型两部分组成的,反射相关函数主要就是获取这两部分。
当我们调用reflect.ValueOf方法的时候,就是获取接口中的变量,并使用一个reflect.Value类型的变量来代表它。
同理,当我们使用reflect.TypeOf方法的时候,它获取的就是接口中存储的变量类型,并使用一个reflect.Type类型的变量来代表它。
reflect.TypeOf方法的声明如下: func TypeOf(i interface{}) Type。
它接收一个空接口类型变量i作为参数,返回一个Type变量,代表了传入参数的类型。
reflect.ValueOf方法的声明如下:func ValueOf(i interface{}) Value
它返回一个Value变量代表传入参数i运行时的数据。
Kind 类型
reflect.Value和reflect.Type类型有一个Kind()方法,它返回一个reflect.Kind类型的变量,代表了反射类型reflect.Type的具体分类。需要注意的是,Kind方法返回的是底层类型,而不是静态声明的类型,如果我们声明了自定义类型type MyInt int,通过Value.Kind()获取的类型仍然为int。
package main
import (
"fmt" "reflect"
)
type User struct {
Name string Age int
}
type MyInt int
func main() {
u := User{"xff", 19}
var i MyInt = 42
t := reflect.TypeOf(u) // reflect.Struct是一个reflect.Kind类型的常量,代表了结构体类型
fmt.Println(t.Kind() == reflect.Struct) // 输出 true
t = reflect.TypeOf(i) // 这里变量i的类型是我们自定义的类型MyInt,但是Type.Kind()方法返回的类型是reflect.Int fmt.Println(t.Kind() == reflect.Int) // 输出true
}
参考:
https://blog.csdn.net/u012291393/article/details/78378386
13 sync.Pool为什么使用?那里面的对象是固定的吗?
1)对象池,避免频繁分配对象(GC有关)。
2)
参考:
https://www.cnblogs.com/sunsky303/p/9706210.html
https://www.jianshu.com/p/494cda4db297
14 go的锁如何实现?用了什么cpu指令?
1)type Mutex struct {
state int32 //互斥锁上锁状态枚举值如下所示
sema uint32 //信号量,向处于Gwaitting的G发送信号
}
const (
mutexLocked = 1 << iota // 1 互斥锁是锁定的
mutexWoken // 2 唤醒锁
mutexWaiterShift = iota // 2 统计阻塞在这个互斥锁上的goroutine数目需要移位的数值
)
上面的state值分别为 0(可用) 1(被锁) 2~31等待队列计数
下面是互斥锁的源码,这里会有四个比较重要的方法需要提前解释,分别是runtime_canSpin,runtime_doSpin,runtime_SemacquireMutex,runtime_Semrelease.
A、runtime_canSpin:比较保守的自旋,golang中自旋锁并不会一直自旋下去,在runtime包中runtime_canSpin方法做了一些限制, 传递过来的iter大等于4或者cpu核数小等于1,最大逻辑处理器大于1,至少有个本地的P队列,并且本地的P队列可运行G队列为空。
//go:linkname sync_runtime_canSpin sync.runtime_canSpin
func sync_runtime_canSpin(i int) bool {
if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
return false
}
if p := getg().m.p.ptr(); !runqempty(p) {
return false
}
return true
}
B、 runtime_doSpin:会调用procyield函数,该函数也是汇编语言实现。函数内部循环调用PAUSE指令。PAUSE指令什么都不做,但是会消耗CPU时间,在执行PAUSE指令时,CPU不会对它做不必要的优化。
//go:linkname sync_runtime_doSpin sync.runtime_doSpin
func sync_runtime_doSpin() {
procyield(active_spin_cnt)
}
C、runtime_SemacquireMutex:
//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
func sync_runtime_SemacquireMutex(addr *uint32) {
semacquire(addr, semaBlockProfile|semaMutexProfile)
}
D、runtime_Semrelease:
//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
func sync_runtime_Semrelease(addr *uint32) {
semrelease(addr)
}
Mutex的Lock函数定义如下
func (m *Mutex) Lock() {
//先使用CAS尝试获取锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
//这里是-race不需要管它
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
//成功获取返回
return
}
awoke := false //循环标记
iter := 0 //循环计数器
for {
old := m.state //获取当前锁状态
new := old | mutexLocked //将当前状态最后一位指定1
if old&mutexLocked != 0 { //如果所以被占用
if runtime_canSpin(iter) { //检查是否可以进入自旋锁
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
//awoke标记为true
awoke = true
}
//进入自旋状态
runtime_doSpin()
iter++
continue
}
//没有获取到锁,当前G进入Gwaitting状态
new = old + 1<<mutexWaiterShift
}
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
//清除标记
new &^= mutexWoken
}
//更新状态
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&mutexLocked == 0 {
break
}
// 锁请求失败,进入休眠状态,等待信号唤醒后重新开始循环
runtime_SemacquireMutex(&m.sema)
awoke = true
iter = 0
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
Mutex的Unlock函数定义如下
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 移除标记
new := atomic.AddInt32(&m.state, -mutexLocked)
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
old := new
for {
//当休眠队列内的等待计数为0或者自旋状态计数器为0,退出
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 {
return
}
// 减少等待次数,添加清除标记
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 释放锁,发送释放信号
runtime_Semrelease(&m.sema)
return
}
old = m.state
}
}
互斥锁无冲突是最简单的情况了,有冲突时,首先进行自旋,,因为大多数的Mutex保护的代码段都很短,经过短暂的自旋就可以获得;如果自旋等待无果,就只好通过信号量来让当前Goroutine进入Gwaitting状态。
2)自旋对应于CPU的"PAUSE"指令,CPU对该指令什么都不做,对程序而言相当于sleep了一小段时间,当前实现是30个时钟周期。
自旋过程中会持续探测Locked是否变为0,连续两次探测间隔就是执行这些PAUSE指令,它不同于sleep,不需要将协程转为睡眠状态。
参考:
https://www.jb51.net/article/117512.htm
https://blog.csdn.net/weixin_34208283/article/details/91699624
15 go什么情况下会发生内存泄漏?
ctx没有cancel的时候
16 怎么实现协程完美退出?
1)使用for-range退出
range能够感知channel的关闭,当channel被发送数据的协程关闭时,range就会结束,接着退出for循环。
o func(in <-chan int) {
// Using for-range to exit goroutine
// range has the ability to detect the close/end of a channel
for x := range in {
fmt.Printf("Process %d\n", x)
}
}(inCh)
2)使用退出通道退出
使用一个专门的通道,发送退出的信号,可以解决这类问题。以第2个场景为例,协程入参包含一个停止通道stopCh,当stopCh被关闭,case <-stopCh会执行,直接返回即可。
func worker(stopCh <-chan struct{}) {
go func() {
defer fmt.Println("worker exit")
// Using stop channel explicit exit
for {
select {
case <-stopCh:
fmt.Println("Recv stop signal")
return
case <-t.C:
fmt.Println("Working .")
}
}
}()
return
}
参考:
https://blog.csdn.net/ytd7777/article/details/90239220
17 用channel实现定时器?
18 怎么理解go的interface?
19 多个线程读,一个线程写一个int32会不会有问题,int64呢?
看数据总线的位数,32位的话写int32没问题,int64就有问题
20 在go语言中,new和make的区别是什么?
new: 是初始化一个指向类型的指针*T,func new(Type) *Type
make: 为slice,map等初始化并返回引用T, func make(Type, size IntergerType)Type
21 Printf()、Sprintf()、Fprintf()函数的区别用法是什么?
Printf(),是把格式字符串输出到标准输出
Sprintf(),是把格式字符串输出到指定字符串中,所以参数比printf多一个char*。
Fprintf(), 是把格式字符串输出到指定文件设备中,所以参数笔printf多一个文件指针FILE*。
22 数组与切片的区别?
数组长度固定不可改变,值传递。var array [10]int
切片长度可变,有三属性: 指针,长度和容量,无需指定大小,是地址传递。var slice []int = make([]int)
23 解释以下命令的作用?
go env: #用于查看go的环境变量
go run: #用于编译并运行go源码文件
go build: #用于编译源码文件、代码包、依赖包
go get: #用于动态获取远程代码包
go install: #用于编译go文件,并将编译结构安装到bin、pkg目录
go clean: #用于清理工作目录,删除编译和安装遗留的目标文件
go version: #用于查看go的版本信息
24 go语言中没有隐藏的this指针,这句话是什么意思?
方法施加的对象显示传递,没有被隐藏。
25 go语言中的引用类型包含哪些?
切片,字典,通道,接口。
26 go语言中指针运算有哪些?
"&"取指针的地址
"*"取指针指向的数据
27 go语言的main函数有什么特点?
main函数不能带参数,不能定义返回值
必须为main包,可使用flag包来解析命令行参数
28 说说go语言的同步锁?
sync.Mutex: 只能被一个goroutine获得,其余goroutine需等待被释放
sync.RWMutex: 读锁被占用,阻止写,不阻止读;写锁占用,则阻止读写。
29 go语言触发异常的场景有哪些?
A. 空指针解析
B. 下标越界
C. 除数为0
D. 调用panic函数
30 说说go语言的beego框架?
beego是轻量级HTTP框架,可以通过注释路由、正则路由等方式完成url路由注入
31 进程,线程,协程的区别是什么?
进程拥有自己独立的堆和栈,操作系统调度
线程拥有自己独立的栈和共享的堆,操作系统调度
协程有独立的栈和局部变量,是自己调度。失去了标准线程使用多CPU的能力。
32 下面代码能运行吗?为什么?如何修改?
type Param map[string]interface{}
type Show struct {
Param
}
func main1() {
s := new(Show)
s.Param["RMB"] = 10000
}
1)不能运行
2)因为Param没有初始化
会报错:字典Param的默认值为nil,当给字典nil增加键值对是就会发生运行时错误
panic: assignment to entry in nil map。
3)修改后
package main
import "fmt"
type Param map[string]interface{}
type Show struct {
Param
}
func main() {
// 创建Show结构体对象
s := new(Show)
// 为字典Param赋初始值
s.Param = Param{}
// 修改键值对
s.Param["RMB"] = 10000
fmt.Println(s)
}
33 请说出下面代码存在什么问题?
type student struct {
Name string
}
func f(v interface{}) {
switch msg := v.(type) {
case *student, student:
msg.Name
}
}
分析:
问题一:interface{}是一个没有声明任何方法的接口。
问题二:Name是一个属性,而不是方法,interface{}类型的变量无法调用属性。
34 写出打印的结果,是否正确?如果不正确,请修改。
type People struct {
name string `json:"name"`
}
func main() {
js := `{
"name":"11"
}`
var p People
err := json.Unmarshal([]byte(js), &p)
if err != nil {
fmt.Println("err: ", err)
return
}
fmt.Println("people: ", p)
}
1)输出内容如下:
people: {}
p中的属性值为空的原因是因为,name的首字母小写,修改成大写,重新运行即可。
2) 将
type People struct {
name string `json:"name"`
}
修改为:
type People struct {
Name string `json:"name"`
}
35 下面的代码是有问题的,请说明原因。
package main
import "fmt"
type People struct {
Name string
}
func (p *People) String() string {
return fmt.Sprintf("print: %v", p)
}
func main() {
p := &People{}
p.String()
}
1)上面的代码出现了栈溢出,原因是因为%v格式化字符串是本身会调用String()方法,
上面的栈溢出是因为无限递归所致。
36 请找出下面代码的问题所在?如果有问题,请修改。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 1000)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}()
go func() {
for {
a, ok := <-ch
if !ok {
fmt.Println("close")
return
}
fmt.Println("a: ", a)
}
}()
close(ch)
fmt.Println("ok")
time.Sleep(time.Second * 100)
}
1)运行结果如下:
panic: send on closed channel
ok
goroutine 5 [running]:
main.main.func1(0xc420098000)
分析:
上面问题出现原因时提前关闭通道所致。
2) 修改如下
func main() {
// 创建一个缓冲通道
ch := make(chan int, 1000)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}()
go func() {
for i := 0; i < 10; i++ {
a, ok := <-ch
if !ok {
fmt.Println("close")
close(ch)
return
}
fmt.Println("a: ", a)
}
}()
fmt.Println("ok")
time.Sleep(time.Second)
}
点评:
最好用sync.WaitGroup实现。
37 请说出下面代码,执行时为什么会报错?如何修改?
type Student struct {
name string
}
func main() {
m := map[string]Student{"people": {"liyuechun"}}
m["people"].name = "wuyanzu"
}
1)报错的原因是因为不能修改字典中value为结构体
2)修改为如下结构
func main() {
m := map[string]Student{"people": {"liyuechun"}}
fmt.Println(m)
fmt.Println(m["people"])
// 不能修改字典中结构体属性的值
//m["people"].name = "wuyanzu"
var s Student = m["people"] //深拷贝
s.name = "xietingfeng"
fmt.Println(m)
fmt.Println(s)
}
38 请说出下面的代码存在什么问题
type query func(string) string
func exec(name string, vs ...query) string {
ch := make(chan string)
fn := func(i int) {
ch <- vs[i](name)
}
for i, _ := range vs {
go fn(i)
}
return <-ch
}
func main() {
ret := exec("111", func(n string) string {
return n + "func1"
}, func(n string) string {
return n + "func2"
}, func(n string) string {
return n + "func3"
}, func(n string) string {
return n + "func4"
})
fmt.Println(ret)
}
1)return <-ch之执行一次,所以不管传入多少query函数,
都只是读取最先执行完的query。
39 Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量?
goroutine可通过channel安全读写共享变量。
40 无缓冲 Chan 的发送和接收是否同步?
无缓冲chan,发送和接收需要同步,发送阻塞直到数据被接收,接收阻塞直到读到数据。
有缓冲chan,不要求发送和接收同步,缓冲满时发送阻塞,缓冲空时接收阻塞。
41 CSP并发模型是什么?goroutine底层是什么实现的?为什么要使用协程?
1)CSP以通信的方式来共享内存
2)goroutine底层时用协程实现并发,协程是运行在用户态的用户线程。
3)用户态避免了内核态与用户态切换的成本
内存占用少。
42 Golang 中常用的并发模型?
A. 通过channel通知实现并发控制
B. 通过sync包中的WaitGroup实现并发控制
为了防止在结束mian函数的时候结束掉Goroutine,所以需要同步等待
在WaitGroup里主要有三个方法:
Add, 可以添加或减少 goroutine的数量.
Done, 相当于Add(-1).
Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0.
C. 在Go 1.7 以后引进的强大的Context上下文,实现并发控制
它就是goroutine 的上下文。 它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个 Context 里,再将它传给要执行的 goroutine 。
context 包主要是用来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理。
context 包的核心是 struct Context,接口声明如下:
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this `Context` is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this Context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}
Done() 返回一个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有一个取消信号
Err() 在Done() 之后,返回context 取消的原因。
Deadline() 设置该context cancel的时间点
Value() 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。
Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。
一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。 其中的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。 典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。
43 JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗?为什么?
1) 不一致
2) nil slice只是声明,没有初始化;
空slice不为nil,但是slice没有值
nil slice:
var slice []int
空 slice:
slice := make([]int,0)
44 协程,线程,进程的区别?
进程: 程序的一次执行过程,是系统进行资源分配的一个独立单位,每个进程都有自己的独立内存空间
线程: 是调度的基本单位,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),线程间通信主要通过共享内存
协程: 用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,
直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
45 互斥锁,读写锁,死锁问题是怎么解决?
互斥锁: 就是互斥变量mutex,用来锁住临界区的.
条件锁: 就是条件变量
读写锁: 写独占,读共享
死锁:
死锁产生的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
a. 预防死锁
可以把资源一次性分配:(破坏请求和保持条件)
然后剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
b. 解除死锁
剥夺资源
从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态.
撤消进程
可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用
46 Golang的内存模型,为什么小对象多了会造成gc压力?
小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配.
47 Data Race问题怎么解决?能不能不加锁解决这个问题?
1)竞争检测机制,可以使用go build -race来进行静态检测。
其在内部的实现是,开启多个协程执行同一个命令, 并且记录下每个变量的状态.
2)可以使用互斥锁sync.Mutex,解决数据竞争(Data race),
也可以使用管道解决,使用管道的效率要比互斥锁高.
48 什么是channel?为什么它可以做到线程安全?
1) 管道,通过它并发核心单元就可以发送或者接收数据进行通讯
2) 发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。
设计Channel的主要目的就是在多任务间传递数据的,这当然是安全的。
49 Epoll原理?
在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,
用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,
以支持快速的查找、插入、删除。
在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,因此就会非常的高效!
如此,一个红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时每次返回这个句柄,而ET模式仅在第一次返回。
50 什么是垃圾回收?常用的垃圾回收的方法有哪些?Golang GC 时会发生什么?
1)对不再使用的内存资源进行自动回收的行为就被称为垃圾回收。
2)常用的垃圾回收的方法:
A)引用计数:
对每个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。当引用计数为0时则立即回收对象。
B)标记-清除:
标记从根变量开始迭代得遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。
也有比较明显的问题:每次启动垃圾回收都会暂停当前所有的正常代码执行,
出现了很多mark&sweep算法的变种(如三色标记法)优化了这个问题。
C)分代搜集:
分代收集的基本思想是,将堆划分为两个或多个称为代(generation)的空间。新创建的对象存放在称为新生代,
随生命周期较长的对象会被提升到老年代中.
3)go使用三色标记清除垃圾回收算法。
gc的过程一共分为四个阶段:
栈扫描(开始时STW)
第一次标记(并发)
第二次标记(STW)
清除(并发)
整个进程空间里申请每个对象占据的内存可以视为一个图,初始状态下每个内存对象都是白色标记。
先STW,做一些准备工作,比如 enable write barrier。然后取消STW,将扫描任务作为多个并发的goroutine立即入队给调度器,进而被CPU处理
第一轮先扫描root对象,包括全局指针和 goroutine 栈上的指针,标记为灰色放入队列
第二轮将第一步队列中的对象引用的对象置为灰色加入队列,一个对象引用的所有对象都置灰并加入队列后,这个对象才能置为黑色并从队列之中取出。循环往复,最后队列为空时,整个图剩下的白色内存空间即不可到达的对象,即没有被引用的对象;
第三轮再次STW,将第二轮过程中新增对象申请的内存进行标记(灰色),这里使用了write barrier(写屏障)去记录
Golang gc 优化的核心就是尽量使得 STW(Stop The World) 的时间越来越短。
51 什么是并发与并行?并发编程的目标是什么?
1)并行是指两个或者多个事件在同一时刻发生;
并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
2)并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
52 负载均衡原理是什么?有哪几层负载均衡?负载均衡器如何选择要转发的后端服务器?
如何判断服务器是否健康?负载均衡算法有哪些?
1)将工作负载分布到多个服务器来提高服务的性能,核心就是网络流量分发
2)
HTTP (七层)
HTTPS (七层)
TCP (四层)
UDP (四层)
3)负载均衡器一般根据两个因素来决定要将请求转发到哪个服务器。首先,确保所选择的服务器能够对请求做出响应,然后根据预先配置的规则从健康服务器池(healthy pool)中进行选择。
4)运行状态检查服务会定期尝试使用转发规则定义的协议和端口去连接后端服务器。
如果,服务器无法通过健康检查,就会从池中剔除,保证流量不会被转发到该服务器,
直到其再次通过健康检查为止。
5)
Round Robin(轮询):为第一个请求选择列表中的第一个服务器,然后按顺序向下移动列表直到结尾,然后循环。
Least Connections(最小连接):优先选择连接数最少的服务器,在普遍会话较长的情况下推荐使用。
Source:根据请求源的 IP 的散列(hash)来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。
52 布式锁应该具备哪些条件?常用分布式锁实现和实现原理是什么?
1)
在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
高可用的获取锁与释放锁;
具备锁失效机制,防止死锁;
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
2)常用的分布式锁实现有三种:
基于数据库,缓存(redis,Zookeeper实现分布式锁。
2.1) 基于数据库实现分布式锁:
在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,
想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,
执行完成后删除对应的行数据释放锁。
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
`desc` varchar(255) NOT NULL COMMENT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
想要执行某个方法,就使用这个方法名向表中插入数据:
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');
因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
2.2) 基于缓存(redis)实现分布式锁:
选用Redis实现分布式锁原因:
Redis有很高的性能;
Redis命令对此支持较好,实现起来比较方便
主要实现方式:
SET lock currentTime+expireTime EX 600 NX,使用set设置lock值,并设置过期时间为600秒,如果成功,则获取锁;
获取锁后,如果该节点掉线,则到过期时间ock值自动失效;
释放锁时,使用del删除lock键值;
2.3) 基于Zookeeper实现分布式锁:
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
创建一个目录mylock;
线程A想获取锁就在mylock目录下创建临时顺序节点;
获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
53 Etcd怎么实现分布式锁?
etcd是键值存储数据库,用于配置共享和服务发现。
Ectd实现分布式锁基本实现原理为:
1)在ectd系统里创建一个key
2)如果创建失败,key存在,则监听该key的变化事件,直到该key被删除,回到1)
3)如果创建成功,则认为获得了锁
第四部分 kubernetes operator
1 kubernetes operator作用是什么?如何查询crd实例?监控哪些资源?
1)通过自定义扩展资源注册到controller-manager,通过list and watch的方式监听对应资源的变化,即解析crd实例的spec部分,
然后在周期内做相应的协调处理,来将自定义资源实例状态修正到期望值。
2) 查询crd实例是通过namespace和name实现。
3) operator监控的资源包含一级资源crd定义本身,以及二级资源crd实例。
2 operator处理的整体流程是什么?
监听了owner为自定义crd类型的资源,将该crd实例入队,从队列中取出待处理
对象,进行协调处理来达到平衡状态,如果在协调处理中经过处理达到期望状态,
则从队列中移除当前处理的对象;否则,该对象会在下一次协调处理中
会被继续处理,直至达到期望状态。
3 kubernetes operator内部原理架构是什么?
下图是Kubernetes 提供的在使用client-go开发 controller 的过程中,client-go 和 controller 的交互流程:
交互流程图如下:
Reflector-------1) List & Watch -------------> Kubernetes API
|
|
2)Add Object
|
V
Delta Fifo queue
|
V
3) Pop Object
|
V
Informer---------4) Add Object------>Indexer----5) Store Object & Key-------> Thread safe store
| |
----------------------- |
| | |
6)Dispatch Event Handler |
functions(Send Object to Informer reference Indexer reference
Custom Controller)
| ^
V |
Res Event Handlers reference |
| |
Resource Event Handlers |
| 9) Get Object for key
7)Enqueue Object Key ^
| |
----> Workqueue---8) Get Key--->Process Item-->Handle Object
client-go组件
Reflector: 在cache包的Reflector类中,监听特定资源类型(Kind)的Kubernetes API,在ListAndWatch方法中执行。
可以监听k8s自定义资源类型。当reflector通过watch API发现新的资源实例被创建,将通过对应的list API获取到
新创建的对象并在watchHandler方法中将其加入到Delta Fifo队列中。
Informer: 在cache包中的base controller中,从Delta Fifo队列中pop出对象,在processLoop方法中执行。
base controller作用是将对象保存一遍后获取,并调用controller将对象传递给controller。
Indexer: 提供对象的indexing方法,定义在cache包的Indexer中。一个indexing场景是基于对象的label创建索引。
Indexer基于几个indexing方法维护索引,使用线程安全的data store来存储对象和他们的key。
cache包的Store类定义了一个名为MetaNamespaceKeyFunc的默认方法,可以为对象生成一个
<namespace>/<name>形式的key。
自定义controller组件
Informer reference:是对Informer实例的引用。知道如何使用自定义资源对象。
自定义controller需要创建正确的Informer。
Indexer reference:是对Indexer实例的引用,自定义controller中需要创建它,在获取对象供后续使用时会用到这个引用。
base controller提供了:
NewIndexerInformer来创建Informer和Indexer,可以使用此方法或者用工厂方法创建informer。
Resource Event Handlers: 回调方法,当Informer想要发送一个对象给controller时,会调用这些方法。
编写回调方法的模式: 获取资源对象的key并放入一个work queue队列,等待进一步的处理(Process item)
Work queue: 在controller代码中创建的方法,用来对work queue中的对象做对应处理。可以有一个或多个其他方法做实际处理。
这些方法一般会使用Indexer reference,或者list方法来获取key对应的对象。
参考:
Go语言实战
Go程序设计语言
https://www.cnblogs.com/maxigang/p/9041080.html
https://segmentfault.com/a/1190000018150987
https://www.nowcoder.com/questionTerminal/e72c6b33bb874d27883b5254e58aa403?orderByHotValue=1&page=1&onlyReference=false
https://baijiahao.baidu.com/s?id=1623334523226223824&wfr=spider&for=pc
https://blog.csdn.net/qq_37133717/article/details/80342307
http://www.imooc.com/article/264183
http://www.imooc.com/article/42071
https://www.kanzhun.com/msh/g1824729-z299541/
https://blog.csdn.net/weixin_34128839/article/details/94488565
https://blog.csdn.net/ahaotata/article/details/84104437
https://www.cnblogs.com/sunsky303/p/9706210.html
https://www.jianshu.com/p/494cda4db297
https://blog.csdn.net/u012291393/article/details/78378386
https://www.jb51.net/article/117512.htm
https://blog.csdn.net/weixin_34208283/article/details/91699624
https://blog.csdn.net/ytd7777/article/details/90239220