golang项目实战《阿里面试常见题》
golang介绍;总结有福利
Go是Google公司开发的一门开源的静态强类型程序设计语言,使用Go语言能够构建简单,可靠,高效率的软件
优势:
1、运行速度快,简单易学。2、跨平台性强,部署简单。3、丰富的标准库,特别是网络库。4、可以直接包含C代码,利用现有的丰富的C库。5、语言层面支持并发,能够轻松的写出高并发的程序。
适合做什么:
1、网络编程,例如网站开发。
2、服务器编程,日志,文件处理。
3、区块链开发
Golang入门;学习一门新的开发语言最重要的就是做到三点:
基础知识学习抄代码学习写代码Go 语言
Go 是 Google 开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。 摘自维基百科。
Go 语言的演进
服务开发语言有哪些?
C/C++JavaPythonGolangphpRustNodeJSErlangRuby…为什么选择 Go ?
天生的服务器编程语言。天生的并发模型。Golang基础Golang 变量-Variable
一、变量以及声明二、零值(nil)三、Golang 特殊运算符1.按位置零 &^2.异或(^)[XOR]一、变量以及声明
1.1 标记(identify)\
规范定义:
标识符 命名 程序实体。
标识符是一个或多个 Unicode 字母和数字的序列。但标识符的第一个字符必须是Unicode 字母(字母和下划线 _)
短变量声明(简式声明 short variable declarations),会根据其使用环境自动推断出去具备的类型。短变量声明在函数内部使用。
注意简式声明和全局变量产生的偶然的变量隐藏,即全局变量被覆盖。例:
5.Go 语言的强制规定,在函数内一定要使用全部声明的变量,若存在未使用的变量,就会发生编译错误(如下所示),因此可以将不需要使用的变量使用空白标识符 _ 或直接注释掉。但未使用的全局变量是不限制的,如上面的 全局变量 b,声明了但是未使用,没有发生编译错误。
6._ (空白标识符)实际上是一个只写变量,不能得到他的值。
7.GO 语言中,如果导入的包未使用,也是不能通过编译的。如不直接使用包里的函数,而只是调用包中的 init() 函数,或者调试代码时去掉了对某些包的功能使用,可以使用下划线标识符,来作为这个包的名字,避免编译失败。
8.被引用的变量一般存储在堆内存中,以便系统进行垃圾回收()GC)。
二、零值(nil)
当一个变量被var 声明之后,如果没有为其声明指定初始值,Go会自动初始化其值为该类型的默认零值。nil 零值,如下所示:3.复合类型的零值,Go会自动递归将每一个元素初始化为其对应类型的零值。
4,Go语言中,需要特别注意,string 字符串的零值不是nil,而是"";
5.对一个nil 的切片中添加元素是没有问题的,但是需要注意对一个map 做同样的事将会发生 panic 运行时恐慌。1
nil map 发生运行时恐慌。
三、Golang 特殊运算符
1.按位置零 &^
将指定位上的值置为零。(将运算符左边数据相异的位保留,相同的位置零)
计算过程:
X、Y 分别用二进制表示, 0000 0010 &^ 0000 0100 = 0000 0010 。若 y的某位是0 ,则取x 上对应位的值;若 y 的某位上 是 1,则结果位上取 0.
(1)如果右侧位是0,则左侧位保持不变。
(2)如果右侧位是1,则左侧位置零。
2.异或(^)[XOR]
Go 语言中,XOR 作为二元运算符存在。但是作为一元运算符出现表示按位取反。
XOR作为二元运算符,是不进位加法计算,即异或计算,例 0000 0010 + 0000 0100 = 0000 0110 = 6
取余运算符只能作用于整数:9%4 =1
浮点数除以 0.0 会返回一个无穷尽的结果,用 +Inf 表示;
5.++;-- 在Go 中是语句,非表达式。
6.rand.Seed(value) 函数提供伪随机数的生成种子,一般情况下都会使用当前时间(纳秒)做自变量。
Golang实战开发环境搭建
golang 的开发环境搭建比较简单,由于是编译型语言,写好 golang 源码后,只需要执行 go build 就能将源码编译成对应平台(本文中默认为 linux)上的可执行程序。本文不再赘述如何搭建 golang 开发环境,只说明下需要注意的地方。
从官网下载对应平台的 golang 安装包中包括 golang 的编译器、一些工具程序和标准库源码。早期的 golang 版本中,需要设置 GOROOT 和 GOPATH 两个环境变量。
从 1.8 版开始,GOPATH 不再需要显示设置。如果没有显示设置,则 GOPATH 的默认值为 $HOME/go 。GOPATH 可以设置多个目录,但推荐只设置一个或直接使用默认值,多个 GOPATH 会造成依赖管理的困难。推荐将 $GOPATH/bin 加到 $PATH 里,这样通过 go install 会安装到 $GOPATH/bin 目录的可执行程序可以像系统命令一样直接运行,不用输入完整路径。
从 1.10 版开始, GOROOT 也不再需要显示设置了,只需要将安装包中的 bin 目录加到 $PATH 里,系统会自动推导出 GOROOT 的值。
编辑器根据个人喜好选择,作者主要使用 vim 和 vscode 。这里介绍了使用 vim 时需要安装的插件(安装过程可能需要翻墙,YCM 安装比较复杂可以不要,gocode 够用了)。
hello world
以下是 golang 版本的 hello world:
golang 安装包自带的 gofmt 能将源码格式化成官方推荐的风格,建议将这个工具整合到编辑器里。
这个简单的程序用 go build 编译出来可执行程序用 ldd 查看发现没有任何动态库依赖,size 也比较大(1.8M ,对等的 C 程序版本只有 7.5K)。实际上这里也体现了 golang 的哲学:直接通过源代码分发软件,所有的代码编到一整个可执行程序里,基本没有动态库依赖(或者只依赖 C/C++ 运行时库和基本的系统库),这也方便了 docker 化(C/C++ 程序员应试能体会动态库依赖有多恶心)。通过 readelf 查看可执行程序会发现代码段和调试信息段占用了比较大的空间,代码段大是因为 golang 的运行时也在里面。调试信息段方便 golang 进程 panic 时会打印详细的进程堆栈及源码信息,这也是为什么 golang 的可执行程序比较大的原因。
命名规范
golang 的标准库提供了 golang 程序命名规范很好的参考标准,命名规范应该尽量和标准库的风格接近,多看下标准库的代码就能体会到 golang 的命名哲学了。
命名在很大程序上也体现了一名程序员的修养,用好的命名写出的代码通常是自注释的,只需要在有复杂的逻辑需要解释的情况下才额外注释。
好的命名应该具有以下特征:
一致性:见名知义,比如标准库中将对象序列化成字符串的操作名为 String ,在你自己的代码里将自定义类型的对象序列化成字符串也应该叫这个名字,并且签名和标准库要一致;
简明精炼:减少敲键盘的次数;
精确性:不要使用有歧义的命名。
通常变量的作用域越广,变量的名字应该越长,反之亦然。
golang 中一般使用驼峰命名法,尽量不要使用下划线(基本只在全大写的常量命名中使用)。首字母缩略词应该全部大写,比如 ServeHTTP , IDProcessor 。
本文中出现的必须、 禁止是指强烈推荐的 golang 风格的规范,但违反这个规范并不会导致程序编译不过。
常量
全大写或者驼峰命名都可以,全大写的情况下可使用下划线分隔单词:
局部变量
通过以下代码片断举例说明局部变量的命名原则:
惯用的变量名应该尽可能短:
使用 i 而不是 index
使用 r 而不是 reader
使用 b 而不是 buffer
这几个字母在 golang 中有约定俗成的含义,使用单字母名字是更 golang 的方式(可能在其他语言的规范中是反例),其他可以举一反三。
变量名中不要有冗余的信息,在函数 RuneCount 里,计数器命名就不需再把 rune 包含进来了,直接用 count 就好了。
在判断 Map 中是否存在某个键值或者接口的转型操作里,通常用 ok 来接收判断结果:v, ok := m[k]。
上文中的示例代码按照以上原则重构后应该是这个样子:
形参
形参的命名原则和局部变量一致。另外 golang 软件是以源代码形式发布的,形参连同函数签名通常会作为接口文档的一部分,所以形参的命名规范还有以下特点。
如果形参的类型已经能明确说明形参的含义了,形参的名字就可以尽量简短:
如果形参类型不能说明形参的含义,形参的命名则应该做到见名知义:
返回值
跟形参一样,可导出函数的返回值也是接口文档的一部分,所以可导出函数的必须使用命名返回值:
接收器(Receivers)
习惯上接收器的命名命名一般是 1 到 2 个字母的接收器类型的缩写:
同个类型的不同方法中接收器命名要保持一致,不要在一个方法中叫 r ,在另一个方法中又变成了 rdr 。
包级导出名
包导出的变量、常量、函数、类型使用时有包名的修饰。这些导出名字里就不再需要包含包名的信息了,所以标准库中 bytes 包里的 Buffer 不需要叫 BytesBuffer 。
接口
只有 1 个方法的接口名通常用方法名加上 er 后缀,不引起迷惑的前提下方法名可以使用缩写:
如果接口有多个方法,则需要选择一个最能精确概括描述接口目的的名词命名(有点难度),但是禁止用多个方法中的某个方法加上 er 后缀来命名,否则别人会误解此接口只有一个方法。可以参考标准库这几个接口所包含的方法及接口的命名:net.Conn, http.ResponseWriter, io.ReadWriter 。
Read, Write, Close, Flush, String 这几个方法在标准库里已经有约定俗成的含义和签名。自定义的接口方法应该要避免使用这几个名字,除非方法的行为确实和标准库这几个接口方法一致,这时候可以使用这些名字,但必须要确保方法的签名和标准库一致。序列化成字符串的方法命名成 String 而不是 ToString 。
错误
自定义错误类型以 Error 作为后缀,采用 XyzError 的格式命名:
Getter/Setter
struct 的首字母大写的字段是导出字段,可以直接读写不需要 Getter/Setter ,首字母小写的字段是私有字段,必要的情况下可以增加读写私有字段的 Getter/Setter 方法。私有字段首字母变大写即为 Getter 方法名字,不需要加 Get 前缀。私有字段首字母变大写加上 Set 前缀即为 Setter 方法名字。例如 struct 中名为 obj 的私有字段,其 Getter/Setter 方法命名分别为 Obj/SetObj 。
包
包名使用纯小写、能精确描述包功能且精炼的名词(有点难度),不带下划线,不引起迷惑的前提下可以用缩写,比如标准库的 strconv 。如果包名比较复杂出现了多个单词,就应该考虑是不是要分层了,参考标准库的 crypto/md5, net/http/cgi 等包。包名应该要和包所在目录名一致,比如标准库的 src/encoding/base64 目录下,源文件的包名为 base64 。避免以下命名:
和标准库同名
util, common 等太过笼统的名字
包路径
包路径的最底层路径名和包名一致:
禁止使用相对路径导入包:
项目代码布局
开发 golang 库时如何组织项目代码可以参考 golang 的标准库。开发应用程序和开发库在工程实践上还是有点不同。有一些开源项目把所有的代码都放在一个包里 (main) ,项目比较小时还能接受,项目比较大时就难以阅读了。golang 的项目代码布局目前业界也没有一个统一的标准。这篇文章讨论了几种布局方案缺陷,然后提出了一些建议。这篇文章在此基础上给出了一个可操作的方案,这也是本文推荐的方案。以下以 xauth 项目为例说明。
这种布局特别适合既有可执行程序又有库的复杂项目。主要规范是在项目根目录下建立 cmd 和 pkg 目录。cmd 目录下存放编译可执行文件的代码。通常一个复杂项目可能会有多个可执行程序,每个可执行程序的代码在 cmd 目录各建立目录存放。比如 git.yingzhongtong.com/combase/xauth/cmd/xauth 下是编译可执行文件 xauth 的源码。编译 xauth 需要使用的内部库直接在 git.yingzhongtong.com/combase/xauth/cmd/xauth 建立目录存放。多个可执行程序都需要用到的公共库应该放到项目根目录下的 pkg 目录里。根目录的 pkg 目录下每个目录都是一个单独的公共库。
建议项目根目录下放一个 Makefile 文件,方便一键编译出所有可执行程序。
总之,这种布局的主要思想是按功能模块划分库,区分私有库和公共库,分别放在不同层级别的目录里。使用这种布局编写代码时,通常可执行程序对应的 main 包一般只有一个 main.go 文件,而且这个文件通常代码很少,基本就是把需要用到的库拼到一起。 github 的这个项目提供了这种布局的模板,可以 clone 下来直接使用(有些文件需要适当调整下)。
github 上很多优秀的开源项目也是采用的这种布局,熟悉这种布局也能帮助你更好的阅读这些开源项目。
以上介绍的项目代码布局是开发大型项目时强烈建议的方案。如果是小型项目代码量很少,直接放在一个目录里也是可以接受的。
依赖管理
golang 早期版本中,依赖管理比较简单,依赖的第三方库通过 go get 下载到 GOPATH 中,编译时会根据 import 的路径去 GOPATH 和 GOROOT 中查找依赖的库。这种方式虽然简单,但是也有很多缺陷:
对依赖的第三方库没有版本管理,每次 go get 时都是下载最新的版本,最新的版本可能存在 bug;
基于域名的第三方库路径可能失效;
多个项目依赖共同的第三方库时,一个项目更新依赖库会影响其他项目。
golang 从 1.6 版本开始引入了 vendor 用来管理第三方库。vendor 是项目根目录下的一个特殊目录,go doc 会忽略这个目录。编译时会优先从 vendor 目录中查找依赖的第三方库,找不到时再去 GOPATH 和 GOROOT 中查找。
vendor 机制解决上述的第 2 个和第 3 个缺陷,因此强烈建议工程实践中将项目的第三方库(所有本项目之外的库,包括开源库及公司级的公共库)全部放到 vendor 中管理。使用这种方式, GOPATH 存在的意义基本很小了,这也是上文中提到 GOPATH 只需要设置 1 个目录或者干脆使用默认值的原因。
vendor 机制支持嵌套使用,即 vendor 中的第三方库中也可以有 vendor 目录,但这样做会导致更复杂的依赖链甚至循环依赖,而且目前也没有完美的解决方案。因此只有在开发可执行程序项目时才需要使用 vendor 。开发库时禁止使用 vendor 。
vendor 机制并没有解决上述的依赖库版本管理问题,并且目前官方也没有提供配套的工具。可以使用开源的第三方工具解决这个问题,推荐 glide 或 godep 。使用教程参考官方文档,这里就不赘述了。
使用 vendor 时要注意,项目中的 vendor 目录不要提交到代码仓库中,但是第三方工具生成的依赖库列表文件必须提交,比如 glide 生成的 glide.lock 和 glide.yaml 。
可执行程序版本管理
有时候生产环境跑的可执行程序可能有问题需要找到对应的源码进行定位。如果发布系统也没有把源码信息和可执行程序关联的话,可能根本找不到可执行程序是哪个版本的源码编译出来的。因此建议在可执行程序中嵌入版本和编译信息,程序启动时可以直接作为启动信息打印。
版本号建议采用通用的 3 级点分字符串形式: <大版本号>.<小版本号>.<补丁号>,比如 0.0.1 。简单的 2 级也可以。使用 git 的话可以把 git commit SHA (通过 git rev-parse --short HEAD 获取)作为 build id 。
性能剖析(profiling)
程序的性能通常和使用的范式、算法、语言特性有关。在性能敏感的场景下,需要使用性能剖析工具分析进程的瓶颈所在,进而针对性的优化。golang 自带了性能剖析工具 pprof ,可以方便的剖析 golang 程序的时间/空间运行性能,以下是从某项目中部分代码改编后的示例代码,用来说明 pprof 的使用。直观上似乎函数 bar 里有更多的计算,调用函数 bar 应该比调用函数 foo 占用更多的 CPU 时间,实际情况却并非如此。
后台程序一般是 HTTP 常驻服务(如果不是 HTTP 服务的话也可以直接在代码里启动一个),import 列表里加上 _ "net/http/pprof" 后,程序启动后 golang 运行时就会定时对进程运行状态采样,采样到的数据可能通过 HTTP 接口获取。还有一种方式是使用 "runtime/pprof" 包,在需要剖析的程序代码里插入启动采样代码将,采样数据写到本地文件用来分析,具体使用方式参考这里。原理和第一种方式一样,只是采样数据读取方式不一样。
启用运行时采样后,以下命令通过 HTTP 接口获取一段时间内(5 秒)的采样数据进行分析,然后进入命令行交互模式:
生成的性能剖析图如下:
从上图可以看出调用函数 foo 占用的 CPU 时间要远大于调用函数 bar 的(耗时占比越大,表示调用的箭头线段也越粗),并且在函数 foo 的耗时主要又耗在调用 runtime 的函数 duffzero 上。虽然这是 golang 的内置函数,但看名字基本上已经能猜到性能瓶颈出在哪里了,这样就可以进行有针对性的优化。这里不解释为什么调用函数 foo 占用的 CPU 时间会远大于调用函数 bar的,留给读者思考。
以上这个示例也说明了优化 CPU 性能关键是要找到影响整个系统的瓶颈,对于一个只占系统总耗时 1% 的函数,就算优化 10 倍意义也没什么意义。
大多数情况下 golang 后台应用性能剖析只需要优化 CPU 占用耗时就可以了。 golang 是自带垃圾回收(GC)的语言,由于 GC 的复杂性,和程序员自己管理内存的 C 语言相比,这类语言一般占用内存都比较大。自带 GC 语言很少会有内存泄露问题,不过也有一种特殊场景的内存泄漏:比如往一个全局的切片里不断 append 数据又不自行清理,这种一般是程序有逻辑错误引起的。pprof 也可以在运行时对对象占用内存进行分析
使用 -inuse_objects 选项可以把采样对象设成对象数目。内存采样数据是对象占用内存状况的实时快照,不需要像采样 CPU 性能数据那样要让进程跑一段时间。
这篇文章介绍了更多 golang 内存泄露的场景,有兴趣可以阅读下。
测试
golang 语言自带了测试工具和相关库,可以很方便的对 golang 程序进行测试。
推荐表驱动测试的方式进行单元测试,golang 标准库中也有很多例子。以下是一个表驱动测试的示例:
使用表驱动测试可以很方便的增加测试用例测试各种边界条件。这个工具可以很方便的生成表驱动测试的桩代码。
单元测试一般只需要对包中的导出函数进行测试,非导出函数作为内部实现,除非有比较复杂逻辑,一般不用测试。
这个视频(PPT)更详细介绍了 golang 测试的最佳实践,值得一看。
1、什么情况下设置runtime.GOMAXPROCS会比较好的提高速度呢?
GO默认是使用一个CPU核的,通过设置runtime.GOMAXPROCS可以设置使用多核,并不是核越多处理速度越快,要根据不同业务场景设置核数,比如:
适合设置多核场景:CPU密集型、并行度比较高的情景,比如多数组排序,复杂计算等。
不适合设置多核场景:IO密集型,读写文件、爬虫如果只是抓网页而不分析等场景。
2、sync.WaitGroup用法
WaitGroup在go语言中,用于线程同步,单从字面意思理解,wait等待的意思,group组、团队的意思,WaitGroup就是指等待一组,等待一个系列执行完成后才会继续向下执行。贴上google官方的代码:
从执行结果可看出:
1、取三个网址信息的时候,结果显示顺序与for循环的顺序没有必然关系。
2、三个goroutine全部执行完成后,wg.Wait()才停止等待,继续执行并打印出over字符
3、go语言的组合继承
这是Golang的组合模式,可以实现OOP的继承。 被组合的类型People所包含的方法虽然升级成了外部类型Teacher这个组合类型的方法(一定要是匿名字段),但它们的方法(ShowA())调用时接受者并没有发生变化。 此时People类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者Teacher类型的功能。
4、go语言select随机性
select 中只要有一个case能return,则立刻执行。
当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。
如果没有一个case能return则可以执行”default”块。
单个chan如果无缓冲时,将会阻塞
5、map线程安全
线程不安全时,可能会出现:可能会出现fatal error: concurrent map read and map write
以下为map不安全的例子:
修改后如下:
5、sync.RWMutex和sync.Mutex区别
golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能.
其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁叶叫做全局锁.
RWMutex是一个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景. func (rw *RWMutex) Lock() 写锁,如果在添加写锁之前已经有其他的读锁和写锁,则lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的 Lock 调用会从获得的锁中排除新的读取器,即写锁权限高于读锁,有写锁时优先进行写锁定.
6、type 用法
7、defer用法总结
总结私信‘资料'可MF领取相关资料,
defer 后面必须跟函数。defer在函数结束前执行,这种结束有可能是:函数正常执行完成后、函数遇到return并执行return后,函数遇到panic。执行顺序:函数内容–return–defer–panic- 免责声明
- 世链财经作为开放的信息发布平台,所有资讯仅代表作者个人观点,与世链财经无关。如文章、图片、音频或视频出现侵权、违规及其他不当言论,请提供相关材料,发送到:2785592653@qq.com。
- 风险提示:本站所提供的资讯不代表任何投资暗示。投资有风险,入市须谨慎。
- 世链粉丝群:提供最新热点新闻,空投糖果、红包等福利,微信:juu3644。