Go基础

1 go的数据类型

基本数据类型bool、float、int 、字符、错误

复合数据类型:ptr、array、slice、map、channel、interface、func()

2 UTF-8 和 Unicode

3 如何高效地拼接字符串

拼接字符串的方式有:+ fmt.Sprintf, strings.Builder, bytes.Buffer, strings.Join

  1. 使用+操作符进行拼接时,会对字符串进 行遍历,计算并开辟一个新的空间来存储原来的两个字符串。
  2. fmt.Sprintf由于采用了接口参数,必须要用反射获取值,因此有性能损耗。
  3. strings.Builder用WriteString()进行拼接,内部实现是指针+切片,同时String()返回拼接后的字符串,它是直接把[]byte转换为string,从而避免变量拷贝。
  4. bytes.Bufferbytes.Buffer是一个一个缓冲byte类型的缓冲器,这个缓冲器里存放着都是byte, bytes.buffer底层也是一个[]byte切片。
  5. strings.joinstrings.join也是基于strings.builder来实现的,并且可以自定义分隔符,在join方法内调用了b.Grow(n)方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。

在Go语言中,strings.Builder的性能通常比bytes.Buffer好,主要有以下几个原因:

  1. 零拷贝:strings.Builder在内部使用了可变长度的[]byte切片来存储字符串,而bytes.Buffer使用了固定长度的[]byte切片。当进行字符串拼接时,strings.Builder可以直接修改切片中的内容,而不需要进行额外的内存分配和拷贝操作,从而避免了不必要的性能开销。
  2. 预分配内存:strings.Builder在初始化时会预分配一定大小的内存空间,避免了频繁的内存分配和释放操作。这样可以减少内存分配的次数,提高性能。
  3. 字符串连接优化:strings.Builder提供了WriteString方法,可以直接将字符串追加到内部的[]byte切片中,而不需要进行类型转换和拷贝操作。这样可以减少不必要的中间步骤,提高字符串连接的效率。需要注意的是,strings.Builder和bytes.Buffer都是用于字符串拼接和缓冲的类型,选择使用哪个取决于具体的需求和场景。如果需要频繁进行字符串拼接操作,尤其是在循环中,strings.Builder通常会更高效。而如果只是简单的缓冲操作,bytes.Buffer也可以满足需求。总结来说,strings.Builder的性能比bytes.Buffer好,主要是因为它采用了零拷贝、预分配内存和字符串连接优化等技术,避免了不必要的内存分配和拷贝操作,提高了字符串拼接的效率。

4 go里面的int和int32是同一个概念吗?

不是一个概念。go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统,int类型的大小就是4字节。如果是64位操作系统,int类型的大小就是8个字节。除此之外uint也与操作系统有关,占字节与int类型情况一致。而int后有数字的话,占用空间大小是固定的:

int8占1个字节,int16占2个字节,int32占4个字节,int64占8个字节。

5 闭包

所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

闭包=函数+引用环境,这里可以简单的理解为闭包是函数内部的匿名函数

func main() {
	// 创建一个玩家生成器
	generator := playerGen("码神")
	// 返回玩家的名字和血量
	name, hp := generator()
	// 打印值
	fmt.Println(name, hp)
}

// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
	// 血量一直为150
	hp := 150
	// 返回创建的闭包
	return func() (string, int) {
		// 将变量引用到闭包中
		return name, hp
	}
}

6 数组和切片的区别

  1. 切片的长度可能在执行期间发生变化 ,而数组的长度不能变化,可以把切片看成一个长度可变的数组。
  2. 数组作为函数参数是进行值传递的,函数内部改变传入的数组元素值不会影响函数外部数组的元素值; 切片作为函数的参数是进行的指针传递,函数内部改变切片的值会影响函数外部的切片元素值。
  3. 数组可以比较,切片不能比较 (对底层数组的引用)。

7 new和make的区别?

  • new只用于分配内存,返回一个指向地址的指针。它为每个新类型分配一片内存,初始化为0且返回类型*T的内存地址,它相当于&T{}
  • make只可用于slice,map,channel的初始化,返回的是引用

8 如何判断 2 个字符串切片(slice) 是相等的?

reflect.DeepEqual(),但反射非常影响性能。

9 go slice是怎么扩容的?

