golang特性

koopkl Lv2

Java转Go,主要记录Go的语法和特点

声明变量

作用范围

  • 和包同级变量,可以被同包内的所有程序看到
  • 函数内的变量,函数内部看到

声明方式

普通声明

1
var 变量名字 类型 = 表达式

其中类型 或 “=表达式”可以省略。如果省略的是类型信息,那么将根据初始化表达式来推到变量的类型信息。如果初始化表达式被省略,那么将利用零值初始化该变量。

声明时可以多个变量同时初始化赋值

1
var a, b, c = true, 2.3, "str"

在包级别声明的变量会在main入口函数执行前完成初始化,局部变量会在声明语句被执行到的时候完成初始化。

一组变量也可以通过调用一个函数,由函数返回多个返回值初始化

简短变量声明

函数内部,有一种称为简短变量声明语句的形式,可以用于声明并初始化局部变量。

1
s := "123"

类似上面的方式。变量的类型会被自动推断。

同样的也有多个变量同时初始化的方式

1
i, j := 0, 1

有一个比较微妙的地方

在使用简短变量声明时,必须至少要声明一个新的变量。如下

1
2
f, err := os.Open(filePath1)
f, err := os.Open(filePath2)

会出现编译失败。

但如下,并不会出现编译失败的情况

1
2
file1, err := os.Open(filePath1)
file2, err := os.Open(filePath2)

基础数据类型

字符串

一个字符串是一个不可改变的字节系列。字符串可以包含任意的数据,包括byte值0。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列。

内置的len函数可以返回一个字符串中的字节数据(不是码点数),索引操作s[i]返回第i个字节的字节值,这里都是字节,并非是码点

子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节,不包括j本身,生成一个新字符串。

[!warning]

这里如果出现j>len(s) 或者 j 小于i,在运行时会出现panic异常

字符串不可变性

go中我们创建一个string类型的变量是无法进行赋值的,即:

1
2
3
var s = "123"
// error
s[0] = '1'

上面这种赋值操作是无法通过编译的。想要修改,可以先将其转换为byte数组的方式,然后进行修改。

1
2
3
4
s := "abc"
b := []byte(s)
b[0] = '1'
s = string(b)

通过上述方式可以对字符串进行修改,但注意上述创建b为byte数组的过程是产生了额外的内存开销的,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。所以修改b的操作对原字符串并不会产生任何影响。

复合数据类型

数组

数组的比较是如果数组元素类型是可以被比较的,那么数组本身是可以被比较的,这时候可以通过==来对数组进行比较,当且仅当数组的所有元素都相等时,两个数组相等。

切片

不能比较,比较两个切片必须要遍历全部元素,即便是1.21后的包函数也是一样的

接口

Go的接口就是一个声明了一些方法的合约,然后我们可以让一些类型去实现这个接口,也即为类型实现对应的方法,如果一个类实现了接口所包含的方法,那么这个类型就可以称为实现了这个接口,这个类型就可以被称为是这个接口类型

接口值,一般包含两部分:

  • 接口类型
  • 接口类型的值

即一个接口比如如下代码:

1
2
var a interface{}
fmt.Println(a == nil) // true
1
2
3
4
5
6
7
8
func main(){
var a interface{} // nil
var b *string // nil
a = b
fmt.Println(a == nil) // false
fmt.Println(b == nil) // true
fmt.Println(b == a) // true
}

这里b赋值给a时,虽然b是nil,但是此时a的类型不再是nil,而是*string,虽然值是nil,但是此时a不再为nil

类型断言

类型断言即判断一个类型是否为某个接口的类型,类似Java中instanceof关键词。类型断言的语法是

1
res, ok := x.(T)

语法介绍:判断x是否为T类型,如果成功则返回给res,ok为true,否则ok为false

这里包含了两部分其实,如果我们不使用两个参数接收类型断言的返回值,那么如果类型断言失败会在运行时抛出panic

注意这里的类型断言有两种断言:

