面经 | Go语言知识点


GoLang
golang 基础 map slice interface 和并发等基本原理搞清楚,gmp goorutine 基本原理,以及垃圾回收机制等

Go其实是一个不严格是面向什么的语言,和传统的面向对象变成是有区别的。
go没有类的定义,但是结构体在使用上其实和类差不多,可以通过struct实现OOP特性。
去除了传统OOP的方法重载、构造函数和析构函数,隐藏了this指针
但是仍然具有面向对象的继承、封装和多态的特性,只不过实现传统面向对象的不一样,如继承不用extends关键字而是使用匿名字段实现。
Go语言严格区分大小写,入口执行函数是main(),不需要手动在语句末尾添加分号,编译器会自动添加。
是按照一行进行编译的,所以一行只有一个语句。如果有没有用的变量和包,是会报错的。

基础

声明变量可以不用赋值
赋值

var a int=1
//等价于
a:=1
//数组定义
var x [2]int
var array=[5]int{1,2,3,4,5}
array:=[5]int{1,2,3,4,5}
//数组使用[...]表示不确定长度初始化
array:=[...]float32{10.0,2.0,3.4,50.0}
//数组可以调用append的方式添加其中
animals:=[][]string{}
row:=[]string{"bird"}
animals=append(animals,row1)
//等价于
x:=1

//静态常亮,会自动判断对象类型
const s string = "123"
const s = "123"
//可以表示枚举类型
const{
    one = 1
    two = 2
}

有一个iota,这个是可以让编译器修改这个值,iota在const出现的时候被重置为0,每执行一行const就加1

const{
    a=iota
    b
    c
}
//输出是:0,1,2。因为默认值为0,执行一行iota会加1
const (
    a = iota   //0
    b          //1
    c          //2
    d = "ha"   //独立值,iota += 1
    e          //"ha"   iota += 1
    f = 100    //iota +=1
    g          //100  iota +=1
    h = iota   //7,恢复计数
    i          //8
)
//输出是:0 1 2 ha ha 100 100 7 8
const (
    i=1<<iota
    j=3<<iota
    k
    l
)

func main() {
    fmt.Println("i=",i)
    fmt.Println("j=",j)
    fmt.Println("k=",k)
    fmt.Println("l=",l)
}
//输出是:1,6,12,24.就是如果这一行不写,就会执行上一行的命令,然后iota再+1,就是3<<2和3<<3 

输出
只有fmt.Printf是格式化输出,使用%v输出变量的值value,如果是%+v则会以k-v形式打印出变量,比如对于结构体

//引入包
import "fmt"
fmt.Print("xxx")、fmt.Print("xxx")
fmt.Printf("xxx")
prin和pting("xxx") println("xxx")
条件语句和循环语句

if不用说了,差不多,条件不带括号,代码块带括号
switch语句
这个还不太一样,C语言的switch有break,不然会走所有case。这个不需要break也不会走所有case,只会走当前case
有特殊用法,可以判断interface接口中实际存储的变量类型

var x interface{}
switch i := x.(type) {
case nil:
	println("是nil")
case *int:
	println("*int", i)
case int:
	println("这是int")
}

虽然不需要用break控制他不继续往下走case,但是可以使用fallthrought控制他继续往下走case
循环语句
for循环

//假设animals是一个二维数组
for i:=range animals{
    fmt.Print(snimals[i])
}

然后,虽然不需要使用break,但是在for循环之类的还是可以用break跳出循环之类的结构,而且可以通过标记指示break需要跳出哪个代码块。
如下,就会跳出re包含的代码块

re:
    for i:=0;i<10;i++{
        break re
    }

对于continue也可以通过标记指定需要从哪个代码块开始重新执行。
go里面是有goto这个关键词的,可以实现条件转移、跳转循环之类的,但是也不推荐使用,和C语言一样

LOOP:
    xxx
goto LOOP
yyy
函数

函数的定义如下

func {name}([parameters]) [return type]{
    ...
}
//示例
func test(a int) int {
	return a + 1
}

返回值可以返回多个返回值

func swap(x, y string) (string, string) {
   return y, x
}

有几个内置函数

  • len():返回数组或者切片长度
  • cap():计算容量,切片的最大长度可以到多少
  • uncafe.Sizeof()

参数
参数类型包括值类型和引用类型
值类型包括:int、float、bool和string,其变量直接指向内存中的具体值,当复制的时候其实就是拷贝的过程。可以使用&获取到变量地址,和C语言其实一样。
值传递的参数在函数中使用是通过拷贝的方式,形参不会修改实参的值
引用的方式就会改变实参的值,就是指针的方式

func swap(a *int, b *int) {
	var temp int
	temp = *a
	*a = *b
	*b = temp
}

也可以在函数内部创建一个函数

func main(){
   /* 声明函数变量 */
   getSquareRoot := func(x float64) float64 {
      return math.Sqrt(x)
   }

   /* 使用函数 */
   fmt.Println(getSquareRoot(9))

}

go也有闭包,闭包的优势就在于可以直接使用函数的内部变量不需要进行声明
就是外部函数的返回值是一个内部函数,直接在return写内部函数体

切片

两种声明方式
make和声明空切片

//声明一个没有指定大小的数组就可以定义切片,容量长度为0,可以通过append添加元素
var x []int
x:=[]int{}
//使用make()创建切片,指定长度为10
var slice []int=make([int],10)
//声明初始化,简写
slice := make([]int, 3)
slice = []int{1, 2, 3}

//添加元素,返回值是原切片,第一个参数是原切片,第二个是插入的
slice=append(slice,2)

使用的话比如截取之类的,就类似python的list使用方法,中括号和冒号
扩容
切片的默认的capacity是0,然后如上给他初始化{1,2,3}以后,长度和容量变为3。
之后如果删除元素,容量不变,长度减少
添加元素,长度增加,如果大于容量值了,会进行扩容

  • 扩容后元素长度小于1024,会扩容为原长度的2倍
  • 扩容后元素长度大于1024,会扩容为原长度的1.25倍
  • 如果扩容的新容量大于原有的二倍,直接扩容到新容量

然后不需要扩容的时候append返回的是原底层数组的原切片,扩容以后的话返回的是新底层数组的新切片,地址会变。
扩容会调用growslice()函数,这个函数最后两行有一个是capmem和newcap,后者会对内存进行一个对齐,具体和内存分配策略有关,所以结果不一定是1.25的整数倍。
go是按照size class大小分配的,有一个数组runtime/sizeclasses.class_to_size,在其中找到一块最小的满足要求的内存分配

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

比如一个切片里面有五个int,只需要40字节,但是会找到一块48字节的内存给他用。
截取
可以用一个切片中截取部分数据赋值给新的切片,用法就类似于python的listx:=slize[1:3]
截取就是new一个新的slice,底层数组服用原先的。长度是截取数据的个数,容量是截取开始下标到原切片的结尾。

slice:=[int]{1,2,3,4,5,6}
s1:=s[1:3]
//s1的内容为2和3,长度为2,容量为2,因为是从开始下标算起,从1开始到末尾,

使用copy(sc,s)可以实现将s切片拷贝给sc切片。拷贝数据的个数是min(len(s),len(sc)),因此需要两个数组大小一致才能完全拷贝

底层结构

一个切片结构由三部分组成:指针、长度和容量。指针指向底层数组,长度就是当前长度,容量是底层数组的长度。
切片只是一个只读对象,相当于是对数组指针的一个封装。
Go的值传递,用切片传递数组参数既可以节省内存也可以更好处理共享内存问题。
但是并不是什么时候都适合用切片代替数组,因为切片底层数组可能会在堆分配内存,而且小数组在栈上拷贝的消耗未必比make消耗大。

集合Map

声明一个map,可以直接map也可以使用make创建

var map_map map[ke_type]value_type
var map_map=make(map[ke_type]value_type)

然后赋值的时候就是普通的字典使用方式test_map[“111”]=“222”
然后有一点好的是,在根据键获取值的时候,可以有两个返回值,第一个是值,第二个是一个布尔型,如果存在,就返回值和true。否则返回空字符串和和false
delete()函数可以删除集合中的元素,根据键

底层结构

GO中map的底层实现
没有总结完
底层是一个hash表,通过键值进行映射。
buckets中存放了哈希中的最小粒度单元bucket桶,数据通过hash函数均匀分布在每个bucket中,其存放的是bucket的地址
bucket
每个bucket最多存放八组key-value,多余的会放在下一个bmap里面,使用overflow指向,这些数据都不是显式保存的,通过偏移量计算得到的。

  • tophash存储哈希函数算出的高八位,加快索引的,只需要比较高八位就可以过滤掉不符合的,然后再比较完整哈希得到value
  • 存储的是key-value,底层将key和value分别放在一起,key大于128字节的时候会用指针存储,value一样。避免长度不同带来的问题
  • 当bucket溢出的时候,指针指向下一个bucket overflow

