引导程序整体结构8个关键字:package、import、const、var、func、defer、go、return。

    声明复合数据结构的4个关键字:struct、interface、map、chan。

    控制程序结构的13个关键字:if、else;for、 range、break、continue;switch、select、type、case、default、fallthrough;goto。

    内置数据类型标识符(20个):

          数值:

                整型(12个):byte、int、int8、int16、int32、int64

                                   uint uint8、uint16、uint32、uint64、uintptr

                浮点型:float32    float64

                复数:complex64、complex128

    字符和字符串型: string     rune 

    接口型:error

    布尔型:bool

内置函数(15个):

              make、new、len、cap、append、copy、delete、panic、recover、close、complex、real、image、print、println。

常量标识符(4个):

                 true、false:表示bool类型的两个常亮值。

                 iota://用在连续枚举类型的声明中。

                 nil:  //指针,引用类型的变量的默认值是nil

空白标识符(1个):

                      _

GO操作符47个。

基本类型的可比较性:

  • 布尔值可比较
  • 整数、浮点数、复数值、字符串值
  • 指针值可比较,如果指针指向相同的变量,或者两个指针的值均为nil,则他们相等。
  • 通道值可比较。如果两个通道值是由相同的make函数调用创建的,或者两个值都为nil,则他们相等。
  • 接口值是可以比较的,如果两个接口值具有相同的动态类型和相等的动态值,或者两个值都为nil,则他们相等。
  • 如果结构体的所有字段都是可比较的,则他们的值是可比较的。
  • 如果数组元素类型的值可比较,则数组值可比较。如果两个数组对应的元素相等,则他们相等。
  • 切片、函数、map是不可比较的。

HTTP协议和websocket协议的区别:

较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。

更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。

保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。

更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。

可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。

更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

map的底层原理:

go语言中的map采用的是哈希查找表,由一个key通过哈希函数得到哈希值,64位系统中就生成一个64bit的哈希值,由这个哈希值将key对应到不同的桶(bucket)中,当有多个哈希映射到相同的的桶中时,使用链表解决哈希冲突

根据哈希函数将key生成一个hash值,其中低位hash用来判断桶的位置,高位hash确定在桶中的哪个cell。。每个桶可以存储8对key-value,存储结构不是key/value/key/value...,而是key/key..value/value,这样可以避免字节对齐时的padding,节省内存空间。

当不同的key根据哈希得到的tophash和低位hash都一样,发生哈希碰撞,这个时候就体现overflow pointer字段的作用了。

如果overflow bucket也溢出了呢?那就再给overflow bucket新建一个overflow bucket,用指针串起来就形成了链式结构,map本身有2^B个bucket,只有当发生哈希碰撞后才会在bucket后链式增加overflow bucket。

map加载因子map长度 / 2^B6.5B
装填因子是否大于6.5
装填因子 = 元素个数/桶个数,大于6.5时,说明桶快要装满,需要扩容

overflow bucket是否太多
当bucket的数量 < 2^15,但overflow bucket的数量大于桶数量
当bucket的数量 >= 2^15,但overflow bucket的数量大于2^15

双倍扩容:装载因子多大,直接翻倍,B+1;扩容也不是申请一块内存,立马开始拷贝,每一次访问旧的buckets时,就迁移一部分,直到完成,旧bucket被GC回收。

等量扩容:重新排列,极端情况下,重新排列也解决不了,map成了链表,性能大大降低,此时哈希种子hash0的设置,可以降低此类极端场景的发生。

切片扩容: 

  • 当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量。
  • 当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。

 查找
根据key计算出哈希值
根据哈希值低位确定所在bucket
根据哈希值高8位确定在bucket中的存储位置
当前bucket未找到则查找对应的overflow bucket。
对应位置有数据则对比完整的哈希值,确定是否是要查找的数据
如果当前处于map进行了扩容,处于数据搬移状态,则优先从oldbuckets查找。
插入
根据key计算出哈希值
根据哈希值低位确定所在bucket
根据哈希值高8位确定在bucket中的存储位置
查找该key是否存在,已存在则更新,不存在则插入
map无序
map的本质是散列表,而map的增长扩容会导致重新进行散列,这就可能使map的遍历结果在扩容前后变得不可靠,Go设计者为了让大家不依赖遍历的顺序,故意在实现map遍历时加入了随机数,让每次遍历的起点--即起始bucket的位置不一样,即不让遍历都从bucket0开始,所以即使未扩容时我们遍历出来的map也总是无序的。

channel底层原理:

channel主要用于goroutine之间的通信,分为有缓冲和无缓冲通道两种类型。

chan的实现在runtime/chan.go,是一个hchan的结构体:

type hchan struct {
  qcount   uint           // 队列中的数据个数
  dataqsiz uint           // 环形队列的大小,channel本身是一个环形队列
  buf      unsafe.Pointer // 存放实际数据的指针,用unsafe.Pointer存放地址,为了避免gc
  elemsize uint16 
  closed   uint32 // 标识channel是否关闭
  elemtype *_type // 数据 元素类型
  sendx    uint   // send的 index
  recvx    uint   // recv 的 index
  recvq    waitq  // 阻塞在 recv 的队列
  sendq    waitq  // 阻塞在 send 的队列
  
  lock mutex  // 锁 
}

可以看出,channel本身是一个环形缓冲区,数据存放到堆上面,channel的同步是通过锁实现的,并不是想象中的lock-free的方式,channel中有两个队列,一个是发送阻塞队列,一个是接收阻塞队列。当向一个已满的channel发送数据会被阻塞,此时发送协程会被添加到sendq中,同理,当向一个空的channel接收数据时,接收协程也会被阻塞,被置入recvq中。
 

1、goroutine函数内部的panic能否被main函数捕捉?go并发函数应该注意些什么?

goroutine 函数panic不会被主routine捕获,你需要在routine内捕获。

panic有两种,1是代码调用panic函数触发,可以recover,1中是map并发写冲突,程序只会挂掉,不能回复。

go并发函数应注意三点:1. map并发写 2. 内存溢出OOM 3.死锁

[]byte[]rune
byteuint8uint8runeruneint32int32stringnil

string和[]byte类型转换时,会产生新的内存占用。一般强制类型转换都会产生新的内存占用。

3、请简述切片和数组的区别

               从语法上来看,数组遵循传统的三要素 – 名称、类型、长度。数组的一切传递都是值拷贝。
              而切片只有名称、类型,这意味着切片是不定长的。

              从内存的角度来看,数据是一整块连续的、固定长度、固定位置的内存。
              而切片则是一个指针,指向一块内存,当容量不够时就开辟更大的内存。

切片扩容:slice在append时如果超出了原来的容量时会翻倍扩容

//原切片长度低于1024时直接翻倍

//原切片长度大于等于1024时,每次只增加25%,直到满足需要的容量

4、在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。

list 的初始化有两种方法:分别是使用 New() 函数和 var 关键字声明,两种方法的初始化效果都是一致的。

1) 通过 container/list 包的 New() 函数初始化 list

变量名 := list.New()

2) 通过 var 关键字声明初始化 list

var 变量名 list.List

在列表中添加删除元素

package main
import "container/list"
func main() {
    l := list.New()
    // 尾部添加
    l.PushBack("canon")
    // 头部添加
    l.PushFront(67)
    // 尾部添加后保存元素句柄
    element := l.PushBack("fist")
    // 在fist之后添加high
    l.InsertAfter("high", element)
    // 在fist之前添加noon
    l.InsertBefore("noon", element)
    // 使用
    l.Remove(element)
}

遍历列表:

l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
for i := l.Front(); i != nil; i = i.Next() {
    fmt.Println(i.Value)
}

5、go语言中哪些类型的值可以被取地址,哪些不可以被取地址?

              Go中以下的值是可寻址的,因此可以被取地址:

              变量、可寻址的结构体的字段、可寻址的数组的元素、任意切片的元素(无论是可寻址切片或不可寻址切片)、指针解引用操作

Go中以下的值是不可寻址的:

字符串的字节、元素映射、元素接口值的动态值(类型断言的结果)、常量值字面值、声明的包级别函数方法(用做函数值)、表达式中间结果值、数据通道接收操作、子字符串操作、子切片操作、加法、减法、乘法、以及除法等等。

函数调用显式值转换各种操作,不包含指针解引用(dereference)操作,但是包含数据通道接收操作、子字符串操作、子切片操作,以及加法/减法/乘法/除法等等。

6、为什么两个nil值有时候会不相等?

    1. nil 标识符是不能比较的

    2. nil 不是关键字或保留字

    3. nil 没有默认类型

    4. 不同类型 nil 的指针是一样的

    5. 不同类型的 nil 是不能比较的

    6. 两个相同类型的 nil 值也可能无法比较

    7. nil 是 map、slice、pointer、channel、func、interface 的零值

   8. 不同类型的 nil 值占用的内存大小可能是不一样的

Gomapslicefunctionnil
mapslicefunction

7、简述go语言中make和new的区别。

           new:原来初始化泛型,并且返回指针存储的位置。

           make:用来初始化一些特别的类型,如slice、map、channel,返回没有指针。

8、哪些类型是值类型,那些是引用类型?

               值类型:int、float、bool、sturct等

              引用类型有:数组、切片、map、channel、interface。

9、函数返回局部变量的指针是否安全?

         能不能返回局部指针变量,不在于这个指针变量的类型和性质(不在于该指针是不是局部指针变量),而在于该指针指向的对象的类型和性质。

         如果该指针指向函数内部的栈空间,则程序非法,如果指向静态区域的地址,则合法。

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

10、switch流程控制代码块中的case表达式能重复吗?

switch-case代码块中的case常量表达式有时候可以重复,有时候则不可以。Go白皮书特地说明了具体编译器可以使用不同的实现。比如,下面这个例子使用标准Go编译器和gccgo都是编译不过的,因为这两个编译器都不允许重复的数值型的case常量表达式。

package main
func main() {
  switch n := 1; n { 
     case 0, 1, 2: 
     case 2, 3: // error: 2重复了 
 }
}

标准Go编译器和gccgo都允许重复的布尔case常量表达式

package main
func main() {
 switch { 
     case 1 != 1, 2 == 2:
     case 3 == 3, true:
     case false, false:
     case false: 
  }
}

标准Go编译器不允许重复的字符串型的case常量表达式,但是gccgo却允许。

package main
func main() {
  switch 'abc' { 
      case 'abc', 'xyz', 'def': 
      case 'abc', 'xyz':    // 上面这行标准编译器报错,    // 但是gccgo允许。 
 }
}

11、简要描述go中的main和init函数的区别:

  • 相同点:

    • 两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
  • 不同点:

    • init可以应用于任意包中,且可重复定义多个
    • main函数只能用于main包中,且只能定义一个
  • 两个函数的执行顺序:

    • 对同一个go文件的init()调用顺序是从上到下的。

    • 对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。

    • 对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。

12、简述下闭包的生命周期和作用范围。

闭包是携带状态的函数,它是将函数内部和函数外部连接起来的桥梁。通过闭包,可以读取函数内部的变量,也可以使用闭包封装私有状态,让他们常驻内存当中。

闭包能够引用其作用域上部的变量进行修改,被捕获到闭包中的变量将随着闭包的生命周期一直存在。

13、go语言是一个面向对象语言吗?

            是的。

14、对任意的非指针和非接口定义类型T,为什么类型*T的方法集总是类型T的方法集的超集,但是反之却不然?

     

16、罗列出在go语言中哪些行为会触发异常,不少于3种。

    参数或者函数的值为nil也会触发异常

     类型断言错误会引发异常

     运行时错误也会触发异常

     goroutine高并发时,容易死锁也会触发异常

17、函数调用time.Sleep(d)和数据通道接收<-time.After(d)操作之间有何区别?

time.Time

19、select可以用于什么?

            用于多路监听多个通道

20、无缓冲和缓冲通道之间有什么区别?

     无缓冲通道:cap()和len()都是0。用于通信和goroutine同步。

     有缓冲通道:len代表没有读取的元素数,cap代表整个通道的容量。主要用于通信

管道没有缓冲区时,从管道读数据会阻塞,直到有协程向管道中写入数据。类似的向管道中写入数据也会阻塞,直到有协程从管道中读取数据。

管道有缓冲区但缓冲区没有数据时,从管道读取数据也会阻塞,直到有协程写入数据。类似的,向管道写入数据时,如果缓冲区已满,那么也会阻塞,直到有协程从缓冲区中读取数据。

对于值为nil的管道,无论读写都会阻塞,而且是永久阻塞。

使用内置函数close()可以关闭通道,尝试已经关闭的通道写入数据会触发panic,但关闭的管道仍可读。

x, ok := <-ch

第一个变量表示读出的数据,第二个变量(bool类型)表示是否成功读取了数据。第二个变量不用于指示管道的关闭状态 。

一个关闭的管道有两种情况:

      ① 管道缓冲区没有数据   // ok =false

      ② 管道缓冲区有数据      // ok=true

只有管道已经关闭且缓冲区没有数据时,管道读取表达式返回的第二个变量才跟管道关闭状态一致。

goroutine退出后,写到缓冲通道中的数据不会消失,它可以缓冲和适配两个goroutine处理速度不一致的情况,缓冲通道和消息队列类似,有削峰和增大吞吐量的功能。

21、goroutine的泄漏怎么处理?

常见导致内存泄露的情况:内存泄漏是如何产生的呢?

1、发送一个没有接受者的channel

2、nil channel      写入到nil channel会永远阻塞

go是一门自己gc的语言,大概两分钟会gc一次。如果有内存泄漏无非两种情况

  • 有goroutine泄漏,goroutine“飞”了,zombie goroutine没有结束,这个时候在这个goroutine上分配的内存对象将一直被这个僵尸goroutine引用着,进而导致gc无法回收这类对象,内存泄漏。

  • 有一些全局(或者生命周期和程序本身运行周期一样长的)的数据结构意外的挂住了本该释放的对象,虽然goroutine已经退出了,但是这些对象并没有从这类数据结构中删除,导致对象一直被引用,无法被回收。

排除掉goroutine泄漏

首先,使用runtime.NumGoroutine返回正在执行和排队的任务总数,没有观察有没有泄漏的goroutine,确定goroutine是否泄露。

其次,确定是不是全局变量无回收

什么是pprof?

pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。

基本使用

使用pprof有多种方式,Go已经现成封装好了1个:net/http/pprof,使用简单的几行命令,就可以开启pprof,记录运行信息,并且提供了Web服务,能够通过浏览器和命令行2种方式获取运行数据。


import (
"fmt"
"net/http"
_ "net/http/pprof"
)
 
func main() {
// 开启pprof,监听请求
ip := "127.0.0.1:6060"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
}

我们输入ip:port/debug/pprof/打开pprof主页
例如我的地址
http://127.0.0.1:6060/debug/pprof/

full goroutine stack dump
goroutine
go tool pprof -inuse_space []()

这个命令的作用是, 抓取当前程序已使用的 heap. 抓取后, 就可以进行类似于 gdb 的交互操作.

  • top 命令, 默认能列出当前程序中内存占用排名前 10 的函数. 

22、如何实现消息队列(多生产者,多消费者)?

package main
import ( 
 "fmt" 
 "time"
)
func consumer(cname string, ch chan int) { 
       //可以循环 for i := range ch 来不断从 channel 接收值,直到它被关闭。
     for i := range ch {
         fmt.Println("consumer-----------", cname, ":", i) 
     } 
     fmt.Println("ch closed.")
 }
 
func producer(pname string, ch chan int) { 
     for i := 0; i < 4; i++ { 
         fmt.Println("producer--", pname, ":", i) 
         ch <- i 
     }
 }
 
 
 func main() { 
 //用channel来传递"产品", 不再需要自己去加锁维护一个全局的阻塞队列 
 ch := make(chan int) 
 go producer("生产者1", ch) 
 go producer("生产者2", ch) 
 go consumer("消费者1", ch) 
 go consumer("消费者2", ch) 
 time.Sleep(10 * time.Second) 
 close(ch) 
 time.Sleep(10 * time.Second)
 }

for i := range ch {
 fmt.Println("consumer-----------", cname, ":", i) 

 } 

这个也可以改成:LOOP:
 for {
     select {
         case i,ok:=<-ch:
             if ok {
                 fmt.Println("consumer--------", cname, ":", i) 
             } else {
                 break LOOP
             }
 
      }
 }

//注意: i := <- ch 从空的channel中读取数据不会panic, i读取到的值是0, 如果channel是bool的,那么读取到的是fa

23、什么是乐观锁?什么是悲观锁?各自的应用场景是什么?

悲观锁:

当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。

借助数据库锁机制,在修改数据之前先锁定再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。

2️⃣悲观锁主要分为共享锁和排他锁:

  • 共享锁shared locks】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改
  • 排他锁exclusive locks】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。

乐观锁:(适用于读操作多的场景

设数据一般情况下不会造成冲突,所以在数据进行提交更新时才正式数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。适用于读操作多的场景,这样可以提高程序的吞吐量

乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身保证数据正确性。

乐观锁机制采取了更加宽松的加锁机制。乐观锁是相对悲观锁而言,也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制。

乐观锁的实现:

  1. CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
  2. 版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会+1。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

2️⃣说明
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁

拿比较常用的 MySql Innodb 引擎举例,来说明一下在 SQL 中如何使用悲观锁。

使用悲观锁必须关闭 MySQL 数据库的自动提交属性。因为 MySQL 默认使用 autocommit 模式,也就是说,当执行一个更新操作后,MySQL 会立刻将结果进行提交。(sql语句:set autocommit=0)

使用乐观锁就不需要借助数据库的锁机制了。

主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是。

五、如何选择乐观锁和悲观锁?

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。
1️⃣响应效率:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
2️⃣冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。
3️⃣重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
4️⃣乐观锁如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户从新操作。悲观锁则会等待前一个更新完成。这也是区别。

随着互联网的提出,悲观锁已经越来越少的被应用到生产环境中了,尤其是并发量比较大的业务场景。

24、赋值是原子操作吗?

         不一定是,还跟上下文有关,如果上下文是一个没有并发进程的程序,那么该代码在该上下文中就是原子的。

25、值传递和指针传递的区别?

T*T

一般的判断标准是看副本创建的成本和需求。

T*T*TT*T

26、panic和recover

       panic用来主动抛出错误,recover用来捕获panic抛出的错误。

       panic的入口是一个空接口类型interface{ },任何变量都可以传递给panic。

27、内置map和syn.map的区别?

内置map在并发情况下,只读线程安全的,同时写线程不安全,所以为了并发安全 & 高效,官方实现了一把。

sync.Map也是在golang提供的map关键字之上封装实现的。

sync.Map 整体的优化可以描述为以下几点:

空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
map只保存key和对应的value的指针,这样可以并发的读写map, 实际更新指向value的指针再通过基于CAS的无锁atomic。
使用只读数据(read),避免读写冲突
动态调整,miss次数多了之后,将dirty数据提升为read。
double-checking。
延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
优先从read读取、更新、删除,因为对read的读取不需要锁。
 

28、两个结构体可以比较吗?

1、两个结构体无论类型是否相同,是否包含不可比较数据类型,都可以通过reflect.DeepEqual(struct1,struct2)进行比较

2、若两结构体是同一个结构体类型,且该结构体类型中不包含map,slice等不可比较数据类型,那么这两个结构体可以使用“==”进行比较是否相等。