1.7版本:如果当前容量小于1024,则判断所需容量是否大于原来容量2倍,如果大于,当前容量加上所需容量;否则当前容量乘2。如果当前容量大于1024,则每次按照1.25倍速度递增容量,也就是每次加上cap/4。

1.8版本:Go1.18不再以1024为临界点,而是设定了一个值为256的threshold,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3*256)/4;

  1. 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
  2. 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
  3. 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。

10 Go面向对象是如何实现的?

Go实现面向对象的两个关键是struct和interface。

封装:对于同一个包,对象对包内的文件可见;对不同的包,需要将对象以大写开头才是可见的。

继承:继承是编译时特征,在struct内加入所需要继承的类即可:

type A struct{}
type B struct{
        A
}

多态:多态是运行时特征,Go多态通过interface来实现。类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量。

11 空 struct{} 的用途

  • 用map模拟一个set,那么就要把值置为struct{},struct{}本身不占任何空间,可以避免任何多余的内存分配。
  • 有时候给通道发送一个空结构体,channel<-struct{}{},也是节省了空间。
  • 仅有方法的结构体

12 如何知道一个对象是分配在栈上还是堆上?

Go的逃逸分析是在编译器完成的;go局部变量会进行逃逸分析。如果变量离开作用域后没有被引用,则优先分配到栈上,否则分配到堆上。

go build -gcflags ‘-m -m -l’ xxx.go.

关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。

如果变量内存占用较大时,优先放在堆上;

如果函数外部没有引用,优先放在栈中;

如果变量在函数外部存在引用;必定在堆中。

13 golang的内存管理

对于微对象的分配流程:

(1)从 P 专属 mcache 的 tiny 分配器取内存(无锁)

(2)根据所属的 spanClass,从 P 专属 mcache 缓存的 mspan 中取内存(无锁)

(3)根据所属的 spanClass 从对应的 mcentral 中取 mspan 填充到 mcache,然后从 mspan 中取内存(spanClass 粒度锁)

(4)根据所属的 spanClass,从 mheap 的页分配器 pageAlloc 取得足够数量空闲页组装成 mspan 填充到 mcache,然后从 mspan 中取内存(全局锁)

(5)mheap 向操作系统申请内存,更新页分配器的索引信息,然后重复(4).

对于小对象的分配流程是跳过(1)步,执行上述流程的(2)-(5)步;

对于大对象的分配流程是跳过(1)-(3)步,执行上述流程的(4)-(5)步.

golang内存管理基本是参考tcmalloc来进行的。go内存管理本质上是一个内存池,只不过内部做了很多优化:自动伸缩内存池大小,合理的切割内存块。

一些基本概念:

页Page:一块8K大小的内存空间。Go向操作系统申请和释放内存都是以页为单位的。

span : 内存块,一个或多个连续的 page 组成一个 span 。如果把 page 比喻成工人, span 可看成是小队,工人被分成若干个队伍,不同的队伍干不同的活。

sizeclass : 空间规格,每个 span 都带有一个 sizeclass ,标记着该 span 中的 page 应该如何使用。使用上面的比喻,就是 sizeclass 标志着 span 是一个什么样的队伍。

object : 对象,用来存储一个变量数据内存空间,一个 span 在初始化时,会被切割成一堆等大的 object 。假设 object 的大小是 16B , span 大小是 8K ,那么就会把 span 中的 page 就会被初始化 8K / 16B = 512 个 object 。所谓内存分配,就是分配一个 object 出去。

mheap

一开始go从操作系统索取一大块内存作为内存池,并放在一个叫mheap的内存池进行管理,mheap将一整块内存切割为不同的区域,并将一部分内存切割为合适的大小。

编辑切换为

mheap.spans :用来存储 page 和 span 信息,比如一个 span 的起始地址是多少,有几个 page,已使用了多大等等。

mheap.bitmap 存储着各个 span 中对象的标记信息,比如对象是否可回收等等。

mheap.arena_start : 将要分配给应用程序使用的空间。

mcentral

用途相同的span会以链表的形式组织在一起存放在mcentral中。这里用途用sizeclass来表示,就是该span存储哪种大小的对象。

找到合适的 span 后,会从中取一个 object 返回给上层使用。

mcache

