【Effective Go】高效Go编程之格式化+代码注释+命名+分号+控制结构

来源

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_base64encodingBase64

​ 包的使用者会使用包名来引用内容,因此包的导出名称里应当利用这点避免命名的重复。比如bufio包的缓冲读取器类型成为Reader而不是BufReader,因为导出的时候会被视为bufio.Reader,这个名称足够清晰简洁。而且使用的时候会根据包名寻址,因为bufio.Readerio.Reader并不会冲突。同理,用于创建ring.Ring新实例的函数,即构造函数,通常会称之为NewRing,但是因为Ring是包导出的唯一类型,所以只称之为New即可,客户端使用ring.New来调用看起来会比ring.NewRing好很多。

3.2 获取器Getter

​ Go语言不会自动提供gettersetter的支持,需要自己提供这些方法,但是在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中的管理是使用MixedCapsmixedCaps而不是下划线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 类型
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

豆沙睡不醒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值