查找
用高八位和第八位哈希进行定位查询,高八位查询bucket是否有对应值(在bucket中的位置),低八位确定数据在哪个bucket。

  • 根据key计算哈希
  • 取哈希确定bucket位置
  • 查询tophash数组,如果tophash[i]等于高八位,就在这个bucket里面寻找
  • 如果没有,顺着往下找

如果找不到返回对应类型的0值
插入元素

  • 计算key的哈希
  • 直接适用低八位哈希确定位置
  • 判断key是否存在,存在更新
  • 否则插入
扩容

负载因子
键值对的数据与桶的数目的比值叫负载因子。
触发扩容的两种方式

  • 负载因子>6/5:平均每个bucket存储6.5个键值对
  • 桶溢出的时候:如果B<15,最大overflow的bucket是2B。如果B>=15,overflow的bucket数量为215的时候

渐进式扩容
存储较多的键值的是哦胡,一次迁移需要成本太大了,因此先分配够多的桶,然后使用一个旧的字段记录旧同位置,一个字段记录迁移进去,相当于偏移量。如果当前是扩容阶段,完成一部份键值对任务的迁移。
将键值对的迁移分成多次进行,避免一次的扩容带来抖动。
等量扩容
创建和旧的一样多的bucket,把原来的复制进去。
为什么呢负载因子没满bucket满了呢,因为有很多键值被删了的情况。导致中间可能会出现空位,导师会溢出很多,这个实际上是一种整理。元素在bucket内会重排,不会换桶
增量扩容
当前bucket不够的时候,扩容,元素会重排,可能会发生桶偏移。
负载因子过大的时候就考虑一个新bucket,新的长度是原有的两倍,旧数据弄到新bucket求。如果一次完整搬迁可能会很耗时间,因此用逐步搬迁,每次访问map都会触发一次,每次搬两个键值对。

接口

Go语言基础 - 接口
接口是一种数据类型,可以把具有共性的方法定义在一起,其他类型只需要实现这个接口就可以实现其中的方法。

大概的用法感觉主要就是体现在函数传参之类的,如果有多个结构体,如果有一个函数需要将这些结构体全部作为参数,那么需要好多个函数,因为参数类型不一样。但是如果用了接口,就只需要一个函数,参数类型是接口类型,然后所有实现接口的所有方法的结构体都可以作为参数进来。

大致步骤:

  • 声明一个接口

  • 定义一个结构体

  • 给结构体绑定所有的接口方法

  • 使用,在函数参数中将类型写成接口即可

代码

type Phone interface {
    buy()
}

type Apple struct {
    Price int
    id    string
}
//函数
func (Apple Apple) buy() {
    fmt.Println("买了一个手i及")
}
func use(MyPhone Phone) {
    MyPhone.buy()
}

参数的传递可以是值传递也是引用传递。引用传递就是传一个指针进去,可以直接修改对象信息
java里面的接口是显式声明,go里面的叫隐式声明,只要一个类型实现了接口中规定的方法,那么就实现了这个接口。
然后只要多个类实现了一个接口,其中一个类型的变量就可以直接赋值为另外几个类型的变量。
然后其实可以实现多个接口,只需要给结构体分别绑定所有方法即可。
如果是空接口interface{},作为函数参数的话,表明可以接受任意类型的参数;如果作为map的value类型,那么value可以储存任意类型

test_map_2 := make(map[string]interface{})
test_map_2["1"] = "1"
test_map_2["2"] = 2
test_map_2["3"] = false

fmt.Println("测试空接口作为map的value类型")
for k, v := range test_map_2 {
	fmt.Println(k, v)
}

接口值
因为接口的值可以是任意一个实现该接口的类型变量值,因此接口除了记录值以外还需要记录这个值的类型,因此由两部分组成,type和value
如下两种,第一种value是Dog的值Name,type则是*Dog类型;第二种其动态值则为nil,而类型仍然为*Dog

type Mover interface{
    move()
}
//Dog是实现接口的结构体
var m Mover
m=&Dog{Name:"123"}
m=new(Dog)

接口变量(如m)之间是可以比较的

  • 如上第二种情况虽然动态value时nil,但是因为类型时*Dog,所以比较的时候不等于nil
  • 两个接口变量进行比较的时候,会比较其动态类型type和动态值value
  • 当type包含不支持互相比较的如切片,比较的时候就会报错

