Effective_go 阅读笔记
Commentary(注释)
- 每一个包都应该有一个 commentary,多文件的包在一个文件里 commentary 就可以了,内容详细或简洁由包来决定
- 使用纯文本,注意拼写,语法,句子结构
- 每一个(大写字母开头)导出的名字,都应该有一个 doc commentary
- doc commentary 的第一句话应该是个摘要
- Go的声明语法允许对声明进行分组。单个doc注释可以引入一组相关的常量或变量。自提出整个声明以来,这样的评论往往是敷衍了事。分组还可以指示项之间的关系,例如一组变量受互斥锁保护的事实。
Naming conventions(命名规则)
包名
- short, concise, evocative
- 小写,一个单词,没有下划线和混合使用
- 导入的最后一个单词为包名
- 尽量不要使用.符号,应为可以用在测试的时候,详见【。。。。。】
接收者
Go不为getter和setter提供自动支持。自己提供getter和setter并没有什么不妥,这通常是合适的,但是将Get放入getter的名字既不是惯用的也不是必需的。如果您有一个名为owner(小写,未导出)的字段,则getter方法应该称为Owner(大写,导出),而不是GetOwner。使用大写名称进行导出提供了将字段与方法区分开的钩子。如果需要,setter函数可能会被称为SetOwner。
接口名称
- 单方法接口命名,常常是方法名加er后缀
- Read, Write, Close, Flush, String等具有规范化的签名和含义,若有相同含义也可以使用
- 字符转换器用String而不是ToString
混合名称
最后,Go中的约定是使用MixedCaps或mixedCaps而不是下划线来编写多字名称。
Semicolons(分号)
- Go语言的分号都是词法分析器(lexer)加上的
- 分号插入规则:不能将控制结构的开括号(if, for, switch or select)放在下一行
Control structures(控制语句)
包括:if, switch, for, break, continue, type switch, 多路复用select
if
- 强制括号鼓励在多行上编写简单的if语句。无论如何,这样做很好,特别是当正文包含一个控制语句,如返回或中断时。
- 正文以break,continue,goto或return结尾 - 省略不必要的else
- 多用if判断error,return error
- 在:=声明中,即使已声明变量v,也可能出现变量v,前提是:
此声明与v的现有声明属于同一范围(如果v已在外部作用域中声明,则声明将创建一个新变量§),
初始化中的相应值可赋值给v,而在声明中至少有一个其他变量被重新声明。
a := "kkk"
a, b := "jkk", "lll"
fmt.Println(a, b)
§值得注意的是,在Go中,函数参数和返回值的范围与函数体相同,即使它们在包围体的括号之外的词法上出现。
for
for init; condition; post {}
for condition {}
for {}
for key, value := range oldMap {
newMap[key] = value
}
array/slice/string/map/channel
- 没有逗号操作,因此在for中使用并行赋值
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
switch
- case的条件没有自动顺延功能,条件之间可以用逗号分开,同时在一个case上
- break/break loop, 都可以用在switch case里面,continue不行
type switch
- 在type switch里重新声明的,实际上声明了一个具有相同名称但在每种情况下具有不同类型的新变量。
函数
多返回值
func (file *File) Write(b []byte) (n int, err error)
可命名结果形参
func nextInt(b []byte, pos int) (value, nextPos int) {
defer
- 典型的例子就是解锁互斥和关闭文件
- defer的函数按照后进先出(LIFO)的顺序执行
Data
new 分配
- 这是个用来分配内存的内建函数, 但与其它语言中的同名函数不同,它不会初始化内存,只会将内存置零。 也就是说,new(T) 会为类型为 T 的新项分配已置零的内存空间, 并返回它的地址,也就是一个类型为 *T 的值。
- 用 Go 的术语来说,它返回一个指针, 该指针指向新分配的,类型为 T 的零值。
- “零值属性” 是传递性的
- SyncedBuffer 类型的值也是在声明时就分配好内存就绪了。后续代码中, p 和 v 无需进一步处理即可正确工作。
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
构造函数和混合字面量
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
return &File{fd, name, nil, 0}
//return &File{fd: fd, name: name}
}
- 注意,返回一个局部变量的地址完全没有问题,这点与 C 不同。该局部变量对应的数据 在函数返回后依然有效。实际上,每当获取一个复合字面量的地址时,都将为一个新的实例分配内存
- 复合字面量的字段必须按顺序全部列出。但如果以 字段: 值 对的形式明确地标出元素,初始化字段时就可以按任何顺序出现,未给出的字段值将赋予零值
- 少数情况下,若复合字面量不包括任何字段,它将创建该类型的零值。表达式 new(File) 和 &File{} 是等价的
- 复合字面量同样可用于创建数组、切片以及映射,字段标签是索引还是映射键则视情况而定。
make 分配
- 内建函数 make(T, args) 的目的不同于 new(T)。它只用于创建slice、reflect和channel,并返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值。
- 若要获得明确指针,使用new分配内存或者显式地获取一个变量的地址
数组
- 数组是值,赋值会复制
- 值传递,产生副本
- 数组大小是类型的一部分,类型 [10]int 和 [20]int 是不同的。
slice
- 切片保存了对底层数组的引用,若你将某个切片赋予另一个切片,它们会引用同一个数组。 若某个函数将一个切片作为参数传入,则它对该切片元素的修改对调用者而言同样可见, 这可以理解为传递了底层数组的指针。
- 因为尽管 Append 可修改 slice 的元素,但切片自身(其运行时数据结构包含指针、长度和容量)是通过值传递的。而且如果是新建的slice,那么地址就变了的
二维切片
example:
在处理像素的扫描行时,这种情况就会发生。 我们有两种方式来达到这个目的。一种就是独立地分配每一个切片;而另一种就是只分配一个数组, 将各个切片都指向它。采用哪种方式取决于你的应用。若切片会增长或收缩, 就应该通过独立分配来避免覆盖下一行;若不会,用单次分配来构造对象会更加高效。
XSize, YSize := 5, 6
picture := make([][]uint8, YSize)
for i := range picture {
picture[i] = make([]uint8, XSize)
}
pixels := make([]uint8, XSize*YSize)
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Maps
- 与slice一样,也是引用类型
- 其键可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。 切片不能用作映射键,因为它们的相等性还未定义。
- set等于一个键值为true的map,再加上一些简单的索引操作,golang里面的是mapset
- 要删除map中的某项,可使用内建函数 delete,它以map及要被删除的key为实参。 即便对应的key不在该map中,此操作也是安全的。
delete(timeZone, "PDT") // Now on Standard Time
打印 Printing
- Go 采用的格式化打印风格和 C 的 printf 族类似,但却更加丰富而通用。 这些函数位于 fmt 包中,且函数名首字母均为大写:如 fmt.Printf、fmt.Fprintf,fmt.Sprintf 等。 字符串函数(Sprintf 等)会返回一个字符串,而非填充给定的缓冲区。
- 你无需提供一个格式字符串。每个 Printf、Fprintf 和 Sprintf 都分别对应另外的函数,如 Print 与 Println。 这些函数并不接受格式字符串,而是为每个实参生成一种默认格式。Println 系列的函数还会在实参中插入空格,并在输出时追加一个换行符,而 Print 版本仅在操作数两侧都没有字符串时才添加空白。
- fmt.Fprint 一类的格式化打印函数可接受任何实现了 io.Writer 接口的对象作为第一个实参;变量 os.Stdout 与 os.Stderr 都是人们熟知的例子。
| type | meaning |
|:-----
|%v|通用,对应值,数组,结构体,map|
|%+v|加上字段名|
|%#v|按照go的语法打印值|
|%q|遇到string,[]byte会加双引号,遇到integer and rune, 会加单引号|
|%#q|会尽可能使用反引号|
|%d|数字|
|%x|十六进制输出string, byte arrays and byte slice|
|% x|有空格的会在字节中加上空格|
|%T|打印值的类型|
|%p|打印地址|
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // 可以:注意转换
}
注意对m进行处理,转换,避免无限的循环
append
- append 会在切片末尾追加元素并返回结果,我们必须返回结果
- append可以直接加几个实参,或者slice
x := []int{1,2,3}
x = append(x, 4, 5, 6)
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
Initialization
constant
- 只能是number,character(rune), string or bool
type ByteSize float64
const (
_ = iota
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
//func (t T)String()string{} 定义T类型的输出格式
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
func main() {
fmt.Printf("%v\n", YB)
}
Init函数
- 每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态。 (其实每个文件都可以拥有多个 init 函数。)而它的结束就意味着初始化结束: 只有该包中的所有变量声明都通过它们的初始化器求值后 init 才会被调用, 而那些 init 只有在所有已导入的包都被初始化后才会被求值。
- 除了那些不能被表示成声明的初始化外,init 函数还常被用在程序真正开始执行前,检验或校正程序的状态。
method
指针 VS. 值
https://bingohuang.gitbooks.io/effective-go-zh-en/content/10_Methods.html
真心没看懂这几段代码。。
- 以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。
- 之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本, 因此任何修改都将被丢弃,因此该语言不允许这种错误。不过有个方便的例外:若该值是可寻址的, 那么该语言就会自动插入取址操作符来对付一般的通过值调用的指针方法。
- 在 []byte 上使用 Write 的想法已被 bytes.Buffer 所实现。
Interfaces and other types
interface
- 每种类型都可以实现多个接口
type Sequence []int
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s Sequence) String() string {
sort.Sort(s)
str := "["
for i, elem := range s {
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
conversions(类型转换)
func (s Sequence) String() string {
sort.Sort(s)
return fmt.Sprint([]int(s))
}
type Sequence []int
// // 用于打印的方法 - 在打印前对元素进行排序。
func (s Sequence) String() string {
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
interface conversions and type assertions(接口转换和类型断言)
- type switch 是类型转换的一种形式:它接受一个interface(目前遇见的都是空接口,,,),在选择 (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()
}
第一种情况获取具体的值,第二种将该接口转换为另一个接口。
- 类型断言:
value.(typeName)
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
若类型断言失败,str将继续存在且为字符串类型,但是它将拥有零值,即空字符串
- 作为对这种能力的说明,这里有个 if-else 语句,它等价于本节开头的类型选择。
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
generality(通用性)
- 若某种现有的类型仅实现了一个接口,且除此之外并无可导出的方法,则该类型本身就无需导出。 仅导出该接口能让我们更专注于其行为而非实现,其它属性不同的实现则能反映该原始类型的行为。 这也能够避免为每个通用接口的实例重复编写文档,
- 构造函数返回一个接口值而非实现的类型
接口和方法
- 既然我们可以为除指针和接口以外的任何类型定义方法,同样也能为一个函数写一个方法。
func main() {
http.Handle("/args", http.HandlerFunc(ArgServer))
fmt.Println("over?")
}
//ArgServer : Argument Server
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
//HandlerFunc call
type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, req *http.Request) {
f(w, req)
}
- 通过一个结构体,一个整数,一个信道和一个函数,建立了一个 HTTP 服务器, 这一切都是因为接口只是方法的集合,而几乎任何类型都能定义方法。
没怎么明白,明白了回来补。。
the blank identifier(空白标识符)
多重赋值
- for range中,以及其他情况
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
未使用的导入和变量
- 有些包或者变量目前未使用,但是之后可能会用到
package main
import (
"fmt"
"io"
"log"
"os"
)
var _ = fmt.Printf // For debugging; delete when done. // 用于调试,结束时删除。
var _ io.Reader // For debugging; delete when done. // 用于调试,结束时删除。
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}
按照惯例,我们应在导入并加以注释后,再使全局声明导入错误静默,这样可以让它们更易找到, 并作为以后清理它的提醒。
为副作用而导入
- 欲导入一个只使用其副作用的包, 只需将该包重命名为空白标识符:
import _ "net/http/pprof"
- 这种导入格式能明确表示该包是为其副作用而导入的,因为没有其它使用该包的可能: 在此文件中,它没有名字。(若它有名字而我们没有使用,编译器就会拒绝该程序。)
接口检查
- 若只需要判断某个类型是否是实现了某个接口,而不需要实际使用接口本身 (可能是错误检查部分),就使用空白标识符来忽略类型断言的值
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
- 当需要确保某个包中实现的类型一定满足该接口时,
var _ json.Marshaler = (*RawMessage)(nil)
在这种结构中出现空白标识符,即表示该声明的存在只是为了类型检查。 不过请不要为满足接口就将它用于任何类型。作为约定, 仅当代码中不存在静态类型转换时才能这种声明,毕竟这是种罕见的情况。
????????
embedding(内嵌)
- 可以将接口内嵌到接口中,只有接口才能内嵌到接口里面
- 内嵌结构体,内嵌类型的方法可以直接引用,这意味着 bufio.ReadWriter 不仅包括 bufio.Reader 和 bufio.Writer 的方法,它还同时满足下列三个接口: io.Reader、io.Writer 以及 io.ReadWriter。
- https://bingohuang.gitbooks.io/effective-go-zh-en/content/13_Embedding.html
concurrency(并发)
- 带缓冲的信道可被用作信号量,例如限制吞吐量
资源管理
//管理资源method
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req) // 这儿有 Bug,解释见下。
<-sem
}()
}
}
上面的代码的req会一直变,导致bug,俩解决办法:传参数or重新定义一个相同变量
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}
func Serve(queue chan *Request) {
for req := range queue {
req := req // Create new instance of req for the goroutine.
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
另一种管理资源的好方法就是启动固定数量的 handle goroutine,一起从请求信道中读取数据。
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}
func Serve(clientRequests chan *Request, quit chan bool) {
// Start handlers
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // Wait to be told to exit.
}
channels of channels
- Go 最重要的特性就是信道是一等值,它可以被分配并像其它值到处传递。 这种特性通常被用来实现安全、并行的多路分解。
示例给出了一个简单的速率有限,并行,非阻塞RPC系统的框架,不包含互斥锁
Parallelization(并行化)
- 目前 Go 运行时的实现默认并不会并行执行代码,它只为用户层代码提供单一的处理核心。
- 若你希望 CPU 并行执行, 就必须告诉运行时你希望同时有多少 goroutine 能执行代码。
- 有两种途径可达到这一目的,要么 在运行你的工作时将 GOMAXPROCS 环境变量设为你要使用的核心数, 要么导入 runtime 包并调用 runtime.GOMAXPROCS(NCPU)。 runtime.NumCPU() 的值可能很有用,它会返回当前机器的逻辑 CPU 核心数。
a leaky buffer(可能泄漏的缓冲区)
依靠带缓冲的信道和垃圾回收器的记录, 我们仅用短短几行代码就构建了一个可能导致缓冲区槽位泄露的空闲列表。
不知道这个例子根本上想要说明一个什么问题
error
- 对类型进行判断,必要时可以使用类型断言,进行可能对错误恢复
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // 恢复一些空间。
continue
}
return
}
panic
- 向调用者报告错误的一般方式就是将 error 作为额外的值返回。 标准的 Read 方法就是个众所周知的实例,它返回一个字节计数和一个 error。但如果错误时不可恢复的呢?有时程序就是不能继续运行。使用panic
- 实际的库函数应避免 panic。若问题可以被屏蔽或解决, 最好就是让程序继续运行而不是终止整个程序。
recover
- 调用recover()只有在defer里面才有用
- 调用 recover 将停止回溯过程,并返回传入 panic 的实参。
- 这种重新触发Panic的惯用法会在产生实际错误时改变Panic的值。
err = e.(Error) // 若它不是解析错误,将重新触发 Panic。
the most important sentence above all the code block