1. 格式化 Formatting
格式化是最有争议但是最不重要的问题。在Go语言里,采用gofmt
来格式化程序,例如以下代码:
type T struct {
name string // name of the object
value int // its value
}
调用gofmt
后的格式为
type T struct {
name string // name of the object
value int // its value
}
这些都可以通过IDE的快捷键来格式化,另外还有一些格式化的简短细节:
- 缩进:使用
tab
进行缩进 - 行的长度:无限制,为了方便观看的话也可以自行拆行
- 括号:需要的括号较少,而且因为操作符的优先级很方便,也可以用空格代替括号。例如代码
x<<8 + y<<16
,含义是"x左移8位的值"加上"y左移16位的值",使用空格来表明运算顺序。
2. 代码注释 Commentary
Go语言提供了类似C语言风格的块注释/**/
和C++语言风格的行注释//
。行注释是常见的注释方式;块注释主要出现在包注释里,但是在表达式内或禁用大量代码时也会使用。
3. 命名 Names
在Go语言里,命名与其他语言一样重要,甚至命名还具有语义效应:一个名称在包外的可见性取决于首字符是否大写。
一些常见的命名约定如下:
- 使用驼峰式命名(CamelCase)来命名函数、变量和类型,即将每个单词的首字母大写并将它们连接在一起,例如MyVariable、MyFunction、MyStruct等。
- 对于只在包内使用的函数、变量和类型,可以将它们的首字母小写,例如myVariable、myFunction、myStruct等。
- 如果名称是一个缩写,则将其全部大写或全部小写,例如HTTP、URL。
- 变量名应该足够描述其用途,而不需要注释。
- 对于表示布尔类型的变量,最好在名称中包含一个形容词,例如isDone、hasError等。
3.1 包名 Package names
导入一个包后,包名成为访问其内容的入口。
按照惯例,包被赋予小写的单词名称,不应该有下划线或混合大小写,因为每个使用该包的人都需要键入该名称,所以倾向于简洁,不要担心冲突。包名只是导入的默认名称,不必在所有的源代码里唯一,如果有冲突的话,也可以在导入的时候选择使用不同的名称。
import "fmt" // 导入标准库fmt包
import ffmt "fmt" // 导入标准库fmt包并命名为ffmt
另一个惯例是包名位源代码目录的基本名称,在 src/encoding/base64
中的包应作为 "encoding/base64"
导入,其包名应为 base64
, 而非 encoding_base64
或 encodingBase64
。
包的使用者会使用包名来引用内容,因此包的导出名称里应当利用这点避免命名的重复。比如bufio
包的缓冲读取器类型成为Reader
而不是BufReader
,因为导出的时候会被视为bufio.Reader
,这个名称足够清晰简洁。而且使用的时候会根据包名寻址,因为bufio.Reader
和io.Reader
并不会冲突。同理,用于创建ring.Ring
新实例的函数,即构造函数,通常会称之为NewRing
,但是因为Ring
是包导出的唯一类型,所以只称之为New
即可,客户端使用ring.New
来调用看起来会比ring.NewRing
好很多。
3.2 获取器Getter
Go语言不会自动提供getter
和setter
的支持,需要自己提供这些方法,但是在getter
的名称中添加Get
既不符合惯例,也不是必要的。比如有一个名为owner
(小写,未导出)的字段,其getter
方法应当称之为Owner
(大写,已导出),而不是GetOwner
。大写字母即为可导出的规定为区分方法和字段都提供了便利。如果需要setter函数,可能为将其成为SetOwner
。
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
3.3 接口名称 Interface names
按照惯例,只会有一个方法的接口名称应当由方法名加上-er
后缀来命名,如Reader
(读取器),Writer
(写入器),Formatter
(格式化器)等等。这类的名称有很多,遵循他们和他们的函数名称是很有成效的。Read,Write,Close,Flush,String
这些都具有规范化的签名和含义。为了避免混淆,不要用这些名称为你的方法命名,除非他们有相同的签名和含义;同理,如果你的类型实现了一个与已知类型的方法具有相同含义的方法,则给他相同的签名和含义。比如,字符串转化方法命名应当为String
而不是ToString
。
3.4 驼峰命名 MixedCaps
Go中的管理是使用MixedCaps
或mixedCaps
而不是下划线mixed_caps
来编写多个单词的名称。
4.分号 Semicolons
像C语言一样,Go的正式语法里使用分号来终止语句,但是这些分号并不会出现在源代码里,而是词法分析器在扫描时使用一个简单的规则自动插入分号,因此输入文本里基本都没有分号。
规则是这样的:如果换行符前的最后一个标记是标识符(包括int,float64
之类的单词)、数字或字符串常量或是以下标记之一:break continue fallthrough return ++ --
,则词法分析器会在后面插入分号。即如果换行符在可以结束语句的标记后出现,则插入分号。
分号在闭括号前可以省略,因此如下的语句无需分号。
go func(){
for {
dst <- <-src
}
}()
通常情况下,Go程序只在for
循环子句等地方使用分号,用来分隔初始化器、条件和继续元素。如果在一行写多个语句,也需要用分号分隔。
不能将控制结构(if,for,switch,select)
的开括号放在下一行,如果这样的话,将在括号前插入分号,带来意外的效果。应该这样写
if i < f() {
g()
}
而不是这样写
if i < f() // wrong!
{
// wrong!
g()
}
5.控制结构
Go语言的控制结构和C语言的类似,但是也存在一些重要的差异。
- Go语言里没有do或while循环,只有for循环
- switch语句更加灵活
- if和switch接受一个可选的初始化语句,像for循环一样
- break和continue语句可以使用可选的标签来标识要中断/执行的位置
- 有一个新的控制结构,类型选择和多路通信复用器的select
- 语法:没有
()
,而且主体要使用{}
括起来
5.1 if
Go里面,简单的if
看起来长这样。
if x > 0 {
return y
}
花括号使得你把简单的if语句写成多行,如果主体里包含控制语句,如break或return,这种代码看起来就很舒服。因为if和switch接受初始化语句,因此常见的做法是设置局部变量。
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
在Go语言的官方库里,如果if
语句体以break,continue,goto,return
结束,那么该语句就不会流到下一条语句,该语句对应的else
语句也会被省略。
f, err := os.Open(name)
if err != nil {
return err
}
codeUsing(f)
下面是一个常见的例子,代码必须防范一系列错误条件的情况。如果成功的控制流沿着页面运行,逐步消除错误情况,代码会更加易读。因为错误情况往往会以return
结束,因此生成的代码不需要else
语句。
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
5.2 声明和分配 Redeclaration and reassignment
上述最后一个例子展现了":="短变量声明的使用方式。在第一行调用了f, err := os.Open(name)
,声明了变量f,err
,第四行又调用了d, err := f.Stat()
,声明了d,err
。其中err
在两个语句中都出现了,由第一个语句声明,在第二个语句里被重新赋值,其使用的是前面已经声明的err
。
在":="声明里,即使已经声明了变量v
,他仍然可以出现在下一个声明里,前提是:
- 本次声明与已声明的
v
处在同一作用域里 - 初始化里的对应值可以分配给
v
,且 - 至少有一个变量是由本次声明创建的
这个特性可以使我们很方便地只使用一个err
值。
值得一提的是,即使Go的函数形参和返回值在括号以外的词法位置出现,他们的作用域也与函数体相同
5.3 for
Go语言的for循环类似C语言的for循环,但不完全相同,它将for和while结合在了一起,但是没有do-while形式。Go语言提供了三种形式的for循环:
// 类似 C 语言中的 for 用法
for init; condition; post {
}
// 类似 C 语言中的 while 用法
for condition {
}
// 类似 C 语言中的 for(;;) 用法
for {
}
使用短声明可以更方便的在循环中声明索隐变量:
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
如果需要遍历一个数组、切片、字符串或映射,或是从一个通道里读取数据,可以使用range子句进行循环控制:
for key, value := range oldMap {
newMap[key] = value
}
如果只需要range的第一个值,可以省略掉第二个值:
for key := range m {
if key.expired() {
delete(m, key)
}
}
如果只需要range的第二个值,需要使用空白标识符(下划线_)来丢弃第一个值
sum := 0
for _, value := range array {
sum += value
}
对于字符串,range可以为你完成更多的工作,通过解析UTF-8
将Unicode码点分解为单独的字符。错误的编码会消耗一个字节并产生替换符U+FFFD
。(rune是Go术语,表示单个Unicode码点和相关的内置类型。详见语言规范。)以下循环:
for pos, char := range "日本\x80語" {
// \x80 在 UTF-8 编码中是一个非法字符
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7
Go没有逗号运算符,++和--是语句而不是表达式。因此如果想在for循环里运行多个变量,应该使用并行赋值,而不是++和--
// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
5.4 switch
Go的switch语句比C的更加通用,表达式不需要是常量或是整数,case语句会按照从上到下的顺序进行评估,并且找到匹配项;如果switch后面没有表达式,将匹配true
,因此可以将if-else-if-else
链路写成一个switch
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
switch并不会自动向下,但是case可以通过逗号分隔来列举相同的处理条件。
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
尽管他们在Go的用法和C语言用法差不多,但是break语句可以使switch提前终止。不过有时候需要中断周围的循环而不是switch,可以在循环外放置一个标签,并break到该标签:
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
当然,continue也能接受一个可选的标签,不过只能在循环里使用。
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
continue outer
}
fmt.Println(i, j)
}
}
这段代码中,我们使用了一个标签 "outer" 标记了外部的 for 循环。当内部的循环中的条件满足 i 等于 1 且 j 等于 1 时,我们使用 continue outer 跳出了外部的循环,而不是内部的循环。如果没有标签 "outer",continue 语句只会跳出内部的循环,而外部的循环仍然会继续执行。
需要注意的是,标签不能定义在函数内,只能定义在循环语句前面。另外,标签名必须唯一,并且只能作用于循环语句。在 Go 中,标签的主要作用是用于跳出多层循环或者选择性地跳过某些代码块。
作为这一节的结束,此程序通过两个switch语句对字节数组进行比较。
// 比较两个字节型切片,返回一个整数
// 按字典顺序.
// 如果a == b,结果为0;如果a < b,结果为-1;如果a > b,结果为+1
func Compare(a, b []byte) int {
for i := 0; i < len(a) && i < len(b); i++ {
switch {
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
switch {
case len(a) > len(b):
return 1
case len(a) < len(b):
return -1
}
return 0
}
5.5 类型选择 Type switch
switch还可以用于发现接口变量的动态类型,这样的类型切换使用类型断言的语法,并在括号里使用type关键字。如果switch在表达式里声明一个变量,那么每个case中这个变量都将拥有对应的类型。在每一个case中,重复利用该变量的名字也是常见的做法,实际上是分别声明一个具有相同名字但是类型不同的新变量。
var t interface{
}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T 打印任何类型的 t
case bool:
fmt.Printf("boolean %t\n", t) // t 是 bool 类型
case int:
fmt.Printf("integer %d\n", t) // t 是 int 类型
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t 是 *bool 类型
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t 是 *int 类型
}