本文发布于Cylon的收藏册,转载请著名原文链接~


数据结构

数据类型总结

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。

基础数据类型包括:

  • 基础类型:
    • 布尔型、整型、浮点型、复数型、字符型、字符串型、错误类型。
  • 复合数据类型包括:
    • 指针、数组、切片、字典、通道、结构体、接口。

什么是反射

在计算机科学领域,反射是指一类应用,它们能够自描述自控制

在go中,编译时不知道类型的情况下,可更新变量、运行时查看值、调用方法以及直接对他们的布局进行操作的机制,称为反射。

场景:无法透视一个未知类型的时候,这时候就需要有反射来帮忙你处理,反射使用TypeOf和ValueOf函数从接口中获取目标对象的信息,轻松完成目的。

rune与byte的区别

  • byte是uint8、rune为uint32,一个仅限于ascii码的值,一个支持更多的值。rune比byte能表达更多的数。
  • golang默认使用utf8编码,一个中文占用3字节,一个utf8数字占用1字节,utf8字母占用1字节

切片

切片的扩容:切片扩容,一般方式:上一次容量的2倍,超过1024字节,每次扩容上一次的1/4

切片的截取:在截取时,capacity 不能超过原slice的 capacity

new() 与 make() 的区别

new(T)make(T, args) 是Go语言内建函数,用来分配内存,但适用的类型不用。

  • new函数用于分配指定类型的零值对象,并返回指向其内存地址的指针。例如,new(int)将分配一个类型为int且值为0的对象,并返回一个指向该地址的指针。可以使用*运算符访问指针指向的值。
  • make函数用于创建和初始化内置类型(如mapslicechannel)的数据结构,并返回其指针。它比new函数更加复杂很多,因为它需要知道类型的大小和结构,以便为其分配内存并初始化其字段或元素。例如,make(map[string]int)将创建一个空的map。它有一个string类型的键和一个int类型的值。

nil切片和空切片指向的地址一样吗?

  • nil切片和空切片指向的地址==不一样==。nil空切片引用数组指针地址为0(无指向任何实际地址)
  • 空切片的引用数组指针地址是有的,且固定为一个值

什么是Receiver

Golang的Receiver是绑定function到特定type成为其method的一个参数,即一个function加了receiver就成为一个type的method。

构体方法跟结构体指针方法的区别(Receiver和指针Receiver的区别)

  • T 的方法集仅拥有 T Receiver。
  • *T 方法集则包含全部方法 (Receiver + *Receiver)。

sync.once

  • 是 Golang package 中使方法只执行一次的对象实现,作用与 init 函数类似。但也有所不同

  • init 函数是在文件包首次被加载的时候执行,且只执行一次

  • sync.Onc 是在代码运行中需要的时候执行,且只执行一次

  • 当一个函数不希望程序在一开始的时候就被执行的时候,我们可以使用 sync.Once

  • 实现:sync.Once 的源码实现非常简单,采用的是双重检测锁机制 (Double-checked Locking),是并发场景下懒汉式单例模式的一种实现方式

    • 首先判断 done 是否等于 0,等于 0 则表示回调函数还未被执行
    • 加锁,确保并发安全
    • 在执行函数前,二次确认 done 是否等于 0,等于 0 则执行
    • 将 done 置 1,同时释放锁 疑问一: 为什么不使用乐观锁 CAS 简单的来说就是 f() 的执行结果最终可能是不成功的,所以你会看到现在采用的是双重检测锁机制来实现,同时需要等 f() 执行完成才修改 done 值 疑问二: 为什么读取 done 值的方式没有统一 比较 done 是否等于 0,为什么有的地方用的是 atomic.LoadUint32,有的地方用的却是 o.done。主要原因是 atomic.LoadUint32 可以保证原子读取到 done 值,是并发安全的,而在 doSlow 中,已经加锁了,那么临界区就是并发安全的,使用 o.done 就可以来读取值就可以了

原子操作和互斥锁的区别

文档:https://zhuanlan.zhihu.com/p/147618421

GMP模型

文档:https://zhuanlan.zhihu.com/p/261590663 文档:https://juejin.cn/post/6844904104343388168

  • G:goroutine

  • M:Machine,内核线程

  • P:Logical Processor,处理器;代表了M所需要的上下文环境

    • runtime.GOMAXPROCS (numLogicalProcessors)可以设置多少个处理器,go 1.5开始,默认是CPU核数;
    • 实际运行时P和CPU核心数并无任何关联,P最大不超过256;P可以理解为并行度的多少,也就是说当前最多只能有P个线程在运行;(是不是很像线程池)
    • P一旦初始化了,就不能修改了 三者关系
  • M的数量和P不一定匹配,可以设置很多M,M和P绑定后才可运行,多余的M处于休眠状态。

  • P包含一个LRQ(Local Run Queue)本地运行队列,这里面保存着P需要执行的协程G的队列。

  • 除了每个P自身保存的G的队列外,调度器还拥有一个全局的G队列GRQ(Global Run Queue),这个队列存储的是所有未分配的协程G。