类型断言
接口值可能为任意类型
可以使用fmt包直接打印出类型
fmt是使用反射机制在程序运行时获得动态类型的名称的

fmt.Printf("%T\n",m)

可以使用x.(type)获得对应实际值的类型。
返回两个参数,第一个是x转化为T后的变量,第二个是布尔值,断言是否成功

x.(T)

var n Mover = &Dog{Name: "旺财"}
v, ok := n.(*Dog)
if ok {
	fmt.Println("类型断言成功")
   	v.Name = "富贵" // 变量v是*Dog类型
} else {
  	fmt.Println("类型断言失败")
}

如果有多个可以使用switch判断。

switch x.(type){
    case int:xxx
    case bool:yyy
    default:zzz
}
底层实现

是一种特殊类型,存在两种接口,一种是带有方法的接口,一种是不带方法的接口。所有变量都可以赋值给空的接口变量,实现接口中定义的方法的变量就可以赋值给带方法的接口变量,并且可以通过接口直接调用对应方法,实现多态。
内部定义
没有方法的接口在内部定义为eface结构体;有方法的接口内部定义为iface结构体。
两种类型都是定义为两个字段的结构体,大小都是16字节。
Go语言中有一个_type类型结构体,记录某些数据类型的一些基本特征,比如占用的内存大小,类型名称等。每个类型都有一个与之对应的结构体,如果比较特殊的类型,可以对其扩展,如接口的类型结构体就是interfacetype,包含有额外字段。
看不懂底层实现

Context

# Go context详解

包含有goroutine的运行状态、环境、现场等信息

主要用于在goutine之间传递上下文信息,包含有:取消信号、超时时间、截至时间、k-v等

context.Context类型的值可以协调多个goroutine中的代码进行取消操作,并且可以存储键值对。是并发安全的,比如进行取消HTTP请求的操作。

起因

Go经常用于编写高并发的后台服务,搭建http server,通常一个请求里可以请求若干个goroutine,有的进行数据库操作,有的调用接口。他们之间需要共享同一个请求的一些基本信息,比如登录的token,处理请求的最大超时时间等,如果到达超时,需要关闭处理的goroutine,资源会说。

go的server模型其实是协程模型,一个协程处理一个请求。

一个场景

在业务高峰期的时候,有的服务响应变慢,没有超时机制,等待服务的协程越来越多,资源消耗逐渐增大,然后可能就会出意外。这给情况只要设置一个超时时间就行了,如果超时还没有得到数据,就返回默认值或者报错。

Go中不能直接杀死协程,一般通过channel+select进行控制。但是有时候一个请求有多个协程处理,需要共享一些全局变量等等,而且需要被同时关闭,就可以用context上下文控制。

context用于解决goroutine之间的退出通知和原数据传递的问题

底层原理

看链接吧

然后
Context接口主要由四个方法

type Context interface {
    // 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
    Done() <-chan struct{}

    // 在 channel Done 关闭后,返回 context 取消原因
    Err() error

    // 返回 context 是否会被取消以及自动取消时间(即 deadline)
    Deadline() (deadline time.Time, ok bool)

    // 获取 key 对应的 value
    Value(key interface{}) interface{}
}
  • Done():返回一个channel,可以表示context被取消的信号。当channel被关闭时,说明context被取消了。这是一个只读的channel,而又知道读取一个关闭的channel会读取到对应0值,且没有其他地方给这里插入元素。因此除非被关闭否则读不出数据,即当读出0的时候,可以认为是需要关闭了。
  • Err():返回channel关闭的原因,被取消还是超时
  • Deadline():返回context截止时间,可以判断接下来干啥。
  • Value():获取之前设置的key对应的value

cancel()函数
功能就是关闭channel:c.down(),递归取消所有子节点,从父结点删除自己。

使用

如下是个简单用法,给参数传入一个k-v

func main() {
    ctx := context.Background()
    process(ctx)

    ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
    process(ctx)
}

func process(ctx context.Context) {
    traceId, ok := ctx.Value("traceId").(string)
    if ok {
        fmt.Printf("process over. trace_id=%s\n", traceId)
    } else {
        fmt.Printf("process over. no trace_id\n")
    }
}

