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


    来源

    1. 格式化 Formatting

    ​ 格式化是最有争议但是最不重要的问题。在Go语言里,采用gofmt来格式化程序,例如以下代码:

    type T struct {
        name string // name of the object
        value int // its value
    }
    
    • 1
    • 2
    • 3
    • 4

    ​ 调用gofmt后的格式为

    type T struct {
        name    string // name of the object
        value   int    // its value
    }
    
    • 1
    • 2
    • 3
    • 4

    ​ 这些都可以通过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
    
    • 1
    • 2

    ​ 另一个惯例是包名位源代码目录的基本名称,在 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)
    }
    
    • 1
    • 2
    • 3
    • 4

    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
        }
    }()
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ​ 通常情况下,Go程序只在for循环子句等地方使用分号,用来分隔初始化器、条件和继续元素。如果在一行写多个语句,也需要用分号分隔。

    不能将控制结构(if,for,switch,select)的开括号放在下一行,如果这样的话,将在括号前插入分号,带来意外的效果。应该这样写

    if i < f() {
        g()
    }
    
    • 1
    • 2
    • 3

    而不是这样写

    if i < f()  // wrong!
    {           // wrong!
        g()
    }
    
    • 1
    • 2
    • 3
    • 4

    5.控制结构

    Go语言的控制结构和C语言的类似,但是也存在一些重要的差异。

    • Go语言里没有do或while循环,只有for循环
    • switch语句更加灵活
    • if和switch接受一个可选的初始化语句,像for循环一样
    • break和continue语句可以使用可选的标签来标识要中断/执行的位置
    • 有一个新的控制结构,类型选择和多路通信复用器的select
    • 语法:没有(),而且主体要使用{}括起来

    5.1 if

    Go里面,简单的if看起来长这样。

    if x > 0 {
        return y
    }
    
    • 1
    • 2
    • 3

    花括号使得你把简单的if语句写成多行,如果主体里包含控制语句,如break或return,这种代码看起来就很舒服。因为if和switch接受初始化语句,因此常见的做法是设置局部变量。

    if err := file.Chmod(0664); err != nil {
        log.Print(err)
        return err
    }
    
    • 1
    • 2
    • 3
    • 4

    在Go语言的官方库里,如果if语句体以break,continue,goto,return结束,那么该语句就不会流到下一条语句,该语句对应的else语句也会被省略

    f, err := os.Open(name)
    if err != nil {
        return err
    }
    codeUsing(f)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下面是一个常见的例子,代码必须防范一系列错误条件的情况。如果成功的控制流沿着页面运行,逐步消除错误情况,代码会更加易读。因为错误情况往往会以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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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 { }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用短声明可以更方便的在循环中声明索隐变量:

    sum := 0
    for i := 0; i < 10; i++ {
        sum += i
    }
    
    • 1
    • 2
    • 3
    • 4

    如果需要遍历一个数组、切片、字符串或映射,或是从一个通道里读取数据,可以使用range子句进行循环控制:

    for key, value := range oldMap {
        newMap[key] = value
    }
    
    • 1
    • 2
    • 3

    如果只需要range的第一个值,可以省略掉第二个值:

    for key := range m {
        if key.expired() {
            delete(m, key)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果只需要range的第二个值,需要使用**空白标识符(下划线_)**来丢弃第一个值

    sum := 0
    for _, value := range array {
        sum += value
    }
    
    • 1
    • 2
    • 3
    • 4

    对于字符串,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)
    }
    
    • 1
    • 2
    • 3
    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
    
    • 1
    • 2
    • 3
    • 4

    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]
    }
    
    • 1
    • 2
    • 3
    • 4

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    switch并不会自动向下,但是case可以通过逗号分隔来列举相同的处理条件。

    func shouldEscape(c byte) bool {
        switch c {
        case ' ', '?', '&', '=', '#', '+', '%':
            return true
        }
        return false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    尽管他们在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)
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    当然,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)
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这段代码中,我们使用了一个标签 “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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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 类型
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  • 相关阅读:
    HummerRisk V0.6.0发布:升级列表高级搜索功能,扩充对象存储和操作审计支持范围等
    Redis——》数据类型
    HTML介绍——HTML筑基
    一篇文章搞懂:词法作用域、动态作用域、回调函数及闭包
    Phoenix的二级索引
    ssh远程连接报错:WARNING: POSSIBLE DNS SPOOFING DETECTED(已解决)
    百万数据直接返回,高性能量化数据库Arctic,基于mongo的列存引擎
    计算机专业毕业设计项目推荐12-志愿者管理系统(Spring+Js+Mysql)
    嵌入式开发需要具备什么技能才能在这个行业躺平
    electron
  • 原文地址:https://blog.csdn.net/weixin_45675097/article/details/132898924