go func()执行流程

  • 创建一个G对象,加入到本地队列或全局队列;
  • 如果还有空闲的P,则创建一个M;
  • M会启动一个底层线程,并结合P,循环执行G;
  • P执行G的顺序是,先从本地队列找,没有则到全局队列找(一次性转移[全局G个数/P个数]),再到其他P中找(一次性转移一半);
  • G是执行顺序是按照队列顺序的;
  • P管理着G队列,但是G要运行,还需要M的绑定;
  • runtime.GOMAXPROCS只会影响P的数量,不会影响M的数量;
  • P和M的关系,就好比用户线程和内核线程的N:M模型;
  • 没有足够的M关联P时,会创建M;在runtime执行系统监控或垃圾回收等任务的时候也会导致新的M的创建。 所以,runtime.GOMAXPROCS只是类似线程池的大小设置而已;
  • 当然,go也可以通过runtime/debug.SeMaxThreads限制操作系统线程数;SetMaxThreads主要用于限制程序无限制的创造线程导致的灾难。目的是让程序在干掉操作系统之前,先干掉它自己。
  • goroutine是按照抢占式调度的,一个goroutine最多执行10ms就会换作下一个;

死锁

死锁是:多个进(线)程是相互竞争的关系,并且互持资源,相互等待,这样产生的永久阻塞的现象称为死锁。

死锁产生的原因

  • 互斥
  • 占有且等待
  • 不可抢占
  • 循环等待

死锁如何解决:死锁的发生很难通过人为干预来解决,只能避免(打破死锁产生的条件)

  • 互斥:线程安全是通过互斥来实现的(无法干预)
  • 占有且等待:申请资源时获取所有所需资源
  • 不可抢占:占用资源的进程在进一步申请其他资源时,如申请不到主动释放已占有的资源
  • 循环等待:按需预防,对所需资源进行排序,按照大小依次申请

Refer deadlock

Go中产生死锁的原因

  • 无缓冲;解决:缓冲或先读后写
  • 缓冲已满(只写不读);解决,需要有消费端
  • 读写互斥;(读写加锁导致一段阻塞变成死锁)
  • 未初始化的channel(读,写,关闭)
  • 多线程只要保证个线程的执行,可以允许死锁
    • 如主进程只读不写造成阻塞,这种情况在没有子线程情况下是死锁

slice和map区别

  • slice是有序的,map是无序的,在每次迭代时,无法确定其顺序
  • slice有容量,map没有容量,map是由go内部控制的数据结构
  • slice可以使用appen(),map不可以
  • slice和map都是引用类型,当作为参数传递时共享相同地址

如何复制slice、map和interface?

这些类型的变量是内存引用类型,可以使用内置函数 copy() 来完成复制 Refer to

a := []int{1, 2}
b := []int{3, 4}
check := a
copy(a, b)
fmt.Println(a, b, check)
// Output: [3 4] [3 4] [3 4]

什么是goroutine

go的多线程是包含在运行时内的一种机制,用的模型是两级,即runtime帮助申请和释放线程,而这个gorutine可以为一对一,也可以为一对多,即操作系统中的 “两级模型”(Two-Level)是严格意义上的多对多模型,可以为单个用户线程专门一对一绑定内核线程的能力的模型

用户线程的缺点:

  • 用户级线程与操作系统的集成度不高;如用空闲线程调度进程,阻塞其线程发起 I/O 的进程,即使该进程有其他线程可以运行,以及有锁的线程取消调度进程。
  • 用户级线程需要非阻塞系统调用,否则,当一个线程阻塞,即使进程中还有可运行的线程,整个进程也会在内核中阻塞。例如,如果一个线程导致页面错误,则进程阻塞。
  • 用户线程和操作系统内核之间缺乏协调性;无论进程有 1 个线程还是 1000 个线程,都仅能获得一个CPU时间片。由每个线程主动将控制权交给其他线程。
  • 由于进程时资源分配的最小单位,多线程情况下,每个线程得到的时间片较少,执行会较慢。

C语言的线程和Goroutine的区别主要表现在以下几个方面

  1. 实现方式不同:C语言的线程是由操作系统内核来实现的,而Goroutine则是通过Go语言的runtime来实现的。
  2. 内存分配方式不同:C语言的线程需要在内存中分配一定的栈空间,而Goroutine则通过自动扩展栈来实现。
  3. 调度方式不同:C语言的线程是由操作系统内核来调度的,而Goroutine是通过Go语言的runtime自己实现的调度器来调度的。
  4. 轻量级:Goroutine是轻量级的线程,一个Goroutine只需要2KB的栈空间,而C语言的线程需要占用更多的内存空间。
  5. 效率高:由于Goroutine是由Go语言的runtime来实现的,因此它具有非常高的执行效率和并发性能,比C语言的线程更加高效。

总的来说,Goroutine是Go语言的并发特性中非常重要的一部分,通过Goroutine可以非常方便地实现高效的并发程序。而C语言的线程则更多地是由操作系统来实现,对于一些需要最大化利用机器性能的场景会更为适合。

应用

Go语言创建TCP连接

  • conn.dial
  • conn.write/read

本文发布于Cylon的收藏册,转载请著名原文链接~

链接:https://www.oomkill.com/2021/10/interview-go/

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」 许可协议进行许可。