GO开发指南

本文最后更新于:2023年6月19日 晚上

记录在进行 go 语言开发遇到的各种问题。

命名规范

文件命名

文件命名一律采用小写,不用驼峰式,尽量见名思义,看见文件名就可以知道这个文件下的大概内容。
其中测试文件以test.go 结尾,除测试文件外,命名不出现
例子:

stringutil.go, stringutil_test.go

package

包名用小写,使用短命名,尽量和标准库不要冲突。
包名统一使用单数形式。

在 go 源文件的开头必须申明文件所属的 package,如下所示:

1
2
package name
......

命名规范

  • 建议 package 命名用小写字母
  • 建议 package 命名必和其路径的最后一段一致(main package 除外)。注意,这并不是 Golang 的强制要求,文件目录只是用于存放同一个 package 的所有源文件,Golang 对目录名并无要求。但一个目录下不允许有多个 package 的源文件
  • main package 中的 main 方法是可执行文件的入口,main package 名一般和路径名不一致
  • 不同路径下 package 命名可以重复,但其完整路径名必须唯一

变量

变量命名一般采用驼峰式,当遇到特有名词(缩写或简称,如 DNS)的时候,特有名词根据是否私有全部大写或小写。
例子:

apiClient、URLString

接口

单个函数的接口名以 er 为后缀

1
2
3
1. type Reader interface {
2. Read(p []byte) (n int, err error)
3. }

两个函数的接口名综合两个函数名,如:

1
2
3
4
1. type WriteFlusher interface {
2. Write([]byte) (int, error)
3. Flush() error
4. }

三个以上函数的接口名类似于结构体名,如:

1
2
3
4
5
1. type Car interface {
2. Start()
3. Stop()
4. Drive()
5. }

方法

方法名应该是动词或动词短语,采用驼峰式。将功能及必要的参数体现在名字中, 不要嫌长, 如 updateById,getUserInfo.
如果是结构体方法,那么 Receiver 的名称应该缩写,一般使用一个或者两个字符作为 Receiver 的名称。如果 Receiver 是指针, 那么统一使用 p。 如:

1
2
3
1. func (f foo) method() {
2. ...
3. }
1
2
3
1. func (p *foo) method() {
2. ...
3. }

对于 Receiver 命名应该统一, 要么都使用值, 要么都用指针。
每个以大写字母开头(即可以导出)的方法应该有注释,且以该函数名开头。如:

1
2
3
4
1. // Get 会响应对应路由转发过来的 get 请求.
2. func (c *Controller) Get() {
3. ...
4. }

大写字母开头的方法以为着是可供调用的公共方法,如果你的方法想只在本包内掉用,请以小写字母开发。如:

1
2
3
1. func (c *Controller) curl() {
2. ...
3. }

注释应该用一个完整的句子,注释的第一个单词应该是要注释的指示符,以便在 godoc 中容易查找。
注释应该以一个句点 . 结束。

各种报错

【1】“与你运行的 Windows 版本不兼容“的解决方法
https://blog.csdn.net/willingtolove/article/details/107924423
其实就是 package 名字错了,必须改成 main,不清楚为什么?
【2】每次创建新的项目都要重新设置一个 configuration 文件,后发现是我的问题,开发的时候,可以把所有的项目都在 goland 里打开,根目录就是$GOPATH,像这样:
image.png
所有小项目都放在 src 里,是不是很棒?
但是!必须有 main 包,必须有 main 函数作为程序入口!
具体原理看:
https://blog.csdn.net/zxy_666/article/details/80390843
如何写出优雅的 Golang 代码
Golang import 包问题相关详解
理解 Go 语言包(package)
【3】结构体属性导出问题
结构体中的属性,如果是小写开头,是无法被跨包使用的,也就是说包外无法访问,太狗了。
Go Struct 超详细讲解 建议好好看看这篇

函数知识

make

make()函数在 golang 的代码如下:
func make(t Type,size IntegerType) Type
使用 make 来创建 slice,map,chanel 说明如下:

1
2
3
4
5
6
7
8
9
10
11

var slice_ []int = make([]int,5,10)
fmt.Println(slice_)
var slice_1 []int = make([]int,5)
fmt.Println(slice_1)
var slice_2 []int = []int{1,2}
fmt.Println(slice_2)
打印结果:
[0 0 0 0 0]
[0 0 0 0 0]
[1,2]

在创建 slice 时第一个参数用于确定初始化该 slice 的大小该 slice 中的值为零值,第三个参数用于确定该 slice 的长度
map:

1
2
3
4
5
6
7
8
9
var m_ map[string]int = make(map[string]int)
m_["one"] = 1
fmt.Println(m_)
var m map[string]int = map[string]int{"1":1}
m["2"] = 2
fmt.Println(m)
打印结果:
map[one:1]
map[1:1 2:2]

根据 size 大小来初始化分配内存,不过分配后的 map 长度为 0,如果 size 被忽略了,那么会在初始化分配内存时分配一个小尺寸的内存

nil

相信写过 Golang 的程序员对下面一段代码是非常非常熟悉的了:

1
2
3
if err != nil {
// do something....
}

当出现不等于nil的时候,说明出现某些错误了,需要我们对这个错误进行一些处理,而如果等于nil说明运行正常。那什么是nil呢?查一下词典可以知道,nil的意思是无,或者是零值。零值,zero value,是不是有点熟悉?在 Go 语言中,如果你声明了一个变量但是没有对它进行赋值操作,那么这个变量就会有一个类型的默认零值。这是每种类型对应的零值:

1
2
3
4
5
6
7
8
9
bool      -> false
numbers -> 0
string -> ""
pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil

举个例子,当你定义了一个 struct:

1
2
3
4
5
6
type Person struct {
AgeYears int
Name string
Friends []Person
}
var p Person // Person{0, "", nil}

变量p只声明但没有赋值,所以 p 的所有字段都有对应的零值。那么,这个nil到底是什么呢?Go 的文档中说到,_nil 是预定义的标识符,代表指针、通道、函数、接口、映射或切片的零值_,也就是预定义好的一个变量:

slices

1
2
3
4
5
6
// nil slices
var s []slice
len(s) // 0
cap(s) // 0
for range s // iterates zero times
s[i] // panic: index out of range

一个为nil的 slice,除了不能索引外,其他的操作都是可以的,当你需要填充值的时候可以使用append函数,slice 会自动进行扩充。那么为nil的 slice 的底层结构是怎样的呢?根据官方的文档,slice 有三个元素,分别是长度、容量、指向数组的指针:
image.png
当有元素的时候,
image.png

channel

参考:https://colobu.com/2016/04/14/Golang-Channels/

Channel 是 Go 中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。
它的操作符是箭头  <- 。

1
2
ch <- v    // 发送值v到Channel ch中
v := <-ch // 从Channel ch中接收数据,并将数据赋值给v

(箭头的指向就是数据的流向)
就像 map 和 slice 数据类型一样, channel 必须先创建再使用:

1
ch := make(chan int)

定义与用法

它包括三种类型的定义。可选的<-代表 channel 的方向。如果没有指定方向,那么 Channel 就是双向的,既可以接收数据,也可以发送数据。

1
2
3
chan T          // 可以接收和发送类型为 T 的数据
chan<- float64 // 只可以用来发送 float64 类型的数据
<-chan int // 只可以用来接收 int 类型的数据

使用make初始化 Channel,并且可以设置容量:

1
make(chan int, 100)

容量(capacity)代表 Channel 容纳的最多的元素的数量,代表 Channel 的缓存的大小。
如果没有设置容量,或者容量设置为 0, 说明 Channel 没有缓存,只有 sender 和 receiver 都准备好了后它们的通讯(communication)才会发生(Blocking)。如果设置了缓存,就有可能不发生阻塞, 只有 buffer 满了后 send 才会阻塞, 而只有缓存空了后 receive 才会阻塞。一个 nil channel 不会通信。
可以通过内建的close方法可以关闭 Channel。
可以在多个 goroutine 从/往 一个 channel 中 receive/send 数据, 不必考虑额外的同步措施
Channel 可以作为一个先入先出(FIFO)的队列,接收的数据和发送的数据的顺序是一致的。
channel 的 receive 支持  multi-valued assignment,如

1
v, ok := <-ch

它可以用来检查 Channel 是否已经被关闭了。

send

send 语句用来往 Channel 中发送数据, 如ch <- 3
它的定义如下:

1
2
SendStmt = Channel "<-" Expression .
Channel = Expression .

在通讯(communication)开始前 channel 和 expression 必选先求值出来(evaluated),比如下面的(3+4)先计算出 7 然后再发送给 channel。