为了提高内存并发申请效率,加入缓存层mcache。每一个mcache和处理器P对应。Go申请内存首先从P的mcache中分配,如果没有可用的span再从mcentral中获取。

14 GoGC工作原理

垃圾回收机制是Go一大特(nan)色(dian)。Go1.3采用标记清除法, Go1.5采用三色标记法,Go1.8采用三色标记法+混合写屏障

标记清除法

分为两个阶段:标记和清除

标记阶段:从根对象出发寻找并标记所有存活的对象。

清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。

缺点是需要暂停程序STW(stop the world)。

三色标记法

  • 1.只要是创建了新对象就标记为白色
  • 2.gc开始就从根节点遍历所有直接一次可达的对象,将其从白色移到灰色
  • 3.遍历灰色集合,将灰色对象引用的对象从白色集合中放入灰色,同时将此次遍历的灰色对象标记为黑色
  • 4.不断重复3过程,直到灰色中无对象,即只有白黑色
  • 5.回收白色对象

所以三色标记最不愿发生的事:

  1. 一个白色对象被黑色对象引用(白挂在黑下)
  2. 灰色对象与它之间可达的白色对象遭到破坏(灰同时放弃了白)

同时发生时,就导致了某个对象的丢失!!!所以需要破坏上述两个条件之一!!!

  • 强三色不变式:对于条件1:白挂黑下->强制性的不允许黑色应用白色对象
  • 弱三色不变式:对于条件2:灰放弃白->黑引用白条件为必须存在其他灰色同时引用了白色,或者可达它的链路上游存在灰色对象

上述就就是解决办法,之后就诞生了屏障

屏障机制

屏障:额外的判断机制

  • 插入写屏障 对象被引用时
  • 删除写屏障 对象被删除时

插入写屏障(强三色)(在堆上启用,栈上禁用)

  • 要求:在A对象引用B对象时,B对象被标记为灰色(B挂在A下,B就必须是灰色)
  • 作用:上述操作保证了堆内强三色,但不会保证栈内强三色,所以在GC前对栈进行一次STW
  • 不足:结束需要STW

删除写屏障(弱三色)(快照stw)

  • 要求:被删除的对象,如果自身为灰色或者白色,那么会被标记为灰色(保护灰白不断)
  • 作用:保证了灰色对象引用的白色对象被删除时不会被本轮gc
  • 不足:回收精度低,真正的垃圾也可以活过本轮gc

三色标记和混合写屏障

  • 1.GC开始优先将栈上对象全部扫描为黑色(之后不进行二次扫描,也不STW)
  • 2.GC期间,任何在栈上创建的新对象均为黑色
  • 3.删除对象标记为灰色
  • 4.被添加的对象标记灰色

15 为什么Go语言中的字符串是不可变的?

  1. 内存安全:字符串的不变性也有助于防止内存安全问题,如缓冲区溢出。由于字符串内容不能被修改,因此无需担心修改操作导致的内存越界问题。
  2. 性能优化:字符串在Go语言中是通过字节数组实现的,且存储在只读内存段中。如果字符串是可变的,那么每次修改字符串都需要重新分配内存和复制数据,这将导致性能下降。而不可变性则避免了这种情况,使得字符串操作更加高效。

16 go如何进行调度的?GMP中状态流转。

Go里面GMP分别代表:G:goroutine,M:线程(真正在CPU上跑的),P:调度器Processor

编辑

调度器是M和G之间桥梁。

go进行调度过程:

  • 某个线程尝试创建一个新的G,那么这个G就会被安排到这个线程的G本地队列LRQ中,如果LRQ满了,就会分配到全局队列GRQ中;
  • 尝试获取当前线程的M,如果无法获取,就会从空闲的M列表中找一个,如果空闲列表也没有,那么就创建一个M,然后绑定G与P运行。
  • 进入调度循环。
  • 找到一个合适的G。
  • 执行G,完成以后退出。
  • work stealing 机制

当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。

  • hand off 机制

当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。

17 map的key不可以是什么类型?map为什么无序?

key需要比较所以只能是基本数据类型,不可以是引用类型

map的底层数据结构机制导致他是无序的,在遍历时会做随即播种,及不确定本次遍历开始于第几个桶的第几个哈希值。

18 map的底层

type hmap struct {
    count       int                 // map中key、value总数
    flags       uint8               // map 是否被 goroutine 并发读写
    B           uint8               // 桶数组长度的指数
    noverflow   uint16              // map 中溢出桶的数量
    hash0       uint32              // 
    buckets     unsafe.Pointer      // 桶数组 
    oldbuckets  unsafe.Pointer      // 扩容过程中老的桶数组       
    nevacuate   uintptr             // 扩容时的进度标识,index 小于 nevacuate 的桶都已经由老桶转移到新桶中
    extra       *mapextra           // 预申请的溢出桶   
}   

type mapextra struct {
    overflow        *[]*bmap        // 供桶数组 buckets 使用的溢出桶
    oldoverflow     *[]*bmap        // 扩容流程中,供老桶数组 oldBuckets 使用的溢出桶
    nextOverflow    *bmap           // 下一个可用的溢出桶
}

type bmap struct {    
    tophash  [bucketCnt]uint8
}

type bmap struct {    
    tophash     [bucketCnt]uint8    // 高八位    
    keys        [bucketCnt]T        
    values      [bucketCnt]T          
    overflow    uint8                
}

map扩容机制

map 扩容机制的核心点包括:

(1)扩容分为增量扩容和等量扩容;

(2)当桶内 key-value 总数/桶数组长度 > 6.5 时发生增量扩容,桶数组长度增长为原值的两倍;

(3)当桶内溢出桶数量大于等于 2^B 时( B 为桶数组长度的指数,B 最大取 15),发生等量扩容,桶的长度保持为原值;

(4)采用渐进扩容的方式,当桶被实际操作到时,由使用者负责完成数据迁移,避免因为一次性的全量数据迁移引发性能抖动.

map扩容流程

map 的扩容类型分为两类,一类叫做增量扩容,一类叫做等量扩容.

(1)增量扩容

表现:扩容后,桶数组的长度增长为原长度的 2 倍;

目的:降低每个桶中 key-value 对的数量,优化 map 操作的时间复杂度.

(2)等量扩容

表现:扩容后,桶数组的长度和之前保持一致;但是溢出桶的数量会下降.

目的:提高桶主体结构的数据填充率,减少溢出桶数量,避免发生内存泄漏.

map读流程

map 读流程主要分为以下几步:

(1)根据 key 取 hash 值;

(2)根据 hash 值对桶数组取模,确定所在的桶;

(3)沿着桶链表依次遍历各个桶内的 key-value 对;

(4)命中相同的 key,则返回 value;倘若 key 不存在,则返回零值.

map 写流程

(1)根据 key 取 hash 值;

(2)根据 hash 值对桶数组取模,确定所在的桶;

(3)倘若 map 处于扩容,则迁移命中的桶,帮助推进渐进式扩容;

(4)沿着桶链表依次遍历各个桶内的 key-value 对;

(5)倘若命中相同的 key,则对 value 中进行更新;

(6)倘若 key 不存在,则插入 key-value 对;

(7)倘若发现 map 达成扩容条件,则会开启扩容模式,并重新返回第(2)步.

19 channel底层

type hchan struct {
    qcount   uint           // 当前 channel 中存在多少个元素
    dataqsiz uint           // 当前 channel 能存放的元素容量
    buf      unsafe.Pointer // channel 中用于存放元素的环形缓冲区
    elemsize uint16
    closed   uint32
    elemtype *_type // channel 元素类型
    sendx    uint   // 发送元素进入环形缓冲区的 index
    recvx    uint   // 接收元素所处的环形缓冲区的 index
    recvq    waitq  // 因接收而陷入阻塞的协程队列
    sendq    waitq  // 因发送而陷入阻塞的协程队列
    
    lock mutex
}

type waitq struct {
    first *sudog
    last  *sudog
}