如果 asserted type 是一个 concrete type,一个实例类 type,断言会检查 x 的 dynamic type 是否和 T 相同,如果相同,断言的结果是 x 的 dynamic value,当然 dynamic value 的 type 就是 T 了。换句话说,对 concrete type 的断言实际上是获取 x 的 dynamic value。

如果 asserted type 是一个 interface type,断言的目的是为了检测 x 的 dynamic type 是否满足 T,如果满足,断言的结果是满足 T 的表达式,但是其 dynamic type 和 dynamic value 与 x 是一样的。换句话说,对 interface type 的断言实际上改变了 x 的 type,通常是一个更大 method set 的 interface type,但是保留原来的 dynamic type 和 dynamic value。

Go并发

Goroutines 和 channels

goroutines在使用上是 go f(x) 类似形式调用一个函数,这样就能够实现开启一个新的协程,并执行我们的函数

channels就是一个管道,分为有缓冲管道和无缓冲管道。

select用法和特性

特性:

  1. 每个case都必须是一个通信
  2. 所有channel表达式都会被求值
  3. 所有被发送的表达式都会被求值
  4. 如果任意某个通信可以进行,它就执行;其他被忽略。
  5. 如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。否则执行default子句(如果有)
  6. 如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

go的锁

go语言中没有可重入锁机制的存在。

sync.Mutex

sync.RWMutex

go的配置

GOPATH

go的依赖,比如go install 或者 go get所获得的包,所存储的位置

GOCACHE

go代码build过程中所产生的缓存

GOENV

go env的配置所存储的位置

GOPROXY

go下载包时的代理地址

GOROOT

指定go的安安装位置

GMP

go使用了goroutine和channel来实现并发。

goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime调度,转移到其他可运行的线程中。对于程序员而言,看不到这些底层的细节。

go中协程被称为goroutine,一个goroutine只占几KB,但是这几KB就足够goroutine运行完,这就能在有限的内存空间中支持大量的goroutine,支持更多的并发。虽然一个goroutine的栈只占几KB,但其实是可伸缩的,runtime会自动为goroutine分配

GMP,这里的G指goroutine,M指thread(惯例叫法),p,processor

老的调度器:GM

但是已经被废弃:

  • 有一个全局的goroutine队列,和全局的线程池,线程从队列中取协程和放入协程都需要加互斥锁。

缺点:

  • 创建、销毁、调度G都需要每个M先获取锁,这存在着相当激烈的锁竞争
  • M转G会造成延迟和额外的系统负载。比如,M上的G想要创建G‘,为了执行G,修奥执行G’,此时需要G‘放到M’上执行,由于局部性,其实G‘更适合在M上执行,此时会造成很差的局部性。
  • 系统调用(M的频繁切换)导致频繁的线程阻塞和取消阻塞操作,增加了系统开销。

新调度器:GMP

P为processor,它包含了运行goroutine的资源,如果一个M想要运行G,必须先获取P,P中包含了可运行的G的队列

调度器的功能是将可运行的goroutine分配到工作线程上。具体可以参考下图:

GMP模型

  1. 全局队列:存放等待运行的G
  2. P的本地队列:和全局队列类型,存放的也是等待运行的G,存的数量有限,不超过256个。新建G‘时,G优先放在P的本地队列中,如果本地队列满了,那么会取本地队列中一半的G到全局队列中,同时G’也会被放到全局队列中(这里并不一定会被放入到全局,要看情况的。。如果G‘是在当前正在执行的G后面执行,那么会被保存在本地队列,利用某个老的G替换新的G加入全局队列)
  3. P列表:所有的P都在程序启动时创建,并保存在数据中,最多有GOMAXPROCS个
  4. M:线程如果想要运行任务就要获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列中拿一批G放到P的本地队列,或者从其他的P的本地队列中偷一半放到自己的P的本地队列,然后M运行G,结束之后再拿新的G

Goroutine调度器和OS调度器是通过M结合起来的,每一个M都代表了1个内核进程,OS调度器负责把内核线程分配到CPU的核上运行。

