Golang笔记

Golang笔记 #

golang 先贴一个客观的教程文档网站http://www.runoob.com/go/go-slice.html

垃圾回收 #

常见的回收算法 #

  1. 引用计数
    • 优点:对象可以被很快回收
    • 缺点:不太好处理循环引用
  2. 标记-清除
    • 优点:解决了引用计数的缺点
    • 缺点:需要 STW(Stop The World),暂时停止程序运行
  3. 分代收集(按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,段的放入新生代,不同代有不同的回收算法和回收频率)
    • 优点:回收性能好
    • 缺点:算法复杂
  4. 三色标记法(初始状态:白色、从根节点开始遍历,遍历到的变成灰色,遍历灰色,将灰色引用的标记灰色,遍历过的灰色对象变为黑色。循环遍历灰色对象。通过写屏障检测对象的变化,重复。收集所有白色对象(垃圾))

GPM调度和CSP模型 #

CSP 模型? #

CSP 模型是“以通信的方式来共享内存”,不同于传统的多线程通 过共享内存来通信。用于描述两个独立的并发实体通过共享的通 讯 channel (管道)进行通信的并发模型。

GPM 分别是什么、分别有多少数量? #

• G(Goroutine): 即Go协程,每个go关键字都会创建一个协 程。 • M(Machine):工作线程,在Go中称为Machine,数量对应真 实的CPU数(真正干活的对象)。 • P(Processor): 处理器(Go中定义的一个摡念,非CPU), 包含运行Go代码的必要资源,用来调度 G 和 M 之间的关联关 系,其数量可通过 GOMAXPROCS() 来设置,默认为核心数。 M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队 列,P可以调度G交由M执行。

Goroutine调度策略 #

• 队列轮转:P 会周期性的将G调度到M中执行,执行一段时间 后,保存上下文,将G放到队列尾部,然后从队列中再取出一个 G进行调度。除此之外,P还会周期性的查看全局队列是否有G等 待调度到M中执行。 • 系统调用:当G0即将进入系统调用时,M0将释放P,进而某个空 闲的M1获取P,继续执行P队列中剩下的G。M1的来源有可能是 M的缓存池,也可能是新建的。 当G0系统调用结束后,如果有空闲的P,则获取一个P,继续执 行G0。如果没有,则将G0放入全局队列,等待被其他的P调度。 然后M0将进入缓存池睡眠。 ![[goroutine.png]]

CHAN 原理 #

结构体 #

type hchan struct {
	qcount uint // 队列中的总元素个数
	dataqsiz uint // 环形队列大小,即可存放元素的个数
	buf unsafe.Pointer // 环形队列指针
	elemsize uint16 //每个元素的大小
	closed uint32 //标识关闭状态
	elemtype *_type // 元素类型
	sendx uint // 发送索引,元素写入时存放到队列中的位置
	recvx uint // 接收索引,元素从队列的该位置读出
	recvq waitq // 等待读消息的goroutine队列
	sendq waitq // 等待写消息的goroutine队列
	lock mutex //互斥锁,chan不允许并发读写
}

读写流程 #

向 channel 写数据: #

  1. 若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。
  2. 若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
  3. 若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入sendq ,进入睡眠,等待被读 goroutine 唤醒。

从 channel 读数据 #

  1. 若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。
  2. 如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。
  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。
  4. 将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine唤醒。

关闭 channel #

  1. 关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会panic。
panic 出现的场景还有: #
  • 关闭值为 nil 的 channel
  • 关闭已经关闭的 channel
  • 向已经关闭的 channel 中写数据

无缓冲 Chan 的发送和接收是否同步? #

// 无缓冲的channel由于没有缓冲发送和接收需要同步
ch := make(chan int)
//有缓冲channel不要求发送和接收操作同步
ch := make(chan int, 2)

channel 无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读 到数据;channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接 收阻塞。

context 结构原理 #

用途 #

Context(上下文)是Golang应用开发常用的并发控制技术 ,它 可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的 上下文。Context 是并发安全的,主要是用于控制多个协程之间 的协作、取消操作。

数据结构 #

Context 只定义了接口,凡是实现该接口的类都可称为是一种context。

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}
  • 「Deadline」 方法:可以获取设置的截止时间,返回值 deadline是截止时间,到了这个时间,Context 会自动发起取消请求,返回值 ok 表示是否设置了截止时间。
  • 「Done」 方法:返回一个只读的 channel ,类型为 struct{}。如果这个 chan 可以读取,说明已经发出了取消信号,可以做清理操作,然后退出协程,释放资源。
  • 「Err」 方法:返回Context 被取消的原因。
  • 「Value」 方法:获取 Context 上绑定的值,是一个键值对,通过 key 来获取对应的值。

竞态、内存逃逸 #

竞态 #

资源竞争,就是在程序中,同一块内存同时被多个 goroutine 访问。我们使用 go build、go run、go test 命令时,添加 -race 标识可以检查代码中是否存在资源竞争。解决这个问题,我们可以给资源进行加锁,让其在同一时刻只能被一个协程来操作。

  • sync.Mutex
  • sync.RWMutex

逃逸分析 #

「逃逸分析」就是程序运行时内存的分配位置(栈或堆),是由编译器来确定的。堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

逃逸场景: #

  • 指针逃逸
  • 栈空间不足逃逸
  • 动态类型逃逸
  • 闭包引用对象逃逸

并发 Channel #

使用关键字go开启goroutine 轻量级线程 代码

go fun_name(paras_list)
# eg: go f(x, y, z)

