一、前言

     语言是一种解决问题的工具,工欲善其事必先利其器,今天总结一下golang的一些知识点,以便闲暇时候回顾。

二、golang知识图谱

 

三、Golang知识点

在go语言中,new和make的区别?

newmake(T, args)new(T)

在go语言中,Printf()、Sprintf()、Fprintf()函数的区别用法是什么?

都是把格式好的字符串输出,只是输出的目标不一样:

Printf(),是把格式字符串输出到标准输出(一般是屏幕,可以重定向),Printf() 是和标准输出文件(stdout)关联的,Fprintf 则没有这个限制.

Sprintf(),是把格式字符串输出到指定字符串中,所以参数比printf多一个char*。那就是目标字符串地址。

Fprintf(), 是把格式字符串输出到指定文件设备中,所以参数笔printf多一个文件指针FILE*。主要用于文件操作。Fprintf()是格式化输出到一个stream,通常是到文件。

说说go语言中,数组与切片的区别?

var array [10]int
var array = [5]int{1,2,3,4,5}
var slice []type = make([]type, len)

说说下列命令的含义

go env: #用于查看go的环境变量

go run: #用于编译并运行go源码文件

go build: #用于编译源码文件、代码包、依赖包

go get: #用于动态获取远程代码包

go install: #用于编译go文件,并将编译结构安装到bin、pkg目录

go clean: #用于清理工作目录,删除编译和安装遗留的目标文件

go version: #用于查看go的版本信息

说说go语言中的协程?

协程和线程都可以实现程序的并发执行;

通过channel来进行协程间的通信;

只需要在函数调用前添加go关键字即可实现go的协程,创建并发任务;

关键字go并非执行并发任务,而是创建一个并发任务单元;

说说go语言中的for循环?

for循环支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环 for循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量

说说go语言中的switch语句?

单个case中,可以出现多个结果选项,只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case

go语言中没有隐藏的this指针,这句话是什么意思?

方法施加的对象显式传递,没有被隐藏起来,golang的面向对象表达更直观,对于面向过程只是换了一种语法形式来表达,方法施加的对象不需要非得是指针,也不用非得叫this

go语言中的引用类型包含哪些?

数组切片、字典(map)、通道(channel)、接口(interface)

go语言中指针运算有哪些?

可以通过“&”取指针的地址

可以通过“*”取指针指向的数据

说说go语言的main函数

main函数不能带参数

main函数不能定义返回值

main函数所在的包必须为main包

main函数中可以使用flag包来获取和解析命令行参数

说说go语言的同步锁?

(1) 当一个goroutine获得了Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex

(2) RWMutex在读锁占用的情况下,会阻止写,但不阻止读

(3) RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占

说说go语言的channel特性?

A. 给一个 nil channel 发送数据,造成永远阻塞

B. 从一个 nil channel 接收数据,造成永远阻塞

C. 给一个已经关闭的 channel 发送数据,引起 panic

D. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值

E. 无缓冲的channel是同步的,而有缓冲的channel是非同步的

go语言触发异常的场景有哪些?

A. 空指针解析

B. 下标越界

C. 除数为0

D. 调用panic函数

说说go语言的beego框架?

A. beego是一个golang实现的轻量级HTTP框架

B. beego可以通过注释路由、正则路由等多种方式完成url路由注入

C. 可以使用bee new工具生成空工程,然后使用bee run命令自动热编译

说说go语言的goconvey框架?

A. goconvey是一个支持golang的单元测试框架

B. goconvey能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面

C. goconvey提供了丰富的断言简化测试用例的编写

 go语言中,GoStub的作用是什么?

A. GoStub可以对全局变量打桩

B. GoStub可以对函数打桩

C. GoStub不可以对类的成员方法打桩

D. GoStub可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为

说说go语言的select机制?

A. select机制用来处理异步IO问题

B. select机制最大的一条限制就是每个case语句里必须是一个IO操作

C. golang在语言级别支持select关键字

说说进程、线程、协程之间的区别?

进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元;

同一个进程中可以包括多个线程;

进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束;

线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程;

进程的创建调用fork或者vfork,而线程的创建调用pthread_create;

线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源;

进程是资源分配的单位

线程是操作系统调度的单位

进程切换需要的资源很最大,效率很低 线程切换需要的资源一般,效率一般 协程切换任务资源很小,效率高 多进程、多线程根据cpu核数不一样可能是并行的 也可能是并发的。协程的本质就是使用当前进程在不同的函数代码中切换执行,可以理解为并行。 协程是一个用户层面的概念,不同协程的模型实现可能是单线程,也可能是多线程。

进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。(全局变量保存在堆中,局部变量及函数保存在栈中)

线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是这样的)。

协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。

协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。

 Golang Slice的底层实现
切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。

切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。