设计思想:

  • work stealing:本线程没有可用G时,尝试从其他线程绑定的P偷取G,而不是销毁线程
  • hand off机制:当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P交给其他空闲线程进行
  • 抢占:在coroutine中等待一个协程主动让出cpu才执行下一个协程,但是在Go中,一个goroutine最多占用cpu10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的地方

调度器的生命周期

image-20240803180735932

M0

M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G, 在之后M0就和其他的M一样了。

G0

G0是每次启动一个M都会第一个创建的gourtine,G0仅用于负责调度的G,G0不指向任何可执行的函数, 每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间, 全局变量的G0是M0的G0。

G0 是用来做调度的,例如:从 G1 切换到 G2 时,会先切回到 G0,保存 G1 的栈等调度信息,然后再切换到 G2。

一个goroutine退出后,并不会立刻被销毁,它的栈空间会被回收,但是其goroutine结构体不会销毁,它会被加入一个全局的队列里面(和上面的全局队列应该不是一个),等待被调度器回收,调度器管理这些goroutine,包括重用他们的栈空间,重新分配他们的cpu时间片等等。?

Golang 何时栈,何时堆?

go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis)当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆
go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身🤣🤣🤣🤣

总的来说,go这个内存分配不是固定的,在分配前会先进行逃逸分析,如果不逃逸会分配在栈,逃逸,则分配在堆

常用的:

[]interface{} 使用数组形式赋值

map[string]interface{},赋值

map[interface{}]interface{},赋值,key value都会逃逸

map[string][]string 尝试赋值

func(*int) 进行函数赋值,会使传递的形参出现逃逸

func([]string),使用[]string{“123”}形式赋值,会出现逃逸

Golang垃圾回收

go1.3 标记清除,整个过程都需要stw

go的染色标记法和Java里面最大的区别为:Java的对象都在堆上,但go的对象可能在堆上也可能在栈上。

go1.5 三色并发标记

白-灰-黑

个人觉得和Java的CMS是类似的

这里为了实现并发,在并发标记阶段使用插入操作屏障和删除操作屏障,即在进行这些操作的时候,如果满足了一些条件,必须将新增引用的指向和被删除的引用节点颜色强制更改。

具体来说,根节点直接能够连接到的点为灰色,然后以灰色为根节点遍历,每一个灰色节点遍历完就是黑色,每一个灰色直连的节点都便为灰色,从而这样递归的遍历所有,最终剩下的白色就被清除即可。

  • 插入操作屏障:黑色增加了对白色的引用,则将白色设置为灰色

  • 删除操作屏障:删除了灰色对白色or灰色的引用,需要将白色or灰色设置为灰色

需要注意,插入操作屏障和删除操作屏障都能够保证在并发标记过程中出现的一些误删问题的出现

插入屏障的唯一缺点就是需要stw,重新扫描栈

删除屏障的唯一缺点就是回收精度低,一些本该回收的没有回收。且在GC开始时,需要扫描整个堆栈作为快照,从而在删除操作时,可以进行拦截,将白色对象置为灰色对象。

go1.8 混合写屏障机制

  1. GC开始时,栈上对象全部设置为黑色,不需要stw

  2. GC期间,任何在栈上创建的新对象都为黑色

  3. 被删除的对象标记为灰色

  4. 被添加的对象标记为灰色

注:这里的3和4针对堆上的对象而言的,即给A对象增加一个下游或者给删除A对象的一个下游,如果A在栈上则不会影响下游的染色,A在堆上则会影响

基本不需要stw

总结:

GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。

GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通

GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。

没有理解为什么写操作屏障和删除操作屏障,需要stw?

答:

针对插入也就是写操作屏障,因为这里不考虑栈上的对象,所以一次扫描结束后,还可能存在堆上的黑色节点引用了栈上的白色节点,这时候我们需要stw,然后rescan栈。

删除操作屏障需要在开始stw记录引用快照然后才可以进行屏障拦截操作

  • Title: golang特性
  • Author: koopkl
  • Created at : 2024-05-03 16:14:57
  • Updated at : 2024-08-03 20:47:32
  • Link: https://lime.popla.cc/2024/05/03/golang特性/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments