[Effective Go 中文翻译] 第一篇

Posted 凌星An

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Effective Go 中文翻译] 第一篇相关的知识,希望对你有一定的参考价值。

介绍

Go 是一门新语言。尽管它借鉴了现有语言的思想,但它具有不同寻常的特性,使得有效的 Go 程序在性质上不同于用其它语言编写的程序。将 C++ 或 Java 程序直接翻译成 Go程序会产生不太可能产生令人满意的结果—Java 程序是用 Java 编写的,而不是 Go。另一方面,从 Go 的角度思考问题,可能会编写一个能得到相同效果但完全不同的程序。换句话说,要写好 Go程序,理解它的属性和习语很重要。了解 Go 编程的既定约定也很重要,例如命名、格式、程序构造等,以便您编写的程序易于其他 Go 程序员理解。

本文档提供了编写清晰、惯用的 Go 代码的技巧。它扩充了language specification、Go 之旅和如何编写 Go 代码,所有这些都是您应该首先阅读的

Formatting 格式化

代码的格式是最有争议但最不重要的问题。不同的人可能习惯于不同的代码风格,但最好不必这样做,如果每个人都坚持相同的风格,那么因代码格式浪费的时间会大量减少。问题是如何在没有冗长的规范下,实现一致的代码风格。

在Go中,采取了一种不同寻常的方法,让机器处理大多数格式问题。
gofmt程序(即go fmt命令,它在包级别而不是源文件级别运行)读取 Go 程序并且将程序代码风格 改为规定的缩进、垂直风格,会保留原有的注释,注释也会变为规定的格式。
你可以运行gofmt函数,了解某些情况下的代码风格。如果得到的结果不正确,请重新排版您的程序(或提交关于gofmt的错误),不要解决它。

eg:无需花时间 纠结 结构字段上的注释。 Gofmt会进行处理。

type T struct 
    name string // name of the object
    value int // its value

使用go fmt后

type T struct 
    name    string // name of the object
    value   int    // its value

注: Go标准库中所有代码都被 gofmt 统一了代码风格

缩进:
gofmt默认使用tab键进行处理。在一些必要情况下,你可以使用一些空格。
行代码长度:
Go每行长度没有限制。不需要太长导致溢出。如果太长,也可以适当位置换行,并使用tab进行缩进。
括号:
Go相比于C和Java需要更少的括号 :控制结构(if,for,switch)在语法不需要括号。运算符优先级层次结构更短也更加清晰

x<<8 + y<<16  (<<的优先级比 +  更高)

Commentary 注释

Go中提供了 C语言风格的 注释方法: /* */ 可以进行块注释 // 行注释
块注释常用于 包 的说明 ,在某些地方也是需要的,也可以注释一块区域的代码。

godoc程序会处理源文件并且提取关于包的注释。注释一般出现在最上边,会和声明一起提取出来,作为解释文档。注释的风格好坏 决定了godoc程序产生的文档质量。

每个包都应该有包注释,(写在package 语句上面 的块注释)。如果一个包当中有多个源文件,包注释只需要在任意一个文件中编写即可。注释应介绍包并提供与整个包相关的信息。它将首先出现在godoc页面上,然后应设置随后的详细文档。

eg:

/*
Package regexp implements a simple library for regular expressions.

The syntax of the regular expressions accepted is:

    regexp:
        concatenation  '|' concatenation 
    concatenation:
         closure 
    closure:
        term [ '*' | '+' | '?' ]
    term:
        '^'
        '$'
        '.'
        character
        '[' [ '^' ] character-ranges ']'
        '(' regexp ')'
*/
package regexp

如果是一个很简单的包,则包注释也可以简单化。
eg:

// Package path implements utility routines for
// manipulating slash-separated filename paths.

注释不需要多余的格式 比如:星号 ;生成的注释可能不会以固定宽度的字体展示,也不需要尝试用空格 控制格式。像gofmt 一样,godoc会进行相关的处理。

依靠上下文,godoc可能不会格式化注释,所以确保注释的格式,使用正确的单词,符号,句子结构,拆分较长的行等等。

在包中,紧接在顶级声明之前的任何注释都用作该声明的文档注释。程序中的每个导出(大写)名称都应该有一个文档注释。

文档注释最好为完整且允许各种各样的自动演示的句子。第一句话应该是一个以声明的名称开头的一句摘要。

// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) 

Names 命名

命名在Go中,和其他语言一样,是非常重要的。命名甚至会有语法影响: 一个变量在包外是否可见取决于变量名称首字母的大小写。(首字母大写,包外可见;首字母小写,包外不可见。)

Package names 包命名:

当包被导入时,包名会成为包内容的访问器。

import “bytes” //导入bytes包

bytes.Buffer //导入包后,可以这么使用包内的变量

包名应该短小、简明、令人容易理解。按照惯例,包应该以一个全部小写的单词命名。不应该有下划线或者混合大写字母。

不需要担心包名冲突问题。包名只是包导入时的默认名称,不需要是唯一的。防止罕见的包名冲突问题,我们可以给冲突的包起别名,进行本地使用。

package main
//给fmt包起本地名称为f,可以使用f名称导入fmt包的内容
import f "fmt" 

func main() 
	f.Println("Hello, 世界")

另一个习惯就是,包名一般为所在文件路径中最后一个文件夹的名称

eg: 包base64 所在的目录为src/encoding/base64

Getters(获取器):

Go 不提供对 getter 和 setter 的自动支持。自己提供 getter 和 setter 并没有什么问题,而且这样做通常是合适的,但是Get将 getter 的名称放入其中既不是惯用的也不是必需的。

如果您有一个名为 owner(小写,未导出)的字段,则应调用 getter 方法Owner(大写,已导出),而不是GetOwner. 使用大写名称进行导出 将字段与方法区分开来。如果需要,可能会调用 setter 函数SetOwner。这两个名字在实践中都很好读:

owner := obj.Owner()
if owner != user 
    obj.SetOwner(user)

Interface names 接口命名:

惯例: 只有一个方法的接口命名为 在方法名后面加上er后缀,
eg: Reader, Writer, Formatter, CloseNotifier

Read, Write, Close, Flush, String 还有很多其他这样的命名,它们都有标准的函数签名和确定的意义。为了避免歧义,不要使用其中的名称命名你写的方法,除非它有相同的函数签名和意义。

MixedCaps

Go中 倘若 命名中 使用多个单词,一般采用单词大写的形式,而不会使用下划线
例如: MixedCaps or mixedCaps

Semicolons 分号

和C一样,Go的语法也是使用分号来结束语句。但又和C不同,分号可以不出现在源码中。词法分析器在检测语法时凭借简单的规则自动插入分号,所以可以在源码中不写分号。

如果换行符前是一个标识符(如 int 、float64) ,字面常量(如 数字常量或者字符串常量 ) 或者 下面符号中的一个

break continue fallthrough return ++ – )

词法分析器会在结尾插入分号。

规则可以概括为:

if the newline comes after a token that could end a statement, insert a semicolon”

分号会立即插入到左括号后面,所以下面的语句不需要写分号。

go func()  for  dst <- <-src  ()

Go中,for循环必须写分号,用来间隔 初始化语句,条件判断语句,延续语句。
eg:

for i:=1;i<100;i++ 

如果一行当中有多条语句,必须使用分号进行间隔。

自动插入分号导致的影响,你不能将左括号放在控制结构的下面。
eg:

if i < f()  // wrong!
           // wrong!
    g()

正确的演示:

if i < f() 
    g()

Control structures 控制结构

Go 的控制结构与 C 的控制结构相关,但在一些方面有所不同。
没有do-while、while循环,可以使用更具概括性的for循环来代替;
switch结构更灵活; if和switch可以像for一样,接收一个可选的初始化语句; break和continue语句采用可选标签来标识要中断或继续的内容;

有新的控制结构,包括 type switch和多路通信多路复用器,select。语法也略有不同:没有括号,必须始终用大括号分隔

if结构

下面是一种良好的示范,尤其是内部有控制语句如return 或者break时。

if x > 0 
    return y

if和switch 接收一个初始化语句,这是设置一个局部变量的普遍方式。
eg:

if err := file.Chmod(0664); err != nil 
    log.Print(err)
    return err

在Go标准库中,如果if 语句不会延申出下一个if判断,尤其是内部以break,continue,goto或者return 结束, else是不必要的,会进行省略。
eg:

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)

Redeclaration and reassignment

短声明 := 在上面的代码中已经使用了
eg:

f, err := os.Open(name)
//声明了两个变量f和err
d, err := f.Stat()
//上面语句看起来声明了d和err变量,但是err在之前已经声明过了。

这种重复是合法的。 err在第一个语句中被声明,在第二个语句只是被重新赋值。这意味着调用f.Stat使用了之前声明的err变量,仅赋了新值。

如果之前使用过:= 声明过变量v ,如果两个处于相同的作用域则会使用之前的变量v,将相应的值赋值给v,但至少要声明一个新变量。如果处于不同的作用域,则会创建一个新的变量v。

:= 这种方式是一种纯粹的使用主义,它很容易使用同一个err变量,在一种长的if-else 链中,是很常用的。

值得注意的是: 在Go中函数的参数和返回值 与 函数体的范围是一样的,即使它们没有在函数体的括号内。

For

Go中的for循环和C中的是相似的,但不完全相同。它结合了for和while 。在Go中是没有do-while和while的。有三种形式,仅有一种形式必须写有分号。

// Like a C for
for init; condition; post  

// Like a C while
for condition  

// Like a C for(;;)
for  

在循环中,可以使用 短声明 很简单的声明下标变量

sum := 0
for i := 0; i < 10; i++ 
    sum += i

如果你在循环遍历数组(array)、切片(slice) 、字符串(string) 、map 、从通道(channel)读取数据,可以使用 range
eg:

for key, value := range oldMap 
    newMap[key] = value

如果你只需要使用key或者index(下标) ,可以不写第二个变量
eg:

for key := range m 
    if key.expired() 
        delete(m, key)
    

如果你只需要使用value ,不需要使用第一个变量,可以使用占位符( _ ) 丢弃第一个变量
eg:

sum := 0
for _, value := range array 
    sum += value

对于字符串,range可以解析UFT-8 编码,而不只是显示单个Unicode 编码。错误的编码占1个字节,会使用rune 类型U+FFFD的形式打印 。

注: rune is Go terminology for a single Unicode code point

eg:

for pos, char := range "日本\\x80語"  // \\x80 is an illegal UTF-8 encoding
    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中没有逗号运算符。 ++和- - 是一个语句,而不是表达式。
(没有前缀++或者- - )
错误使用:

a:=0
//syntax error
++a
--a
b:=a++

正确使用:

a:=0
a++
a--

如果你想要在for循环中运行多个变量,你可以使用 平行赋值
eg:

// 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]

Switch

Go中的switch比C当中的更加自然,强大。swith后的表达式,不限于常量或者整形数据 ,case 从上到下匹配,直到找到匹配项。如果switch后没有表达式,默认表达式为true 。
所以将if-else if -else 链改写为switch 是十分有用且常见的。

eg:

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

There is no automatic fall through, but cases can be presented incomma-separated lists.

case 允许是逗号分隔的列表

eg:

func shouldEscape(c byte) bool 
    switch c 
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    
    return false

Go中和C语言不相同的用法是,break 语句用于终止最近的switch 。当然,有时也会终止最近的循环,而不单单是switch。Go中可以给循环设置label (标签) ,使用 break 终止标签的循环。
eg:

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语句,也可以接收一个label(标签) ,他仅适用于循环。

下面是一段使用两个switch实现byte切片比较的代码

// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
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

Type switch

switch也用于识别接口变量的动态类型 。所谓的type switch是使用了 关键字type够成的类型断言。如果 switch 在表达式中声明了一个变量,则该变量将在每个分支中具有相应的类型。在这种情况下重用名称也是惯用的,实际上是在每种情况下声明一个具有相同名称但类型不同的新变量。

eg:

var t interface
t = functionOfSomeType()
switch t := t.(type) 
default:
    fmt.Printf("unexpected type %T\\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\\n", *t) // t has type *int

以上是关于[Effective Go 中文翻译] 第一篇的主要内容,如果未能解决你的问题,请参考以下文章

[Effective Go 中文翻译]函数篇

[Effective Go 中文翻译] Initialization篇

Python 程序员快速学 Go+ 系列,第一篇+官方手册翻译

Effective Go中文版(更新中)

《Effective Java 第三版》新条目介绍

effective java 3th 序