GoGuide
Go基础
Hello Work!
新建hello.go文件,键入以下内容:
package main
import "fmt"
func main() {
fmt.Println("Hello Work!")
}在文件的根目录下执行:
> go build hello.gogo命令会根据操作系统在同一级目录生成一个可执行文件hello.exe。执行该文件:
> hello.exe
Hello Work!Go语言中,所有的代码都必须组织在package中,package由同一目录下的多个源码⽂件组成,每个package作为被其他项目引用的基本单元。
引入package使用import关键字。如上,我们引入了标准库中的fmt包,并在main方法中调用了其方法来输出字符串。
go build命令可以指定文件根据操作系统生成对应的可执行文件,文件必须包含在名为main的包中,同时提供名为main的入口函数,如hello.go
类型
变量
Go是静态语言,变量类型必须在编译时就确定。声明变量和常用类型如下:
基本类型:
bool
false
byte
0
rune
0
int,uint
0
整型
float
0.0
浮点型
complex
复数
uintptr
存放指针的整数
array
数组,值类型
struct
值类型
string
""
UTF-字符串
slice
nil
引⽤类型
map
nil
引⽤类型
channel
nil
引⽤类型
interface
nil
接口
function
nil
函数
常量
常量值必须是编译期可确定的数字、字符串、布尔值。
Go还提供了关键字iota来方便的创建枚举,iota第一次使用时为0,之后的值为与第一次使用所在行的差数:
以下是Go标准库中的代码:
自定义类型
type关键字可用与在全局或者函数内定义新的类型:
bool、int、string等类型属于命名类型,而array、 slice、map 等和具体元素类型、⻓度等有关,属于未命名类型。
对于相同类型的未命名类型可以进行隐式转换
arr、slice、map、指针等未命名类型,其对应的基类型、元素、键值相同的,将被视为同一类型。对于channel则还需要有相同的传送方向,才会被视为同一类型。此外以下类型也会被认为是同一类型:
具有相同字段序列 (字段名、类型、标签、顺序) 的匿名 struct
签名相同 (参数和返回值 的 function
⽅法集相同 (⽅法名、⽅法签名) 的 interface
表达式
if
Go语言没有三元运算符
for
range
range用于迭代数据,支持array、slice、map、channel:
需要注意的是,range的迭代会使用复制值,对迭代值的修改不会改变原始数据:
switch
流程跳转
Go语言的流程中可以通过continue和break来控制循环,break 可⽤于 for、switch、select,⽽continue 仅能⽤于 for 循环。
此外Go还提供了标签的方式来进行流程控制:
函数
Go语言中的函数支持
函数参数
多返回参数,命名返回参数
可变参数
匿名函数和闭包
Go语言不支持:
重载
默认参数
嵌套(即函数中定义函数)
函数参数
可以将复杂的函数签名(函数名、参数以及返回值)定义为类型:
可变参数
Go语言中的可变参数必须定义在最后,且只能有一个,其本质上是一个slice,并且slice作为可变参数传入时,必须使用...展开:
延迟调用
defer可以延迟调用函数,被调用的函数将会在return之后函数实际返回之前被调用:
值得注意的是,defer调用按照FILO的顺序执行,且如果其中有函数报错,其他的函数依旧会执行,可被后续延迟调⽤捕获,但仅最后⼀个错误可被捕获
错误处理
Go语言中抛出错误使用panic,抓取错误使用recover,其函数签名分别接收和返回一个interface类型,也就是说,可以抛出或抓取任意类型:
panic的抛出会中断程序执行,更常见的作法是使用标准库来返回错误,并对返回的错误判断并处理:
数据
Array
Go中的数组属于值类型,可以直接使用==和!=来比较,且数组长度必须在定义的时候确定,相同长度,相同类型的数组才被视为同一类型。数组的定义方式如下:
同时Go支持多维数组、指针数组[]*int和数组指针*[]int
值得注意的是,数组参数在函数中的传递,采用的是值传递的方式,如果只是读取数据,或需要修改数组的话,可以采用传递数组指针的方式。
Slice
slice(切片)更接近于其他语言中的数组概念,是一个结构体。其内部包含一个数组指针,同时包含长度变量len,和容量变量cap:
len表示切片存储的元素个数cap表示切片所能存储的最大元素个数
切片的定义:
切片还可以通过截取数组来定义,其长度等于截取数组的元素个数,容量等于截取位置到数组最后一个元素的个数:
切片还可以通过截取切片来获得:
值得注意的是,从数组或切片获取的切片,其内部的数组指针指向的是原数组,对数组的修改将会改变切片的值:
当切片的容量已满时,如果还需要添加元素,可以使用append方法。其会创建一个两倍于原底层数组的数组,并指向该数组。在原切片容量大于1000时,扩容将会被缩小至1.25倍:
值得注意的是:定义一个空切片和nil切片是不一样的,所有的空切片其数组指针会指向同一个内存地址:
参考资料: 深度解析 Go 语言中「切片」的三种特殊状态
copy可以将一个切片复制给另一个切片:
切片和数组的遍历均可以使用一个或者两个参数接收:
每次遍历的value会由一个单独的内存空间来存储,所以如果对value取地址都将会获得同一个地址:
所以如果需要更改原切片中的值需要采用以下方式:
Map
key必须是支持==和!=的类型,value可以是任意类型:
需要注意的是没有分配内存的map是不能存取键值对的:
此外通常情况下为了避免map频繁的扩容,应该使用make提前预估map长度来创建:
当插入的键值对数量大于make分配的长度时,并不会出现类似数组下标越界的情况,而是会自动扩容:
需要注意的是,使用for语句遍历map的时候,不能保证迭代的返回次序。
此外,遍历的是复制值,如果需要替换value的话,可以通过以下方式,或者让value保存变量指针:
当用两个参数来接收map的值时,可以判断key是否存在:
Struct
使用type可以自定义一个值类型的结构体类型:
也支持匿名结构体:
初始化:
Go不支持继承,可以通过结构体嵌套达到类似效果:
结构体还可以定义标签,在使用对应的读取时会根据对应标签的名称来获取或者设置值:
值得注意的是,空的struct不占用任何内存空间,可以用于实现只有方法的"静态类"或者set数据结构:
方法
方法定义
Go语言里面方法和函数非常相似,但在定义声明上方法比函数多了一个接收者。
假设我们有结构体Cat,一个完整的方法定义应该如下:
其表示给结构体Cat的实例定义一个可接收string类型的SayHello方法,并返回bool类型
可以看到,在方法中可以是使用名为cat的参数来获取接收者的属性
此外,以上的接收者被定义为值类型,我们也可以定义为指针类型(cat *Cat)
在Cat类型的实例调用Rename()方法之后,其name,也将被改变
需要注意的是:
只能为当前包内命名类型定义⽅法
参数
receiver,不能是接⼝或指针receiver在方法中未被使用,可以省略参数名
方法集
方法根据调用者不同可以分为两种表现形式:
可以通过一下的例子来感受一下:
以下对于WhoAmI()和Rename()两种写法其实是等价的:
这里值得注意的是,cat作为Cat类型的值依然可以调用到作为指针接收的(cat *Cat) Rename方法。这是因为go在编译的时候会为我们做隐式转换,实际上cat.Rename("Jerry")相当于(&cat).Rename("Jerry")。 但是如果尝试使用以下两种方法调用,则编译会不通过:
这就涉及到方法集的概念,方法集决定了可以在该类型上操作的方法。每个类型都有与之关联的⽅法集,比如上面例子中的WhoAmI()就是定义在Cat类型的方法集中的一个方法。
定义同一个receiver上的方法,可能属于多个方法集:
类型 T ⽅法集包含全部 receiver T ⽅法。
类型 *T ⽅法集包含全部 receiver T + *T ⽅法。
如类型 S 包含匿名字段 T,则 S ⽅法集包含 T ⽅法。
如类型 S 包含匿名字段 *T,则 S ⽅法集包含 T + *T ⽅法。
不管嵌⼊ T 或 *T,S ⽅法集总是包含 T + *T ⽅法。
接口
接口定义
Go中的接口是一个或多个方法签名的集合:
切类型实现接口无需显示添加声明,只需要实现该接口的所有方法即可:
接⼝对象由接⼝表Itab指针和数据指针data组成,其中接口表包括括接⼝类型、动态类型,以及实现接⼝的⽅法指针,数据指针持有的则是目标对象的只读复制:
接口还可以作为变量类型,或结构成员:
此外Go中接口还有以下特点:
可在接⼝中嵌⼊其他接⼝
类型可实现多个接⼝
超集接⼝对象可转换为⼦集接⼝,反之则报错
接口转换
空接口interface{}没有任何方法签名,即所有类型都实现了空接口。所以可以使用空接口来传递任意返回值以及返回值。
此时需要获取Cat的name属性则需要将参数转换成原有类型:
同时,接⼝转型返回临时对象时,只有使⽤指针才能修改其状态:
利⽤类型推断,可判断接⼝对象是否某个具体的接⼝或类型:
实现接口
在实践中我们在使用接口的过程中,通常都会遇到接口增加方法的时候,这样就需要在接口的实现中添加对应的方法。由于 Go 语言的特性,并没有强制要求指定结构体对接口的实现。所有我们需要明确找到借口实现,添加接口方法。如果是使用的 Goland 编辑器,有一个小技巧,帮助我们快速实现添加的方法。 比如我们有一个结构体,需要实现io.Reader接口。我们可以添加如下的冗余代码:
这样 Goland 会提示我们r没有实现io.Reader中的方法,我们可以借此快速添加缺失的方法。
内存布局
Number

string

struct

slice

interface

new

make

反射
当一个对象转换成接口时,会在该接口的Itab中存储与该类型相关的信息,标准库中的reflect包则是根据这些信息来操作对象的
Type
假设有如下结构体:
Type是reflect包中的一个接口,用于表示Go中的类型。通过reflect.TypeOf()方法可以返回对象的类型(reflect.Type):
通过reflect.Type可以获取到对象的各字段:
字段上自定义的标签可以通过tag名来获取对应的值:
指针本身是没有任何字段的,所以反射指针对象时,需要通过Elem()将其转换成基本类型:
当然也可以通过基本类型生成复合类型:
在创建结构体时,内存分配可能会被Go优化,我们可以通过reflect中的Size()、Align等方法来查看结构体在内存中的对其信息:
FildAlign()可以查看字段在结构体中的对齐信息:
此外reflect.Type还提供了Implements()来判断是否实现了某接口、AssignableTo()用于赋值、ConvertibleTo()用于转换判断
Value
reflect包中提供的Value相关的方法跟Type相关的方法相当类似,只不过改成了获取字段的值:
如果未找到值会返回一个invalid Value
对于导出字段(首字母大写的字段)可以使用Interface()获取值
复合类型值获取:
此外Kind()方法可以获取reflect.Value的类型,IsNil()可以判断reflect.Value对象是否为空,需要注意的是,基本类型拥有默认值均不为空,所以不适用于该方法
ValueOf() 会返回一个新的实例,且该实例只读,但是如果这是一个指针的实例,且是导出字段则可以对指向的目标进行修改:
如果是非导出字段,则可以获取其地址改变其值:
复合类型值修改:
Method
通过反射还可以获取方法的入参,返回值等信息:
输出:
不仅如此,通过方法类型的reflect.Value还可以直接调用方法:
方法中的可变参数可以通过reflect.Value类型的slice传入,并通过CallSlice()方法调用:
Make
reflect包提供的一系列Make方法可以动态创建各引用类型的数据结构,甚至包括func。这里以动态创建func为例子,MakeSlice、MakeMap()、MakeChan()等方法可以自行查看reflce中的源码:
IO
经常遇到io相关的问题就有点摸不着头脑,故此将IO单独整理写到基础里面以供查阅。 这里讨论的IO并非操作系统对文件的读写操作。虽然Go中的ioutil包曾利用os包封装了API,直接对文件进行操作,但是自Go 1.16之后已经废弃了这些方法。所以这里讨论的io更多的是io包提供的接口和接口方法。
IO最主要的就是理解三个东西:Reader、Writer和[]byte。 几乎所有通用的包的Reader、Writer,都是为了实现io.Reader和io.Writer接口,以对外提供方法。并且对应的struct底层都会对应一个[]byte。 而Reader的Read()方法,和Writer的Write()都是相对于这个底层[]byte来说的。即分别从这个[]byte读取数据和向这个[]byte写入数据。我们可以看一下Reader读取数据的例子:
关于Writer的例子会稍微难理解一点:
在这里buf才是重点。我们通过bytes包提供的方法根据一个空的[]byte生成一个Buffer类型的变量buf。但是从输出的结果可以看到,这里传入的[]byte并不是Write()方法操作的byte数组。如果进入该方法的源码可以看到,这里的b仅仅作为buf的初始内容,buf中单独维护了一个可变长度的[]byte。
从Writer的例子可以看到,通常Write并不是向调用者所提供的[]byte中写入数据,而是向该Writer内部提供的[]byte写入数据。甚至更常见的情况是,直接进行系统调用,向文件写入数据:
频繁的写入少量数据,会导致频繁的访问本地磁盘的文件,造成大量的开销。bufio包提供了中间的缓冲,当缓冲区数据装满时才会调用创建时提供的Writer进行写数据:
我们多次写入的数据并没有装满缓冲区,但最后通过Flush()方法,仍然可以将数据强制写出,这里则是进行系统调用,将数据写入文件。
测试
之所以将测试单独写一章,就是为了表明测试对一个项目的重要性。
单元测试
Test
单元测试是用来测试一部分代码的函数。单元测试的是确认目标在给定的场景下有没有按照预期工作。 根据测试文件需要按照以下约定:
测试文件的文件名必须以
_test结尾。测试函数必须以
Test开头,并且必须接收一个testing.T类型的指针,且无返回值。
我们先再编写一个计算Fibnacci数列和的函数,然后再编写测试:
然后在同一个包下创建测试文件fib_test.go
然后在文件同级目录下,运行命令go test -v:
如果不添加-v参数,将不会输出t.Log()中的内容。 让我们修改一下函数的内容,使得返回一个错误的结果,在运行测试:
如果执行t.Fatalf()将会直接停止当前测试函数的运行,如果希望抛出错误并且继续运行函数可以使用t.Errorf()
go在运行测试方法时,各个包之间是相互隔离并且并行的(当然这取决于你设置的运行cpu数量)。同一个包下的测试会按照文件名顺序执行。同一个文件中的多个测试会按照从上到下的顺序执行。 t.Run()可以让我们在同一个Test方法中,顺序执行多次,并可以设置名称:
通过简单的修改函数,我们还可以同时对一组结果进行测试:
为了避免不必要的重复执行,一个成功的单元测试将会被缓存,直到当前测试包下的内容有修改。 即使某个方法存在类似time.Now()这种动态生成的变量的时候,比如测试方法包含time.Now().Unix() < 1668152900这样的判断,并通过的情况下,即使该判断存在二义性,因为测试通过了就会被缓存。所以应该避免在单元测试中使用不确定的参数。
在没有指定文件的情况下go test会尝试加载执行目录下的所有go文件。如果只想加载部分文件,可以在命令最后通过文件名指定:
TestMain
如果测试文件中包含TestMain(m *testing.M),那么生成的测试将调用 TestMain(m *testing.M),而不是直接运行测试。我们可以将此利用,在TestMain(m *testing.M)做一些共有的操作:
testflag
可以通过go help test和go help testflag查看更多的参数
-v
打印详细的测试输出
-run
-run参数,后面赋值对应需要执行的测试方法, 并且-run支持赋值正则表达式,比如之前的例子:
-args
go test允许我们传入自定义参数,一下两种方式都可以被接收:
基准测试
基准测试用于测试代码性能。与单元测试一样,基准测试的文件名也必须以_test.go结尾。且测试函数必须以Benchmark开头,并且必须接收一个testing.B类型的指针,且无返回值。再次测试Fib():
进行基准需要添加-bench="Bench*"。为了避免运行前面的单元测试,我们添加选项-run="none"
我们可以看到程序在1.316s内运行了57981378次
还可以通过-benchtime选项来设置最短运行时间或运行次数:
此外添加-benchmem可以打印出每次操作分配的内存,以及分配内存的次数:
由于Fib()函数直接在栈上调用,所有内存分配为0 值得注意的是,Benchmark 测试会现执行一次,然后再从i=0依次递增进行处理,比如下面这段代码的输出,为0 0 1 2 3 而非0 1 2 3 :
Mock
依赖注入
有些情况下我们的方法会依赖一些外部调用,比如数据库,HTTP请求等,这时候的单元测试就会形成对外部调用返回结果的依赖。我们可以先看一下下面的这个例子:
*A.greet()方法会依赖函数Bar()的返回值来判断,而Bar()的返回值是随机的,所以在执行测试函数TestGreet()的时候不是幂等的。 我们可以对代码进行适当的修改以达到幂等的目的。比如将函数Bar()以参数方式传入:
如此一来我们变解决了对函数Bar()的依赖。而实际上我们更常见的情况是成员方式的调用,比如:
这是在很多项目中都很常见的一种情况,Service 直接将 Model 作为成员变量。这样就会又会遇到我们刚刚的问题,A 对 B 的依赖导致不太好编写单元测试。 解决这一问题的核心就是解开 A 和 B 的耦合,解耦利器接口可以帮助我们对代码进行简单修改:
可以看到,通过Inf我们将*A.greet()方法的逻辑抽离了出来。然后利用对Inf接口的不同实现,我们可以控制*A.greet()方法的执行逻辑。这种方式也被称作为依赖注入(dependency injection,缩写为DI)。
目前有一些成熟的工具来帮助我们生成实现接口的方法,只需要我们自己设置入参和返回值,比如GoMock。首先我们安装工具:
然后指定需要实现的接口作为数据源,并设置报名,然后生成文件:
执行命令后GoMock会为我们在mock_info.go中生成一个或多个结构体来实现mock_test.go中的接口。 然后我们可以直接在单元测试中使用:
如果接口的方法需要入参,也可以基于入参数来设置返回值:
我们还也自己实现gomock提供的Matcher来做一些判断,比如有些情况入参可能会有很多参数,但实际上我们只关心其中一个:
HTTP
HTTP 请求的依赖在编程实践中也是比较常见的。下面是http.CLient的结构:
其中最主要的就是RoundTripper接口,它提供了基本的 HTTP 服务:
简单来说,RoundTripper需要实现一个RoundTrip方法,接收一个*http.Request返回一个*http.Response。借此我们可以实现一个自己的http.Client来提供 Mock:
当然我们可以在MockRoundTrip提供更多的 case,或者传入 case 来处理更多不同的情况。
测试覆盖率
通过go test -cover可以查看测试的覆盖率:
默认情况下go test只会运行当前目录下的测试文件,可以在最后指定其他目录:
通过以下命令可以将覆盖率信息保存到cover.out文件,然后再通过cover工具,可以查看当前包下各函数的覆盖率:
通过-html选项会直接浏览器中打开测试内容的覆盖情况:
Example
除了标准的测试以外,还可以利用go doc支持的Example来进行测试:
更多关于Example的内容可见doc的章节
环境配置
Windows环境和MacOS,Linux的设置环境变量稍有区别:
查看环境变量:
通常我们会将环境写入命令解释器的配置文件,比如zsh的配置文件在~/.zshrc。
环境变量
GOROOT 用于指定Go的安装路径。通常指定为/usr/local/go
GOPATH 指定项目的工作空间,在mod引入之前通过GOPATH指定的路径来管理一个项目,通常包含三个目录:
src保存源码pkg保存引入的包,通过go get下载的包会被放在这个文件夹下bin保存内建的可执行文件
GOBIN 指定通过go install命令安装的应用存放的目录。通常设置为GOPATH/bin
GOPRIVATE 用来控制go命令执行时的识别指定的私有仓库,私有仓库将会跳过proxy server和检验检查。 可以通过逗号分隔开来填写多个值。
GOPROXY 从Go1.13开始,GOPROXY随着go module引入,用来控制go module的下载源。GOPROXY用于修改下载go相关数据的代理:
GOSUMDB 我们先试想一种情况:某项目的v1版本被很多其他很多项目引入,这个时候该项目在v1版本进行了新的修改,这样所有引入了该项目的其他项目在重新拉取依赖时,即使没有做过任何修改也会出现和原来不一致的情况。 为了解决以上问题, Go 加入了go.sum对下载的依赖进行校验。通过对 mod 的版本及所有文件进行 Hash 计算会得到一个校验值,一个公开 mod 的这个校验值都会存在 Checksum 数据库中,这个服务由 sum.golang.org 提供。当我们通过 go 命令下载 mod 的时候,也会根据下载的版本及下载的所有文件去计算这个 mod 的校验值,并到 sum.golang.org 去校验 mod 内容是否有被篡改,从而保证 mod 的安全性。所以 Go 公开的 mod 即使很小的改动,都需要重新打 tag。
所以从Go1.13开始,GOSUMDB随着go module引入可以用来配置使用哪个校验服务器来做依赖包的校验:
也可以通过GONOSUMDB指定那些包不需要校验。
版本切换
如果需要安装或者切换go版本,我们可以先到官网下载系统对应的压缩包。比如我这里下载的是go1.18.2.darwin-amd64.tar.gz`。然后将原来的go版本直接改名:
然后将压缩包解压到/usr/local,因为这里我$GOROOT的地址是/usr/local/go.
可以看到已经安装成功
工具
构建
go build
Go提供的命令行工具build可包涵main函数的文件统计目录生成可执行文件。
修改环境变量可以生成不同平台的可执行程序:
默认情况下编译器只会加载main.go文件,其他文件需要手动添加。-o参数可以指定生成的文件路径和文件名,其后的第一个用于指定生成的可执行文件名称:
如果直接通过文件构建,可以生成同名的可执行文件:
甚至可以直接通过远程的 mod 来直接构建可执行文件:
此外添加-x和-v参数可以输出构建时每一步执行的操作。
go1.18版本之后,在执行go build时会将版本信息嵌入到二进制文件当中。我们可以通过go version -m [binaries] 来查看具体信息:
go install
install和build很相似,区别在于其构建的可执行文件会直接放在$GOPATH/bin目录下,可被全局调用。
go run
相较于build, run命令也会编译源码,并执行源码的main()函数,但是不会在当前目录留下可执行文件。并且也可以直接运行远程的文件。
go clean
clean可以用于清理build命令生成的文件,添加-i选项,可以将install命令在$GOPATH/bin目录下生成的文件一并删除。
-x参数可以打印出执行的命令:
构建约束
在 go 编译时,我们可以设置一些条件来指定满足条件的文件才被编译,不满足条件的则舍去。目前支持的构建约束方式有两种:通过文件名后缀,以及在文件中添加编译标签(build tag)。
文件名后缀 文件名后缀需要满足以下格式:
比如 Go 源码中的 os 包中的 Linux,Windows 实现:
build tag 通过在文件顶部添加注释来设置构建条件,目前有两种写法:
在tag中可以指定一下内容:
操作系统,即环境变量中GOOS的值;
操作系统的架构,即环境变量中GOARCH的值
使用的编译器,比如 gc 或者 gccgo;
其他自定义标签;
枚举值
ignore,指定该文件不参与编译;
对于自定义的标签,可以在go build的时候,添加-tags参数来设置筛选:
vet
Go 中自带的静态分析工具,用来检查一些代码中的错误。
mod
mod是1.11版本引入的官方包管理工具,相较于之前的GOPATH的引入方式,更具简洁。 Gomod引入依赖的方式很简单,首先需要使用go mod init进入到项目根目录对项目进行初始化:
初始化完成后,会在目录下生成一个go.mod的文件来记录依赖。 此时需要再引入外部工具包的话,可以直接在go.mod的同级目录执行:
go get会下载并缓存最新的版本,也可以指定特定的版本。如果引入的包较多,还可以直接更改go.mod文件,然后运行go mod tidy,来自动下载依赖。以下是一个修改过的简单示例:
其中包含了被注释了// indirect的间接引入的包。 如果引入的包符合gomod的命名规范,将会附带版本号,如gopkg.in/yaml.v2 v2.4.0。 否则就会自动生成一个版本号其中包含提交时间和提交的hash值,如:golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect **值得注意的是:**修改go.mod文件然后执行go mod tidy更新依赖的方式如果遇到存在同一个间接引入的依赖需要不同版本时会抛出错误。这时候可以通过go get来更新间接引入的依赖。
引用分支
mod还可以指定引入依赖的分支,如上引入的github.com/xuri/excelize/v2 v2.4.2-0.20211201164820-4ca1b305fe5b包,则是指定了分支的结果。在执行go mod tidy之前,go.mod文件的该行内容为:
引用本地包
通过在go.mod中指定replace可以将某依赖,替换为另外的依赖,以实现引入本地已经通过go mod init包:
常用的关于mod的命令:
go work
go 自1.18版本后引入了workspace的概念,可以简单理解为是将原来go.mod中需要临时替换本地库的replace找了一个新家。比如,之前我们需要临时使用本地包,需要修改go.mod:
这样会导致对go.mod的反复修改,workspace通过在go.work文件中编写原来的replace语句以此来保证go.mod的正确性。我们仍然可以不对go.mod进行任何改变:
然后通过在go.mod文件的同级目录执行go work init 来创建一个workspace,这条命令会自动为我们创建一个go.work文件,我们需要为我们的workspace指定包含的包。(go 在1.15以后使用go.mod来表示一个包)我们可以直接执行go use .来指定这个workspace包含当前路径下的go.mod文件。
然后指定golang.org/x/example替换为我们本地的包:
这样就可以使用本地的包进行调试了。
在这个示例里面如果./example/go.mod中的module名就是golang.org/x/example,我们也可以直接使用go work use ./exmple命令来指定,执行后go.work文件的内容如下:
当我们不需要再使用本地的包时,直接删除掉go.work的文件,一切就恢复了原状。
generate
go generate默认会扫描当前目录包下添加了//go:generate注释的文件,然后运行设置的命令。这通常和一些工具配合生成一些代码或者相关文件。比如我们有一下文件:
我们在同级目录执行:
将会生成1.txt及2.txt两个文件。当然我们也可以用以下方式指定目录下的所有文件:
godoc
godoc也是官方提供的工具之一,可以根据项目生成对应的文档。使用之前需要先安装这个工具:
然后通过如下命令即可启动一个http的文档服务,然后访问localhost:6060即可:
关于生成的文档又一些规则,比如
在当前目录下任意go文件的
package上面一行写的注释会被生成为Overview。如果在多个文件中都写了包注释,则会有冲突。通常会专门用一个名为doc.go的文件来写关于这个项目的Overview包下的函数,结构体等,都会被生成到
Index中如果为函数或者结构体编写了
example函数,则会在文档对应的函数或者结构体中也生成Example
Example
编写Example函数的文件名需要以_test.go为后缀,并且包名为{PackageName}_test。需要作为例子展示的函数需要以Example为函数名前缀。下面是一个简单的例子:
这个Example函数将会被生成到mydoc到Version函数下。并且这个函数是可以被测试的:
测试的结果是将这里结尾的Output:以下的注释内容与ExampleVersion()这个函数执行时的输出进行比较。如果完全一致才会通过测试。 除此之外还可以为结构体的方法添加Example,名称需要遵循格式:Example{StructName}_{MethodName}:
golangci-lint
golangci-lint 集成了各种go语言的静态检测工具,包括语法检测(lint),注释风格(style),错误处理(bugs),圈复杂度(cyclop)等各种工具。可以通过以下命令安装:
安装后可以通过golangci-lint linters 查看开启配置的检查器。运行目录下文件名为.golangci.yml的文件可以被读取为配置。配置内容可以参考这里。
Web Go Playground
go官网提供了运行环境,且可以生成链接用于分享代码片段。
项目结构示例
小型项目:
大型项目:
最后更新于
这有帮助吗?