type sudog struct {
    g         *g
    next      *sudog
    prev      *sudog
    elem      unsafe.Pointer // 读取/写入 channel 的数据的容器
    isSelect  bool       // 标识当前协程是否处在 select 多路复用的流程中
    c         *hchan     // 标识与当前 sudog 交互的 chan
}
  • • 判断申请内存空间大小是否越界,mem 大小为 element 类型大小与 element 个数相乘后得到,仅当无缓冲型 channel 时,因个数为 0 导致大小为 0;
  • • 根据类型,初始 channel,分为 无缓冲型、有缓冲元素为 struct 型、有缓冲元素为 pointer 型 channel;
  • • 倘若为无缓冲型,则仅申请一个大小为默认值 96 的空间;
  • • 如若有缓冲的 struct 型,则一次性分配好 96 + mem 大小的空间,并且调整 chan 的 buf 指向 mem 的起始位置;
  • • 倘若为有缓冲的 pointer 型,则分别申请 chan 和 buf 的空间,两者无需连续;
  • • 对 channel 的其余字段进行初始化,包括元素类型大小、元素类型、容量以及锁的初始化.

01 实现使用字符串函数名,调用函数。

思路:采用反射的Call方法实现。

package main
import (
        "fmt"
    "reflect"
)

type Animal struct{
    
}

func (a *Animal) Eat(){
    fmt.Println("Eat")
}

func main(){
    a := Animal{}
    reflect.ValueOf(&a).MethodByName("Eat").Call([]reflect.Value{})
}

02 有三个函数,分别打印”cat”, “fish”,”dog”要求每一个函数都用一个goroutine,按照顺序打印100次。

此题目考察channel,用三个无缓冲channel,如果一个channel收到信号则通知下一个。

package main

import (
        "fmt"
        "time"
)

var dog = make(chan struct{})
var cat = make(chan struct{})
var fish = make(chan struct{})

func Dog() {
        <-fish
        fmt.Println("dog")
        dog <- struct{}{}
}

func Cat() {
        <-dog
        fmt.Println("cat")
        cat <- struct{}{}
}

func Fish() {
        <-cat
        fmt.Println("fish")
        fish <- struct{}{}
}

func main() {
        for i := 0; i < 100; i++ {
                go Dog()
                go Cat()
                go Fish()
        }
        fish <- struct{}{}

        time.Sleep(10 * time.Second)
}

03 两个协程交替打印10个字母和数字

思路:采用channel来协调goroutine之间顺序。

主线程一般要waitGroup等待协程退出,这里简化了一下直接sleep。

package main

import (
        "fmt"
        "time"
)

var word = make(chan struct{}, 1)
var num = make(chan struct{}, 1)

func printNums() {
        for i := 0; i < 10; i++ {
                <-word
                fmt.Println(1)
                num <- struct{}{}
        }
}
func printWords() {
        for i := 0; i < 10; i++ {
                <-num
                fmt.Println("a")
                word <- struct{}{}
        }
}

func main() {
        num <- struct{}{}
        go printNums()
        go printWords()
        time.Sleep(time.Second * 1)
}

04 启动 2个groutine 2秒后取消, 第一个协程1秒执行完,第二个协程3秒执行完。

思路:采用ctx, _ := context.WithTimeout(context.Background(), time.Second*2)实现2s取消。协程执行完后通过channel通知,是否超时。

package main

import (
        "context"
        "fmt"
        "time"
)

func f1(in chan struct{}) {

        time.Sleep(1 * time.Second)
        in <- struct{}{}

}

func f2(in chan struct{}) {
        time.Sleep(3 * time.Second)
        in <- struct{}{}
}

func main() {
        ch1 := make(chan struct{})
        ch2 := make(chan struct{})
        ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)

        go func() {
                go f1(ch1)
                select {
                case <-ctx.Done():
                        fmt.Println("f1 timeout")
                        break
                case <-ch1:
                        fmt.Println("f1 done")
                }
        }()

        go func() {
                go f2(ch2)
                select {
                case <-ctx.Done():
                        fmt.Println("f2 timeout")
                        break
                case <-ch2:
                        fmt.Println("f2 done")
                }
        }()
        time.Sleep(time.Second * 5)
}

05 当select监控多个chan同时到达就绪态时,如何先执行某个任务?

可以在子case再加一个for select语句。

func priority_select(ch1, ch2 <-chan string) {
        for {
                select {
                case val := <-ch1:
                        fmt.Println(val)
                case val2 := <-ch2:
                priority:
                        for {
                                select {
                                case val1 := <-ch1:
                                        fmt.Println(val1)

                                default:
                                        break priority
                                }
                        }
                        fmt.Println(val2)
                }
        }

}
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