package main
import (
	"fmt"
	"time"
)
func say(s string) {
	for i := 0; i < 5; i++ {
	    time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}
func main() {
    go say("world")
    say("hello")
}

Channel #

用于传递数据的数据结构 可用于两个goroutine之间传递指定类型值,同步和通讯 <- 指定通道方向(发送or接受),未指定则双向通道

声明通道 #

使用chan关键字, 在使用之前,需先创建.

ch := make(chan int)
// 示例
package main
import "fmt"

func sum(s []int, c chan int) {
	sum := 0
    for _, v := range s {
	    sum += v
	}
	c <- sum // 把 sum 发送到通道 c
}
func main() {
	s := []int{7, 2, 8, -9, 4, 0}
	c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 从通道 c 中接收
	fmt.Println(x, y, x+y)
}

Cache-有缓冲通道 #

在创建通道时可创建缓冲区,做压入存储

ch := make(chan int, 100)   // cache size
// 示例
package main
import "fmt"
func main() {
    // 这里我们定义了一个可以存储整数类型的带缓冲通道
    // 缓冲区大小为2
    ch := make(chan int, 2)
    // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
    // 而不用立刻需要去同步读取数据
    ch <- 1
    ch <- 2
    // 获取这两个数据
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

go的循环 #

  • golang 只有for while==for
  • golang 中没有小括号包裹, 只需要用{}分隔作用域就可以

for 可以有

  1. for A;B;C {}
  2. for ;B; {}
  3. for {}

go的switch #

  • 每个case自动break
  • fallthrough 显式声明可以继续执行下一个case
  • case 无需常量
  • switch {case} 可做 if-else 用

go的指针 #

go的指针 没有指针运算.

函数定义 #

func function_name([参数列表])[返回类型]{
    balabala
}

如:

func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
    if varDivider == 0 {
        dData := DivideError{
            dividee: varDividee,
            divider: varDivider,
    }
    errorMsg = dData.Error()
    return
    } else {
        return varDividee / varDivider, ""
    }
}

接口 #

package main
import (
    "fmt"
)
# 或者 直接 import "fmt"
type Phone interface {
    call()
    }
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}
func main() {
    var phone Phone
    phone = new(NokiaPhone)
    phone.call()
    phone = new(IPhone)
    phone.call()
}

错误 #

package main
import (
    "fmt"
)
// 定义一个 DivideError 结构
type DivideError struct {
    dividee int
    divider int
}
// 实现     `error` 接口
func (de *DivideError) Error() string {
// 以上函数中,取结构体地址是(等同于面向对象实例化对象吧,然后下方可以
//  de.使用.的方式去调取结构体中定义的某变量)
//错误Error()是调用接口中的函数,制定返回类型为string
    strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
`
    return fmt.Sprintf(strFormat, de.dividee)
}
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
    if varDivider == 0 {
        dData := DivideError{
            dividee: varDividee,
            divider: varDivider,
        }
        errorMsg = dData.Error()
        return
    } else {
        return varDividee / varDivider, ""
    }
}
func main() {

    // 正常情况
    if result, errorMsg := Divide(100, 10); errorMsg == "" {
        fmt.Println("100/10 = ", result)
    }
    // 当被除数为零的时候会返回错误信息
    if _, errorMsg := Divide(100, 0); errorMsg != "" {
            fmt.Println("errorMsg is: ", errorMsg)  
    }
}

以上总结 #

函数定义真是… 也或许刚开始接触太多。也不知道这么设计的目的 其中 功能重写见d.Data.Error。先是将d.Data定义为结构体,然后再去调用结构体下的Error,分明结构体里并没有定义,所以下面的那个函数定然是将Error,与DicideError连接的函数。

所以Go并没有类与对象一说吧…

生动点来说那个函数(func (de *DivideError) Error() string {...})的定义就好像是在强行给这个地址的结构体中塞进去一个执行函数。 一个言简意骇的对象实现方法

附录 #

快问快答 #

1. go 中除了加 Mutex 锁以外还有哪些方式安全读写共享变量? #

Go 中 Goroutine 可以通过 Channel 进行安全读写共享变量。

2. golang中new和make的区别? #

  • make 仅用来分配及初始化类型为 slice、map、chan 的数据。
  • new 可分配任意类型的数据,根据传入的类型申请一块内存,返回指向这块内存的指针,即类型 *Type
  • make 返回引用,即 Type,new 分配的空间被清零, make 分配空间后,会进行初始。

3. Go中对nil的Slice和空Slice的处理是一致的吗? #

首先Go的JSON 标准库对 nil slice 和 空 slice 的处理是不一致。

  • slice := make([]int,0):slice不为nil,但是slice没有值,slice的底层的空间是空的。
  • slice := []int{} :slice的值是nil,可用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。

4. 协程和线程和进程的区别? #

进程: #

进程是具有一定独立功能的程序,进程是系统资源分配和调度的最小单位。 每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

线程: #

线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程: #

协程是一种用户态的轻量级线程,协程的调度完全是由用户来控制的。协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

Golang的内存模型中为什么小对象多了会造成GC压力? #

通常小对象过多会导致GC三色法消耗过多的CPU。优化思路是,减少对象分配。


Vim 高亮 #

emmmm,偶尔会有不支持Go高亮的情况 所以,步骤如下:

cd ~
mkdir .vim
cd .vim
mkdir autoload  plugged
cd plugged
git clone https://github.com/fatih/vim-go vim-go
cd autoload
wget https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

配置 .vimrc

vim ~/.vimrc
set shiftwidth=4 softtabstop=4 expandtab
call plug#begin()
Plug 'fatih/vim-go', { 'do': ':GoInstallBinaries' }
call plug#end()
let g:go_version_warning = 0
over~!