Skip to content

Latest commit

 

History

History

effectivego

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Effective Go 学习笔记

Godoc

godoc 既是一个程序,又是一个 Web 服务器,它对 Go 的源码进行处理,并提取包中的文档内容。 出现在顶级声明之前,且与该声明之间没有空行的注释,将与该声明一起被提取出来,作为该条目的说明文档。这些注释的类型和风格决定了 godoc 生成的文档质量。

每个包都应包含一段包注释,即放置在包声明子句前的一个块注释。对于包含多个文件的包,包注释只需出现在其中的任一文件中即可。包注释应在整体上对该包进行介绍,并提供包的相关信息。它将出现在 godoc 页面中的最上面,并为紧随其后的内容建立详细的文档。

在包中,任何顶级声明前面的注释都将作为该声明的文档注释。在程序中,每个可导出(首字母大写)的名称都应该有文档注释。

命名

包名:应该简洁明了易于理解,包名应该以小写单词来命名,且不应该使用下划线或者驼峰法;包名应该为源码目录的 base name,如 “encoding/base64” 的包名为 “base64”

由于被导入的项总是通过它们的包名来确定,因此包名可以不用包含在变量命名中

长命名并不会使其更具可读性。一份有用的注释说明文档通常比额外的长名更有价值。

Getters:不要将 Get 放在 getter’s name 中
Setters:Setter’s name 可以包含 Set

接口命名:

按照约定,只包含一个方法的接口应当以该方法的名称加上 - er 后缀来命名,如 Reader、Writer、 Formatter、CloseNotifier 等。

Read、Write、 Close、Flush、 String 等都具有典型的签名和意义。为避免冲突,请不要用这些名称为你的方法命名, 除非你明确知道你的方法和它们的签名和意义相同。。反之,若你的类型实现了的方法, 与一个众所周知的类型的方法拥有相同的含义,那就使用相同的命名。请将字符串转换方法命名为 String 而非 ToString。

变量

在满足下列条件时,已经被声明的变量 v 可以出现在 := 声明中

  • 本次声明与已声明的 v 处于同一作用域中(若 v 已在外层作用域中声明过,则此次声明会创建一个新的变量)
  • 在初始化中与其类型相同的值才能赋值给 v,且在此次声明中至少有一个变量是新声明的

即便 go 中的函数形参和返回值在词法上处于大括号之外,但它们的作用域和该函数体仍然相同

Switch Type
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
   fmt.Printf("unexpected type %T", 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
}

Functions

可命名结果形参 Go 函数的返回值或结果 “形参” 可被命名,并作为常规变量使用,就像传入的形参一样。 命名后,一旦该函数开始执行,它们就会被初始化为与其类型相应的零值; 若该函数执行了一条不带实参的 return 语句,则结果形参的当前值将被返回。 此名称不是强制性的,但它们能使代码更加简短清晰:它们就是文档。

Defer

Go 的 defer 语句用于预设一个函数调用(即推迟执行函数), 该函数会在执行 defer 的函数返回之前立即执行。它是特殊的能够有效处理例如:资源释放,典型的例子就是解锁互斥和关闭文件。

推迟诸如 Close 之类的函数调用有两点好处: 第一,它能确保你不会忘记关闭文件。 第二,它意味着 “关闭” 离 “打开” 很近,这总比将它放在函数结尾处要清晰明了。

被推迟函数的实参(如果该函数为方法则还包括接收者)在推迟执行时就会求值, 而不是在调用执行时才求值。这样不仅无需担心变量值在函数执行时被改变, 同时还意味着单个已推迟的调用可推迟多个函数的执行。

func DeferTest() {
   fmt.Println("defer Test begin...")
   for i := 0; i < 5; i++ {
      defer fmt.Printf("%d ", i)
   }

   fmt.Println("defer Test end...")
   return
}

被推迟的函数按照后进先出(LIFO)的顺序执行,上述函数输出:

defer Test begin...
defer Test end...
4 3 2 1 0

New 和 Make

  • 内置函数 new 分配空间。传递给new 函数的是一个类型,不是一个值。返回值是 指向这个新分配的零值的指针。
  • 内建函数 make 分配并且初始化 一个 slice, 或者 map 或者 chan 对象。 并且只能是这三种对象。 和 new 一样,第一个参数是 类型,不是一个值。 但是make 的返回值就是这个类型(即使一个引用类型),而不是指针。 具体的返回值,依赖具体传入的类型。
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)

数组

在 Go 中:

  • 数组是值。将一个数组赋予另一个数组会复制其所有元素。
  • 若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
  • 数组的大小是其类型的一部分。类型 [10]int 和 [20]int 是不同的。

Slice

Slice 是引用类型,存在内存的重新分配

Map

映射是方便而强大的内建数据结构,它可以关联不同类型的值。其键可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。 切片不能用作映射键,因为它们的相等性还未定义。与切片一样,映射也是引用类型。 若将映射传入函数中,并更改了该映射的内容,则此修改对调用者同样可见。

方法

以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本,因此任何修改都将被丢弃,语言不允许这种错误。(通过变量并不一定总能取到相应的指针,而通过指针总能取到相应的值)

接口

Go 中的接口为指定对象的行为提供了一种方法:如果某样东西可以完成这个, 那么它就可以用在这里。 如果一个动物走路像鸭子,叫得像鸭子,name 它就是鸭子。

类型断言

switch - case : switch v := value.(type)
v, ok := value.(type)

空白标识符

检查某个类型是否满足某个接口:

var _ Interface = (*Type)(nil),如:
var _ json.Marshaler = (*RawMessage)(nil)

内嵌

接口内嵌

type Reader interface {
   Read(p []byte) (n int, err error)
}
type Writer interface {
   Write(p []byte) (n int, err error)
}
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
   Reader
   Writer
}

struct 内嵌

type A struct {

}

type B struct {

}

type AB struct {
   A // 匿名成员;a A 命名成员
   B // 匿名成员; b B 命名成员
}

并发

通过通信共享内存。go channel 是并发安全的。

func Serve(queue chan *Request) {
   for req := range queue {
      req := req // 为该 Go 程创建 req 的新实例。
      sem <- 1
      go func() {
         process(req)
         <-sem
      }()
   }
}

req := req 在 Go 中这样做是合法且惯用的。你用相同的名字获得了该变量的一个新的版本, 以此来局部地刻意屏蔽循环变量,使它对每个协程保持唯一。(闭包也需要这么做)

panic & recover

实际的库函数应避免 panic。若问题可以被屏蔽或解决, 最好就是让程序继续运行而不是终止整个程序。一个可能的反例就是初始化:若某个库真的不能让自己工 作,且有足够理由产生 panic,那就由它去吧

当 panic 被调用后(包括不明确的运行时错误,例如切片检索越界或类型断言失败), 程序将立刻终止当前函数的执行,并开始回溯 Go 协程的栈,运行任何被推迟的函数。若回溯到达 Go 协程栈的顶端,程序就会终止。不过我们可以用内建的 recover 函数来重新或来取回 Go 协程的控制权限并使其恢复正常执行。

调用 recover 将停止回溯过程,并返回传入 panic 的实参。 由于在回溯时只有被推迟函数中的代码在运行,因此 recover 只能在被推迟的函数中才有效。