切片对象非常小,是因为它是只有3个字段的数据结构:

  • 指向底层数组的指针
  • 切片的长度
  • 切片的容量

这3个字段,就是Go语言操作底层数组的元数据。

map如何顺序读取?

可以通过sort中的排序包进行对map中的key进行排序

package main

import (
    "fmt"
    "sort"
)

func main() {
    var m = map[string]int{
        "hello":         0,
        "morning":       1,
        "my":            2,
        "girl":   		3,
    }
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    for _, k := range keys {
        fmt.Println("Key:", k, "Value:", m[k])
    }
}

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


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

  • 字符串的字节元素
  • 映射元素
  • 接口值的动态值(类型断言的结果)
  • 常量值
  • 字面值
  • 声明的包级别函数
  • 方法(用做函数值)
  • 中间结果值

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

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

Golang GC 时会发生什么?

首先我们先来了解下垃圾回收.什么是垃圾回收?

内存管理是程序员开发应用的一大难题。传统的系统级编程语言(主要指C/C++)中,程序开发者必须对内存小心的进行管理操作,控制内存的申请及释放。因为稍有不慎,就可能产生内存泄露问题,这种问题不易发现并且难以定位,一直成为困扰程序开发者的噩梦。如何解决这个头疼的问题呢?

过去一般采用两种办法:

  • 内存泄露检测工具。这种工具的原理一般是静态代码扫描,通过扫描程序检测可能出现内存泄露的代码段。然而检测工具难免有疏漏和不足,只能起到辅助作用。

  • 智能指针。这是 c++ 中引入的自动内存管理方法,通过拥有自动内存管理功能的指针对象来引用对象,是程序员不用太关注内存的释放,而达到内存自动释放的目的。这种方法是采用最广泛的做法,但是对程序开发者有一定的学习成本(并非语言层面的原生支持),而且一旦有忘记使用的场景依然无法避免内存泄露。

为了解决这个问题,后来开发出来的几乎所有新语言(java,python,php等等)都引入了语言层面的自动内存管理 – 也就是语言的使用者只用关注内存的申请而不必关心内存的释放,内存释放由虚拟机(virtual machine)或运行时(runtime)来自动进行管理。而这种对不再使用的内存资源进行自动回收的行为就被称为垃圾回收。

常用的垃圾回收的方法:

  • 引用计数(reference counting)

这是最简单的一种垃圾回收算法,和之前提到的智能指针异曲同工。对每个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。当引用计数为0时则立即回收对象。

这种方法的优点是实现简单,并且内存的回收很及时。这种算法在内存比较紧张和实时性比较高的系统中使用的比较广泛,如ios cocoa框架,php,python等。

但是简单引用计数算法也有明显的缺点:

  1. 频繁更新引用计数降低了性能。

一种简单的解决方法就是编译器将相邻的引用计数更新操作合并到一次更新;还有一种方法是针对频繁发生的临时变量引用不进行计数,而是在引用达到0时通过扫描堆栈确认是否还有临时对象引用而决定是否释放。等等还有很多其他方法,具体可以参考这里。

  1. 循环引用。

当对象间发生循环引用时引用链中的对象都无法得到释放。最明显的解决办法是避免产生循环引用,如cocoa引入了strong指针和weak指针两种指针类型。或者系统检测循环引用并主动打破循环链。当然这也增加了垃圾回收的复杂度。

  • 标记-清除(mark and sweep)

标记-清除(mark and sweep)分为两步,标记从根变量开始迭代得遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。这种方法解决了引用计数的不足,但是也有比较明显的问题:每次启动垃圾回收都会暂停当前所有的正常代码执行,回收是系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如三色标记法)优化了这个问题。

  • 分代搜集(generation)

java的jvm 就使用的分代回收的思路。在面向对象编程语言中,绝大多数对象的生命周期都非常短。分代收集的基本思想是,将堆划分为两个或多个称为代(generation)的空间。新创建的对象存放在称为新生代(young generation)中(一般来说,新生代的大小会比 老年代小很多),随着垃圾回收的重复执行,生命周期较长的对象会被提升(promotion)到老年代中(这里用到了一个分类的思路,这个是也是科学思考的一个基本思路)。

因此,新生代垃圾回收和老年代垃圾回收两种不同的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。

Golang GC 时会发生什么?

Golang 1.5后,采取的是“非分代的、非移动的、并发的、三色的”标记清除垃圾回收算法。

golang 中的 gc 基本上是标记清除的过程:

 

gc的过程一共分为四个阶段:

  1. 栈扫描(开始时STW)
  2. 第一次标记(并发)
  3. 第二次标记(STW)
  4. 清除(并发)

整个进程空间里申请每个对象占据的内存可以视为一个图,初始状态下每个内存对象都是白色标记。

  1. 先STW,做一些准备工作,比如 enable write barrier。然后取消STW,将扫描任务作为多个并发的goroutine立即入队给调度器,进而被CPU处理
  2. 第一轮先扫描root对象,包括全局指针和 goroutine 栈上的指针,标记为灰色放入队列
  3. 第二轮将第一步队列中的对象引用的对象置为灰色加入队列,一个对象引用的所有对象都置灰并加入队列后,这个对象才能置为黑色并从队列之中取出。循环往复,最后队列为空时,整个图剩下的白色内存空间即不可到达的对象,即没有被引用的对象;
  4. 第三轮再次STW,将第二轮过程中新增对象申请的内存进行标记(灰色),这里使用了write barrier(写屏障)去记录

Golang gc 优化的核心就是尽量使得 STW(Stop The World) 的时间越来越短。

详细的Golang的GC介绍可以参看Golang垃圾回收.

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

根据Goroutine和channel的读写可以实现消息队列 。


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)
}

Go运行时内存分配的策略


对于小对象(<=32kb),go runtime首先从,Cache开始,然后是Cental,最后Heap。
对于大对象(>32KB),直接从堆中获取。

  • heap: 全局根对象。负责向操作系统申请内存,管理由垃圾回收器收回的空闲 span 内存块。
  • central: 从 heap 获取空闲 span,并按需要将其切分成 object 块。heap 管理着多个central 对象,每个 central 负责处理一一种等级的内存分配需求。
  • cache: 运行行期,每个 cache 都与某个具体线程相绑定,实现无无锁内存分配操作。其内部有个以等级为序号的数组,持有多个切分好的 span 对象。缺少空间时,向等级对应的 central 获取新的 span 即可。
     

怎么限制Goroutine的数量?

package main

import "fmt"

var ch chan  int

func elegance(){
	<-ch
	fmt.Println("the ch value receive",ch)
}

func main(){
	ch = make(chan int,5)
	for i:=0;i<10;i++{
		ch <-1
		fmt.Println("the ch value send",ch)
		go elegance()
		fmt.Println("the result i",i)
	}

}

package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)
// Pool Goroutine Pool
type Pool struct {
	queue chan int
	wg *sync.WaitGroup
}
// New 新建一个协程池
func NewPool(size int) *Pool{
	if size <=0{
		size = 1
	}
	return &Pool{
		queue:make(chan int,size),
		wg:&sync.WaitGroup{},
	}
}
// Add 新增一个执行
func (p *Pool)Add(delta int){
	// delta为正数就添加
	for i :=0;i<delta;i++{
		p.queue <-1
	}
	// delta为负数就减少
	for i:=0;i>delta;i--{
		<-p.queue
	}
	p.wg.Add(delta)
}
// Done 执行完成减一
func (p *Pool) Done(){
	<-p.queue
	p.wg.Done()
}
// Wait 等待Goroutine执行完毕
func (p *Pool) Wait(){
	p.wg.Wait()
}

func main(){
	// 这里限制5个并发
	pool := NewPool(5)
	fmt.Println("the NumGoroutine begin is:",runtime.NumGoroutine())
	for i:=0;i<20;i++{
		pool.Add(1)
		go func(i int) {
			time.Sleep(time.Second)
			fmt.Println("the NumGoroutine continue is:",runtime.NumGoroutine())
			pool.Done()
		}(i)
	}
	pool.Wait()
	fmt.Println("the NumGoroutine done is:",runtime.NumGoroutine())
}

 

如何测试代码是否有goroutine泄漏的?

使用runtime.Stack在测试代码运行前后计算goroutine数量,当然我理解测试代码运行完成之后是会触发gc的。如果触发gc之后,发现还有goroutine没有被回收,那么这个goroutine很有可能是被泄漏的。

堆栈将调用goroutine的堆栈跟踪格式化为buf 并返回写入buf的字节数。如果全部为真,则在当前goroutine的跟踪之后,Stack格式化所有其他goroutine的跟踪到buf中。
func Stack(buf []byte, all bool) int {
	if all {
		stopTheWorld("stack trace")
	}

	n := 0
	if len(buf) > 0 {
		gp := getg()
		sp := getcallersp()
		pc := getcallerpc()
		systemstack(func() {
			g0 := getg()
			// Force traceback=1 to override GOTRACEBACK setting,
			// so that Stack's results are consistent.
			// GOTRACEBACK is only about crash dumps.
			g0.m.traceback = 1
			g0.writebuf = buf[0:0:len(buf)]
			goroutineheader(gp)
			traceback(pc, sp, 0, gp)
			if all {
				tracebackothers(gp)
			}
			g0.m.traceback = 0
			n = len(g0.writebuf)
			g0.writebuf = nil
		})
	}

	if all {
		startTheWorld()
	}
	return n
}

常用的第三方包

机器学习