1
2
3
4
5
c := make(chan int)
defer close(c)
go func() { c <- 3 + 4 }()
i := <-c
fmt.Println(i)

send 被执行前(proceed)通讯(communication)一直被阻塞着。如前所言,无缓存的 channel 只有在 receiver 准备好后 send 才被执行。如果有缓存,并且缓存未满,则 send 会被执行。
往一个已经被 close 的 channel 中继续发送数据会导致run-time panic
往 nil channel 中发送数据会一致被阻塞着。

receive

<-ch用来从 channel ch 中接收数据,这个表达式会一直被 block,直到有数据可以接收。
从一个 nil channel 中接收数据会一直被 block。
从一个被 close 的 channel 中接收数据不会被阻塞,而是立即返回,接收完已发送的数据后会返回元素类型的零值(zero value)。
如前所述,你可以使用一个额外的返回参数来检查 channel 是否关闭。

阻塞

缺省情况下,发送和接收会一直阻塞着,直到另一方准备好。这种方式可以用来在 gororutine 中进行同步,而不必使用显示的锁或者条件变量。
如官方的例子中x, y := <-c, <-c这句会一直等待计算结果发送到 channel 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}

Range

for …… range语句可以处理 Channel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
go func() {
time.Sleep(1 * time.Hour)
}()
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}

range c产生的迭代值为 Channel 中发送的值,它会一直迭代直到 channel 被关闭。上面的例子中如果把close(c)注释掉,程序会一直阻塞在for …… range那一行。

defer

https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.4.html
这个博客讲的挺好的,其他知识也有,可以常看看。
另外还有:https://sanyuesha.com/2017/07/23/go-defer/

其他知识

goroutine

https://www.cnblogs.com/wdliu/p/9272220.html 调度原理
代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"time"
)

func cal(a int , b int ) {
c := a+b
fmt.Printf("%d + %d = %d\n",a,b,c)
}

func main() {  
for i :=0 ; i<10 ;i++{
go cal(i,i+1) //启动10个goroutine 来计算
}
time.Sleep(time.Second * 2) // sleep作用是为了等待所有任务完成
}
//结果
//8 + 9 = 17
//9 + 10 = 19
//4 + 5 = 9
//5 + 6 = 11
//0 + 1 = 1
//1 + 2 = 3
//2 + 3 = 5
//3 + 4 = 7
//7 + 8 = 15
//6 + 7 = 13

goroutine 异常捕捉

当启动多个 goroutine 时,如果其中一个 goroutine 异常了,并且我们并没有对进行异常处理,那么整个程序都会终止,所以我们在编写程序时候最好每个 goroutine 所运行的函数都做异常处理,异常处理采用 recover.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"time"
)

func addele(a []int ,i int) {
defer func() { //匿名函数捕获错误
err := recover()
if err != nil {
fmt.Println("add ele fail")
}
}()
a[i]=i
fmt.Println(a)
}

func main() {
Arry := make([]int,4)
for i :=0 ; i<10 ;i++{
go addele(Arry,i)
}
time.Sleep(time.Second * 2)
}
//结果
add ele fail
[0 0 0 0]
[0 1 0 0]
[0 1 2 0]
[0 1 2 3]
add ele fail
add ele fail
add ele fail
add ele fail
add ele fail

方法

Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:

1
2
3
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

/* 定义结构体 */
type Circle struct {
radius float64
}

func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}

函数传参

数组和切片很像,但是在调用的时候他们传递的类型是不同的!

  • Go 语言的数组是值,其长度是其类型的一部分,作为函数参数时,是 值传递,函数中的修改对调用者不可见
  • Go 语言中对数组的处理,一般采用 切片 的方式,切片包含对底层数组内容的引用,作为函数参数时,类似于 指针传递,函数中的修改对调用者可见。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 数组
b := [...]int{2, 3, 5, 7, 11, 13}
func boo(tt [6]int) {
tt[0], tt[len(tt)-1] = tt[len(tt)-1], tt[0]
}
boo(b)
fmt.Println(b) // [2 3 5 7 11 13]
// 切片
p := []int{2, 3, 5, 7, 11, 13}
func poo(tt []int) {
tt[0], tt[len(tt)-1] = tt[len(tt)-1], tt[0]
}
poo(p)
fmt.Println(p) // [13 3 5 7 11 2]

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!