这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记
Golang
后端开发常用的语言是Java、Golang、Python。Java语言类型的程序员是目前市面上最多的,也是很多公司都会选择的。Java语言开发的整个后端项目有比较好的项目规范,适用于业务逻辑复杂的情况。本人从事的是Golang语言,Golang语言适用于开发微服务,特点是开发快、效率高等。大多数的公司(字节主力语言 Go, B站主力Go, 腾讯偏向Go 等等),都开始选择用Golang开发,因为这门语言相比于Java、Python最大的一个特点就是节省内存,支持高并发,简洁高效,容易上手学习。
Golang语言的后端框架有很多,像 Gin 、Beego、Iris等,关系型数据库的操作有gorm。这些是Golang后端开发掌握的基础能力。
微服务框架有: go-zero kratos
Golang语言有一些特有的特性,比如协程go routine,它比线程更加轻量级、高效。比如通道channel,是一种通过共享内存支持协程之间的通信方式。在多个go routine消费channel中的数据时,channel内部支持锁机制,每一条消息最终只会分配给一个go routine消费。在Golang内部,有一整套goroutine调度机制GMP,其中G指的是Go routine,M指的是Machine,P指的是Process。GMP的原理大致就是通过全局Cache和各个线程Cache的方式保存需要运行的Go routine,通过Process的协调,将Goroutine分配在有限的Machine上运行。
Golang是支持GC的语言,内部使用三色标记垃圾回收算法,原理大概的说就是通过可达性算法,标记出那些被引用的对象,将剩下来没有被标记也就是需要被释放的对象进行内存回收。在之前比较老的版本,STW的影响比较大,所谓的STW就是在GC的时候,因为多线程访问内存会出现不安全的问题,为了保证内存GC的准确性,在标记对象的时候,会通过屏障停止程序代码继续运行下去,直到所有对象都被处理过后,再继续运行程序,这个短暂的时间,就被成为STW(Stop The World)。由于STW,会导致在程序无法提供服务的问题。在Java中,也存在这种现象。但是目前随着Golang版本的不断更新,GC算法也在不断优化,STW的时间也慢慢越来越短。
需要注意的一点,在定义map的时候,尽量不要在value中存放指针,因为这样会导致GC的时间过长。
就是Golang的map是一个无序map,如果需要从map中遍历数据,需要用slice进行保存,按照一定的顺序进行排序,这样才能保证每次查询出来的数据顺序一致。并且map是无锁并发不安全的。在使用map进行内存缓存的时候,需要考虑到多线程访问缓存带来的安全问题。常见的两种办法,一种是加读写锁RWLock,另一种是使用sync.Map。在写多读少的场景,推荐使用RWLock,因为sync.Map内部使用空间换时间的方法,内部有两个map,一个支持读操作一个支持写操作,当写操作过于频繁,会导致map不断更新,带来的是频繁GC操作,会带来比较大的性能开销。
Golang里面开出来的Go routine是无状态的,如果需要主函数等待Go routine执行完成或者终止Go routine运行,通常有三种方法。第一种是使用sync中的waiteGroup,包含Add、Done、Wait方法。可以类比于Java中的CountDownLatch。第二种是使用Context包中Done方法,将主函数的context带入到Goroutine中,同时在主函数中使用select监听Go routine接收的context发出的Done信号。第三种是自定义一个channel,传入Go routine中,主函数等待读取Go routine中执行完成向channel发送的终止信息。
Golang没有继承的概念,只有组合的概念,每一个struct的定义,可以当做一个类,struct与struct之间可以组合嵌套。在软件设计原则中,类的组合比类的继承更能达到解耦的效果。Golang没有明显的接口实现逻辑,当一个struct实现了一个interface声明的所有方法,这个struct就默认实现了这个interface。在函数调用的入参中,我们通常在调用方传入具体实现了这个interface的struct,而在函数体的接收参数定义这个interface来接收,以此达到被调用函数的复用效果。这也是面向对象特性中多态思想的体现。
在Golang中,error的处理是最繁琐的。基本上十个函数调用有九个会返回error,对于每一个error都需要进行处理或者向上抛。通常在业务逻辑中,我们都会自定义error,声明error的类型。在Golang官方errors包中,error只是一个struct,它提供了New、Wrap、Error等方法,提供了创建error、向上抛出error、输出error信息的功能。所以需要注意的是,我们不能用string的等值比较error是否相同,因为error是一个struct,是一个实例对象,尽管两个error的值信息一样,但是对象在内存中只是一个存放地址值,两者并不相同。通常我们在函数的第一行,使用defer的功能,对函数体中所有的error进行统一的处理。其中defer是延迟处理标志,函数会在return前拦截处理defer匿名函数内的代码。(可以使用 pkg/errors 包解决)
Golang的项目结构在github有一个比较出名的example,可以参考或者模仿。需要注意的是,当外部项目需要调用该项目的代码时,只能调用internel包以外的函数或者对象方法。对于internel包内的代码,对外部调用项目来说,是不可用的。这也是一种代码保护机制。