取消goroutine
场景:启动一个功能定期向后端轮询,后台启动一个协程,然后客户端关闭功能,需要退出goroutine
普通的方法可能会在goroutine的循环里加一个flag,通过true和false判断是否继续。
如果goroutine多了,循环嵌套多了就不好了。
使用context处理,在go的函数里使用select进行判断一下。context本身是没有取消函数的,是保证只能从外部函数调用取消函数,避免子节点调用。

func Perform(ctx context.Context) {
    for {
        calculatePos()
        sendResult()

        select {
        case <-ctx.Done():
            // 被取消,直接返回
            return
        case <-time.After(time.Second):
            // block 1 秒钟 
        }
    }
} 
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
go Perform(ctx)

// ……
// app 端返回页面,调用cancel 函数
cancel()

并发

Go最大的特点就是从语言层面支持并发,不用关心底层逻辑、内存管理,只要编写好自己的业务逻辑。也自带一个比较好的垃圾回收机制,不用关心线程的创建和销毁。
通过go关键词可以开启goroutine,这个是一种轻量级线程,有自己的栈和程序计数器等,由GoLang负责调度。
同一个程序中的所有go routine共享一个地址空间
并发是基于CSP(通信顺序进程)的,这个模型是描述两个独立并发实体通过共享管道(channel)进行通信的模型。
go关键字其实就是协程,实现了goroutine的内存共享,比线程更加好用高效
协程和线程的区别
最重要的区别就是线程切换需要进入内核态,进行上下文的切换,而协程在用户态就可以进行切换,开销小。
用户可以自行决定何时切换协程
线程固有的栈大小是2M,用于保存局部变量之类的信息;而goroutine里固定大小的栈可能会资源浪费,使用动态扩张收缩策略,初始化2KB,最大可以到1GB

go RuntineTask("go")
	RuntineTask("普通")
	time.Sleep(time.Second * 1)

}

func RuntineTask(Message string) {
	for i := 0; i < 10; i++ {
		fmt.Println(Message)
	}
}

这里一定要给一个延时感觉,不然就go来不及执行协程他就结束main了
不对,也可以通过一个计数器实现sync.WaitGroup
协程终止会调用defer函数,这个函数里执行defer 函数,函数里调用wg.down表明计数器减1
当计数器0-的时候程序结束了,不然还没执行程序就结束了。

var wg  sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i:=1; i<100;i++  {
			fmt.Println("A:",i)
		}
	}()
	go func() {
		defer wg.Done()
		for i:=1; i<100;i++  {
			fmt.Println("B:",i)
		}
	}()
	wg.Wait()

channel
channel是连接协程之间的一个机制,可以让一个协程发送特定值到另一个协程。
go语言中channel是一个特殊的类型,相当于一个传送带或者队列,遵循先入先出的原则,保证收发顺序,声明一个channel的时候需要指定数据类型。
使用-<操作符用于指定通道的方向,如发送还是接收

//声明一个通道
ch := make(chan int)
v:=3
//传递数据
ch-<v
v:=-<ch

默认情况下channel是没有缓冲区的,是阻塞式,两端必须同时有发送和接收才行。
也可以在初始化的时候指定一个大小作为缓冲区,这样就可以异步了,当存储的数据超过缓冲区时,就会停止插入数据。发送方会阻塞到值被拷贝到缓冲区,然后缓冲区满了以后,就会阻塞到直到缓冲区有空余
一个小例子

func testchannel(Num int, ch chan int) {
	ch <- (Num * Num)
}
func main(){
    ch := make(chan int)
	go testchannel(4, ch)
	go testchannel(5, ch)
	aa := <-ch
	bb := <-ch
	fmt.Println(aa, bb)
}

也可以使用range遍历读取的数据。
通过close(cn)可以关闭通道。

调度器

CPU感知不到goroutine的,只知道内核线程,M就是对于内核线程的一种封装,Go调度本质就是将G分配给M执行。
设计思想

  • 线程复用
  • 并行(利用多核CPU)
  • 抢占调度(解决公平)

Go的runtine实现了一个小型任务调度器,可以将CPU资源分给每个任务,主要三个函数
Gosched()
使当前Go协程放弃处理器,让其他协程运行,不会挂起协程,未来会恢复运行。Go的协程是抢占式的,长时间执行或者系统调用的时候会把CPU释放,让其他runtine运行。
出现这几种情况就会发生调度

  • syscall
  • 调用C语言函数
  • 调用Gosched
  • goruntine运行超过10ms(可以被抢占)而且调用了非内联函数

Goexit():
终止调用的Go协程,其他协程不影响,在终止之前会执行所有defer函数。
go协程需要等主函数执行完再退出,如果主线程停了好像就不运行了,需要在main加个Sleep或者用WorkGroup

func RuntineTask1() {
	defer fmt.Println("这是task  1的defer")
	fmt.Println("这是task2  的普通函数")
}
func RuntineTask2() {
	defer fmt.Println("这是task  2的defer")
	fmt.Println("这是 的普通task2")

go RuntineTask1()
go RuntineTask2()

GOMAXPROCS
设置程序运行时使用的CPU数,默认使用最大CPU计算。设置的同时可以返回可执行的最大CPU数。
通过runtime.GOMAXPROCS(1)可以设置使用的核心数。

GMP

GMP是Go运行时调度层面的实现,包括G、M、P和Sched
G
代表Goroutine协程,存储执行栈信息、状态和任务函数等,G的数量是无限制的,受内存限制。一个G的栈大小2-4K,一般简单的机器也可以启动数十万个goroutine。G退出的时候还可以把G清理的内存放在P或者全局闲置列表gFree复用。
M
代表Machine,Go对操作系统线程的一个封装,相当于操作系统内核线程,现在CPU执行代码必须有线程,通过系统调用clone创建线程。M在绑定一个有效的P以后进入一个调度循环。
循环的机制大概就是从P的本地队列以及全局队列中获得G,切换到G的执行栈上并运行G的函数,调用goexit清理工作回到M。M不会保留G的状态,这是G可以跨M调度的基础。
M的数量有限,默认一千,可以设置最大数。
P
虚拟处理器Processor,M执行G所需要的资源和上下文,只有将M和P绑定,才能让P的runq中的G真正运行起来,P的数量决定了系统最大可并行的G的数量,P的数量受到本机CPU的限制,也可以通过变量修改,默认CPU核心数。
Sched
调度器结构,维护有存储的M和G的全局队列,以及调度器的一些状态信息。
但是这个模型的缺点是不支持抢占式调度。如果一个G有死循环逻辑,G将永久占用分配的M和P,这个M和P的其他G会被饿死。
因此后来引进了基于写作的抢占式调度和基于信号的抢占式调度。

内存管理

# go——内存分配机制
Go可以自主管理内存,包括如内存池、预分配等机制,不会每次分配内存都进行系统调用。
设计思想

  • 每个线程维护自己的独立内存池,分配的时候有先从内存池获取,内存池不够了才会加锁全局内存池申请,减少系统调用避免线程间的锁竞争
  • 内存回收没有真正释放给操作系统,而是放回预先分配的大块内存中。只有当闲置内存太多的时候才会返还一些给操作系统

内存管理主要组件有mspan、mcache .mcentral和mheap

  • mspan是内存管理的一个基本单元
  • mcache是线程私有的,每一个goroutine都有一个mcache,一个mcache有多个- mspan。
  • mcentral是一个中心缓存,全局的,里面有不同规格的mspan,每个mspan分为空闲的和非空闲的两类。
  • mheap是一个全局的页堆,和中心缓存一样有一把琐,里面也有mspan和mcentral

分配对象

  • 微对象(0-16K):先使用线程缓存上的微型分配器,再依次从线程缓存、中心缓存和堆分配内存
  • 小对象(16-42K):依次从线程缓存、中心缓存和堆分配内存
  • 大对象(32-∞):直接堆内存分配

分配的流程

  • 先计算需要的大小规格
  • 使用mcache对应大小的块分配
  • 如果mcentral没有足够大的,找mheap申请一个合适的mspan
  • 如果mspan太大了,会按照要求切分,返回所需的内存,剩余的页重新构造一个mspan放回mheap
  • 如果没有合适的mspan,mheap会找操作系统申请新页,最小1MB
内存逃逸

每个函数都拥有自己的内存空间存放入参、局部变量和返回地址等,会由编译器在栈上分配,每个函数都有一个栈帧,函数运行结束后销毁。但是有时候需要在函数结束后仍然使用这个栈,就需要将其在堆上分配,从栈上逃逸到堆就叫内存逃逸。
在栈分配的内存由系统申请和释放,不会带来额外的开销。堆上分配到内存则需要进行GC,会带来一定的性能开销。
编译器会自行决定变量是否被外部引用从而是否逃逸
如果外部函数没有引用,就优先在栈里;
如果外部函数引用了,必定在堆上分配
如果栈放不下,就在堆里。

GC

编译器可以自动回收释放栈分配的内存,堆是程序共享的内存,需要使用GC进行回收。
分为两个半独立的组件:赋值器(就是用户态代码)和回收器(负责垃圾回收的代码)
三色标记算法
所有对象初始化都是白色对象
从根对象开始扫描所有根对象,标记为灰色。(根对象是当前所有全局变量和goroutine中的栈中对象)
从灰色对象中取出一个对象,看他有没有引用其他对象,没有的话标记为黑色,放入黑色队列。如果引用了,被引用的对象也被标记为灰色,同时该引用对象标记为黑色,放入黑色对象。也就是所有灰色都会被放在黑色里面。
如果灰色队列不空的话,继续上面遍历扫描,最终内存中只有黑色和白色对象,将所有的白色对象清理了。
Go
Go的GC主要分为几个步骤

  • Mark:标记对象,分为两个阶段Mark Prepare和GC Drains,减少STW时间。
    • Mark Prepare:初始化GC任务,开启写屏障和辅助GC,统计跟对象个数,需要STW
    • GC Drains:扫描所有的root对象,扫描的时候相应goroutine需要暂停运行,将其加入到灰色队列,然后需要循环扫描灰色队列的对象,直到其为空
  • Mark Termination:完成标记工作以后,重新扫描全局指针和栈,因为在刚才标记的过程中可能会有新的对象分配和内存赋值。通过写屏障记录下来,使用re-scan检查,也会STW
  • Sweep:按照标记结果回收白色对象,这一步是后台运行的。

反射

# Go的反射
反射指的是程序运行期间对程序本身进行访问和修改的能力,能够在运行时动态获取变量各种信息。
go反射通过接口的类型信息实现:当向接口变量赋予一个实体类型的时候,接口会储存实体的类型信息和值信息,在运行时可以利用反射:

  • 获取变量的类型信息,如Type,如果是结构体还能获取到具体字段信息
  • 获取变量的值信息或者实例信息value
  • 还可以修改变量的值,调用关联方法

Type是reflect的一个interface、Value是reflect的一个struct,Value实现了Type接口
提供有两个主要方法

  • ValueOf:获取变量值信息,Value
  • TypeOf:获取变量类型信息,Type

通过反射获取接口变量的类型信息

如果是结构体只能通过获取结构体类型变量的基础类型Type获取其字段信息,如果不是指针,直接reflect.TypeOf()获取concreate type;如果是指针,需要Type.Elem()获取基础类型
这里有两种分类

  • Type原生数据类型:int、string、bool、float32、以及type定义的类型,reflect.Type.Name()
  • Kind对象归属的品种:Int、Bool、Float32、Chan、String、Struct、Ptr、Map、Inteface、Slice、Array、、Unsafe Pointer等

例如type Person struct,Person就是Type,struct是kind
通过反射获取接口变量的值信息Value

注意点

就在写代码的时候使用println和fmt.Println,然后发现输出的顺序不一致,我还以为是没有输出,然后才发现好像是函数的问题

这两个函数的本质其实都不一样,前者会重定向输出到stderr标准错误,字体颜色都不一样,vscode中是红色。这个一般用于程序启动和调试,Go内部使用。不接受数组和结构体参数,对于不同类型的组合参数比如用逗号连接好多变量,输出的是地址

后者会重定向输出到stdout标准输出,是正常颜色字体,vscode中是蓝色。是有返回值的,返回写入的字节数和遇到的任何写入错误

如果一个实参由String() string和Error() string方法,fmt和log库打印的时候会调用这两个方法,内置的print和println会忽略这些参数。

面经

www.topgoer.com

new和make区别

new是初始化一个指向类型的指针,new是内建函数,参数是一个类型不是一个值,返回值是指向这个类型0值的新分配的指针

make作用是初始化切片slice、集合map或者通道chan并返回其引用,也是内建函数。第一个参数是类型,第二个是长度,返回值是一个类型。

make只用来创建slice、map和chan并返回类型是T的初始化实例。

Printf()、Sprintf()和Fprintf()
都是格式化输出字符串,比如使用占位符%d之类的。

  • Printf():是把格式化字符串输出到标准输出,一般是屏幕,可以重定向
  • Sprintf():把格式化字符串输出到指定字符串中,比上面多一个char,是目标字符串的地址
  • Fprintf():格式化输出到一个stream流,一般输出到文件中

数组和切片的区别
数组是具有固定长度且有零个或多个相同类型元素的序列,数组长度是数组类型的一部分,[3]int和[4]int是两个类型。
数组需要指定大小,否则会根据初始化自动确定,不可变,是值传递,作为参数直接传入的时候,是拷贝一份作为形参,而不是直接引用地址。
切片是储存相同类型元素的一个可变长度的序列,是一个轻量级数据结构,包含有指针、长度和容量。切片可以使用数组或者make初始化,可以不指定长度

go终端常用命令

  • go env:查看go的环境变量,比如如果需要更换代理就可以在这里查看
  • go run:编译并运行go源码文件
  • go build:编译源码文件
  • go get:动态获取远程代码包
  • go install:编译go文件并安装到bin、pkg目录下
  • go clean:清理工作目录,删除遗留的目标文件
  • go version:查看版本

go的协程
协程和线程都可以实现程序的并发。
go中可以直接使用go关键词创建一个协程go runtine,在协程之间可以使用channel进行通信。
go的引用类型包含有哪些
数组切片、字典、通道和接口interface{}
go的指针运算*
和c语言差不多,&和*,前者对变量取地址,后者是取得指针所指向地址的数据。
go语言的main函数
不能有参数
没有返回值
main函数所在的包必须是main包
main函数可以使用flag包获取命令行传入的参数
go语言同步锁
一个goroutine获得Mutex之后,其他的goroutine只能等待,除非释放锁
RWMutex在读锁占用的时候,会禁止写操作,可以读
RWMutex在写锁占用的时候,会禁止任何goroutine读或者写操作,相当于全部独占

go里面channel的特性

  • 无缓冲的channel是同步阻塞的,有缓冲区的channel是异步非阻塞的
  • 给关闭的channel发送消息,会引起panic
  • 从关闭的channel接收消息,如果缓冲区没有消息,返回0

触发异常的场景

  • 空指针解析
  • 下标越界
  • 除数为0
  • 调用panic函数

go语言的select机制
用来处理异步io的,最大的一条限制就是每个case里面必须是一个io操作,在语言级别支持select关键字
进程线程和协程的区别
进程是资源的分配和调度的一个独立单元,线程是CPU调度的基本单元。
一个进程里面可以有多个线程,进程结束后所有线程都会结束,线程的结束不会影响进程以及其他线程。
线程共享进程的资源,包括寄存器、堆栈和上下文,一个进程至少有一个线程。
进程的切换资源消耗很大,效率低,县城切线的资源消耗一般效率一般,协程是小号最小的效率最高。协程的本质是当前进程在不同函数代码中切换执行,是一个用户层面的,可以是单线程多协程也可以是多线程多协程。
map实现有序排序
map本身是无序的,想有序的话只能通过对key处理。比如使用slice对key进行有序存放,然后通过slice的索引在map里面取值。
channel应用场景和原理
# Go channel的使用场景,用法总结
应用场景:

  • 消息传递
  • 信号广播
  • 事件订阅 和广播
  • 请求、相应转发
  • 任务分发、结果汇总
  • 并发、同步和异步

分为三种状态:nil、active和closed
其底层原理就是在内存中实例化了一个hchan结构体,返回一个chan指针。
通道使用互斥锁mutex,让goroutine以FIFO的方式进入到结构体中,需要收发消息的时候,锁住结构体,缓存中的数据按照链表顺序存放,按照链表顺序读取
如果通道满了,继续发送消息会阻塞goroutine,原理是通过Go运行时的scheduler完成调度。
go如何实现面向对象
面向对象的三个特点:封装、继承和多态

  • 封装:将需要抽象的字段放在结构体中,之后给结构体绑定相应的抽象方法,即可实现封装
  • 继承:没有显式继承,通过结构体中嵌套一个结构体实现组合继承
  • 多态:给多个不同类型结构体绑定相同的方法,实现多态
继承
type Person struct{
    name string
}
func (p *Person) setName(name string){
    p.name=name
}
func (p *Person) getName(){
    fmt.Println(p.name)
}

func main(){
    p:=Person("name")
    p.setName("ddd")
    p.getName()
}
//封装

//多态
type Dove struct{}
type Eagle struct{}
func (d *Dove) fly(){}
func (e *Eagle) fly(){}

func main(){
    var b Birds//抽象接口
    b=&Dove
    b.fly()
    b=&Eagle
    b.fly()
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值