New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Effective Go 摘记 #16

Open
rainyear opened this Issue Sep 9, 2015 · 0 comments

Comments

Projects
None yet
1 participant
@rainyear
Owner

rainyear commented Sep 9, 2015

Official document: Effective Go

Formatting

一切格式问题交给 gofmt

  1. VIM 配置: Vim as Golang IDE
  2. Atom 配置: go-plus

Commentary

/*
package comment.

package 源文件的说明文档,当存在多个文件时,只需要一个文档即可。
*/

// 单行注释

Names

Go package 中变量的公开性由变量首字母是否大写决定。

Package names

包名应该:单个单词,全小写,简短,有意义。

包名在引用时应该引用到最后一层目录的名称,在使用时只用最后一层目录名称作为包名:

// 源文件位置:/src/encoding/base64
import "encoding/base64"

base64.StdEncoding.EncodeToString([]byte(data))

避免使用:import .;避免以较长的名字来说明函数的功能,而应该以说明文档替代之。

@sdbaiguanghe 指正,在使用时应该由当前目录中 package x 定义决定,但最好还是保持两者一致。

Getters

Go 不自动支持 getterssetters,可以自己实现,但并不一定要以 Get 为名:

owner := obj.Owner()
if owner != user {
  obj.SetOwner(user)
}
Interface names

只有一个方法的接口,方法名通常在后面加 -er 后缀,如Reader, Writer, Formatter, CloseNotifier 等。

Semicolons

通常不需要加分号(like python, not js)。

Control structures

循环只有 for,条件判断只有 if & switch

If
if x > 0 {
  return y
}
Redeclaration and reassignment
f, err := os.Open(name)

d, err := f.Stat()

以上两行代码看起来 err 通过 := 被声明并定义了两次,但实际上这里第二次只是重新定义而非重复声明,因此是合法的。在满足下面这些条件时,即使变量 v 事先声明过,也可以用 := 符号进行重新定义:

  1. := 与事先的声明是在相同的作用域;
  2. 类型符合,可以赋值给 v
  3. 至少需要两个重新声明的变量。

这一点纯粹是处于实用主义的考虑,因为实践过程中可能有很多 err 需要捕捉,如果每一次都定义新的变量就太麻烦了。

For

与C语言不同,Go将 for/while/do-while 整合成一个,因此Go中循环语句只有 for,包括下面三种形式:

// 像 C 中的 for
for init; condition; post { }

// 像 C 中的 while
for condition { }

// 像 C 中的 for(;;)
for { }
Switch

Go中的 switch 比C更加一般化,被判断的变量类型没有限制;switch会自上而下检验每一条case直到遇到通过的条件为止,而且case语句不会自动下落因此默认不需要break关键词。

由于 case 不会自动下落,可以通过逗号连结不同的判断条件以实现返回相同解雇:

switch c {
  case ' ', '?', '&', '=', '#', '+', '%':
    return true
}

可以在 break/continue 后面加上指定标签将流程指向指定位置。

Type switch

switch 语句还可以用于 type switch 来判断变量所属类型:

var t interface{}
t = functionReturnSomeType()
switch t := t.(type) {
  default:
    fmt.Printf("Unexpected type %T\n", t) // %T 打印类别
  case bool:
    fmt.Printf("Boolean %t\n", t)
  case int:
    fmt.Printf("Integer %d\n", t)
}

Functions

Multiple return values

哇,可以返回多个值。。。

Named result parameters

除了可以返回多个值,还可以规定返回变量名:

func ReadFull(r Reader, buf []byte) (n int, err error) {
  for len(buf) > 0 && err == nil {
    var nr int
    nr, err = r.Read(buf)
    n += nr
    buf = buf[nr:]
  }
  return
}
Defer

defer 后的表达式会在函数结束后执行,用于释放资源。

f, err := os.Open(filename)
defer f.Close()

首先保证了我们不会忘记 Close() 操作,其次 CloseOpen 紧挨一起, 看起来更清楚……

defer 的函数以后进先出(LIFO)的顺序执行:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}
//=> 4 3 2 1 0

Data

Allocation with new

Go有两种分配变量空间的内置指令:newmake

Go中的 new(T) 分配一段新的内存并赋予其零值的类型 T,用Go术语来说,就是返回一个指向类型为 T 的零值的指针。由于 new 已经赋予其零值,不需要初始化操作,也就是说“即新即用”。

type SyncedBuffer struct {
  lock sync.Mutex
  buffer bytes.Buffer
}
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer     // type SyncedBuffer

上面两种分配或声明方式都使得新的变量可以立即投入使用。

Constructors and composite literals

new 初始化零值不能满足需求的时候,可以自己创建构造函数:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

与C不同,Go中可以返回函数中局部变量的地址。因此上面的代码可以简写为:

return &File{fd, name, nil, 0}

进一步,可以省略零值:

return &File{fd: fd, name: name}
Allocation with make

另一分配地址方法 make(T, args) 只能用于生成 slice/map/cahnnel,而且返回 T 类型(而非 *T)的初始化地址(非零值)。

make([]int, 10, 100)

上面的操作先分配长度为100个 int 类型数组的空间,再生成一个 slice 类型,且指向数组的前10个元素。

相比如果是 new([]int) 则是返回一个指向新创建的 slice 的指针,且该新创建的 slice 已赋零值,即 nil

Arrays

数组在规划内存分配时很有用,有时也用于避免内存肥婆,但其主要作用是用于创建 slice

Go中数组与C不同之处在于:

  1. 数组自身有值,将一个数组赋值给另一个数组会复制所有元素;
  2. 如果讲数组作为参数传递给函数,函数将接收一份拷贝,而非其指针;
  3. 长度是数组类型的一部分,[10]int[20]int 是不同类型。

尽量使用 slice

Slices

切片内部保存着指向实际存储数据的数组的指针,如果将切片赋值给另一个切片,两个切片将指向同一个数组。因此切片作为参数传递给函数也有可能改变原数据内容。

如下是一个文件读方法的示例:

func (f *File) Read(buf []byte) (n int, err error)

n, err := f.Read(buf[0:32])

切片的长度可以改变,只要还在内部数组长度限制范围内;要改变其长度,只要将其赋值为一个新的切片:

func Append(slice, data []byte) []byte {
  l := len(slice)
  if l + len(data) > cap(slice) {  // reallocate
    // Allocate double what's needed, for future growth.
    newSlice := make([]byte, (l+len(data))*2)
    // The copy function is predeclared and works for any slice type.
    copy(newSlice, slice)
    slice = newSlice
  }
  slice = slice[0:l+len(data)]
  for i, c := range data {
    slice[l+i] = c
  }
  return slice
}

上例中假设 cap(nil) = len(nil) = 0

上例中的 Append 方法最后我们必须 return slice,因为虽然切片中指向内部数组的指针可以直接在函数内部传递并重新指向,但切片里面其它属性(长度、容量)是传值的。

Two-dimensional slices
// 初始化总的二维切片
picture := make([][]uint8, YSize)
// 循环每一排,并为第二维初始化新的切片
for i := range picture {
  picture[i] = make([]uint8, XSize)
}
/*
OR
*/
// 初始化总的二维切片
picture := make([][]uint8, YSize)
// 创建一个大的切片,保存所有像素
pixels := make([]uint8, XSize * YSize)
for i := range picture {
  picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Maps

Map 将两种不同类型的数据配对联系在一起。key 可以是任何类型。和切片一样,Map 保留所有指向内置数据的指针,如果作为参数传递给函数,有可能改变其原先的值。

初始化:

var timeZone = map[string]int{
  "UTC":  0*60*60,
  "EST": -5*60*60,
  "CST": -6*60*60,
  "MST": -7*60*60,
  "PST": -8*60*60,
}
// 从 Map 中根据 key 获取 value
offset := timeZone["EST"]

试图从 Map 中寻找不存在的 key,将返回响应类型的零值。可以通过多重赋值检验:

var seconds int
var ok bool
seconds, ok = timeZone[tz]

若要删除 Map 中的数据,可以用delete

delete(timeZone, "PDT") // 即使不存在 "PDT" 也不会出错
Printing

参考一下官方文档:[package fmt](https://golang.org/pkg/fmt/)

Append

内置的 append 方法的函数签名如下:

func append(slice []T, elements ...T) []T {}

由于Go中的函数无法接受任意类型作为参数,因此 append 方法是一个内置函数,由编译器完成。

Initialization

Constants

常量在编译时才创建,即使定义在局部函数中。并且只能是数字、字符(runes)、字符串或布尔类型。例如 1 << 3 可以赋值给常量,而 math.Sin(math.Pi/4) 就不行,因为调用 math.Sin 函数需要在运行时完成。

Go中的枚举常量可以用 iota 来创建:

type ByteSize float64
const (
  _ = iota // 跳过第一个数字
  KB ByteSize = 1 << (10 * iota)
  MB
  GB
  TB
)
Variables

变量可以像常量一样初始化,不过其赋值表达式允许在运行时完成:

var (
  home   = os.Getenv("HOME")
  user   = os.Getenv("USER")
  gopath = os.Getenv("GOPATH")
)
The init function

每一个源文件都可以定义自己的 init 函数(实际上可以定义多个,最后定义的最后执行),用于完成一些配置。

init 在当前包中所有变量完成初始化以及所有引用包加载完成之后被执行。

Methods

Pointers vs. Values

(接口的)方法可以定义给任意被命名过的类型,接收器并不一定要是结构类型。

Interfaces and other types

Interfaces
Conversions

两个类型如果除了字面的名字不同之外是相同的,那么他们彼此之间可以相互转化。这种转化并不是创建新的值,只是从行为上看起来好像是多了另外一个类型(可以使用另外一个类型的方法)。但从整数转化成浮点数,却是创建了新的值。

Interface conversions and type assertions

在 type switch 中已经出现过一种形式的转化:接受一个接口,在每一个 case 从句中就像是将其转化为不同的类型:

type Stringer interface {
    String() string
}
var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
  return str
case Stringer:
  return str.String()
}

如果我们只关心它是不是某一种类型,可以用下面的方法检验:

if str, ok := value.(string); ok {
  return str
} else if str, ok := value.(Stringer); ok {
  return str.String()
}
Generality
Interfaces and methods

The blank identifier

The blank identifier in multiple assignment
Unused imports and variables
Import for side effect
Interface checks

Embedding

Concurrency

Share by communicating
Goroutines
Channels
Channels of channels
Parallelization
A leaky buffer

Errors

Panic
Recover

A web server

@rainyear rainyear added the Go label Sep 9, 2015

@rainyear rainyear self-assigned this Sep 9, 2015

@RoCry RoCry referenced this issue Sep 27, 2015

Open

2015-09-27 #57

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment