[Effective Go] Golang进阶

[Effective Go] Golang进阶

Golang进阶(其实还是基础)
看官网文档做的笔记。厌倦了每次捡起Go都要重看文档了。

基础语法可以通过 Tour of Go 学习,然后看 Effective Go进阶, 主要是学习一些注意事项及规范。这里是记录 Effective Go的笔记。
语言规范用来查阅不清楚的地方(但是最好全部浏览一下)

[语法参考](https://go.dev/ref/spec)
[标准库参考](https://pkg.go.dev/std)

Table of Contents

      • [Effective Go] Golang进阶
        • 简介
        • 格式化
        • 注释
        • 命名
          • 包命名
          • Getters,Setters 命名
          • Interface 命名
          • MixedCaps 混合命名法
        • 分号
        • 控制结构
          • If
          • 重声明与重赋值
          • For
          • Switch
        • 函数
          • 多个返回值
          • 为返回值命名
          • defer
        • 数据
          • 使用 new 分配
          • 构造函数和字面量初始化
          • 使用 make分配
          • 数组
          • 切片
          • 二维切片
          • 映射
          • 打印
          • Append
        • 初始化
          • 常量
          • iota
          • 变量
          • 初始化函数
        • 方法
          • 指针绑定 VS 值绑定
        • 接口和其他类型
          • 接口
          • 转换
          • 接口转换与类型断言
          • 通用性
          • 接口和方法
        • 空白标识符
          • 多赋值中的空白标识符
          • 未使用的引入和变量
          • 特殊需求引入
          • 接口检查
        • 嵌入
        • 并发
          • 通过交流共享
          • Goroutines
          • Channels
          • Channels of channels
          • 并行化
          • 缓存泄露
        • 异常
          • panic
          • recover

简介

是用来介绍一些书写Go的基本原则和注意点的一篇文章,基本到什么程度,就是说,以至于这篇文章(EG)自从09年Go发行以来没怎么更新过,但是因为Go本身也没什么大的变化,作者认为就介绍怎么书写高效的Go而言,这篇文章够用了,不用改,他希望保留文章原来的风格,也不希望别人指手画脚。

然后EG就变成类似时间胶囊的东西(?),但是确实,即使是在CS领域,一些东西也是永远不会过时的。
(黑你好浪漫)

还有一点是,Go好像是对标C开发出来的(?),也好像目标受众主要是C程序员?所以EG作者经常拿Go的语法和C作比较,三句话不离C,C语言基础不扎实的话可能不好看懂(比如白,虽然我也)

格式化

go fmt 命令会在包层次进行格式化,就实际使用而言,我保存一次文件她就格式化一次
一些格式化细节:

  1. 使用 Tab进行缩进,gofmt会给你替换,只有在必要时才使用空格
  2. 对行没有长度限制,太长的行gofmt会给你包裹起来然后用一个Tab缩进(会吗?gofmt怎么定义长呢,比如初始化多个变量时?)
  3. 很多地方没有括号,运算符表达式易读性可能较差,所以gofmt会添加空格以增强运算符表达式的可读性

注释

行注释,块注释
行注释更常用,块注释通常是包注释,但是在expression内或者想注释大块代码时也很有用
出现在顶层声明正上(之间无空行)的注释会被视为文档注释可被解析为基本文档导出, 可用于生成文档, 有一点是, 文档注释可以是多行行注释或者是块注释, 但是多行行注释用的居多,我也不知道为什么 ,另外, 文档注释有一个简单的类似markdown的语法可以参考以写出更便于提取生成的文档, 详见 Go Doc Comments

// C++-style line comments
// [Go Doc Comments](https://go.dev/doc/comment)
/*
	C-style block comments
*/

命名

命名在Go里甚至有语义影响(大写命名被导出)

包命名

当引入一个包时, 包名是获取包内容的获取器.
好的包名应当是: 简短的, 明确的, 易记的.
按照惯例, 包名通常是小写单词, 无需加下划线或混合大小写.
但不能过度追求简洁.
无需担心包名冲突, 包名在引入时只是一个默认引入名, 你可以指定别名引入.
冲突的情况极少, 因为引入语句中的路径往往决定了在用哪个包(特别是在远程引入的情况下).
另一个惯例是, 包名是包内文件所在源目录的目录名, 对于src/encoding/base64, 引入路径为encoding/base64, 但是包名仅仅是base64, 就是说, 同一个包的文件尽可能放在一个以包名为目录名的目录下
使用包的内容时应该用包名来引导, 避免所有引入包的导出命名发生冲突
不要使用import .,除非是用在必须在 被测试文件包外 测试的测试文件中简化引入

 Similarly, the function to make new instances of ring.Ring—which is the definition of a constructor in Go—would normally be called NewRing, but since Ring is the only type exported by the package, and since the package is called ring, it's called just New, which clients of the package see as ring.New. Use the package structure to help you choose good names.

大意是,实例化对象时,如果这个对象所在的包只导出了这个对象,并且构造函数名按照惯例叫NewObject,可以直接用packagename.New实例化

Getters,Setters 命名

Go没有自动支持Getters和Setters的机制
把 get 这个单词放到 getter 的名字里是不必要且不常用的
对于一个未导出的域(私有属性),比如 obj.field 用首字母大写的该域的名称来作为Getter的函数名(给 obj绑定一个叫 Field的函数作为Getter),用 SetField作为Setter的函数名,经验告诉我们这样可读性不错

Interface 命名

按照惯例,如果这个接口只有一个方法,应该把方法名后缀 er 或做类似的修改变成施动者名词来作为接口名
Go本身也有很多类似这样的接口及方法命名,Read,Write,Close,Flush,String等,为了避免混淆,你不应该使用这些方法命名,除非你是真的需要实现这些接口

MixedCaps 混合命名法

Go的惯例是使用混合大小写( 导出 MixedCaps 不导出 mixedCaps )进行多词命名,而不是下划线

分号

与C类似,使用分号来划分语句,但是很多分号不需要写在源代码里,编译时词法分析程序会自动插入分号
遵循一定的插入规则
这样会导致的结果之一是,你不能把控制结构(if,for,switch,select)的左花括号换行写

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

控制结构

控制结构总体与C类似,但没有do``while,只有for和更灵活的switch, ifswitch 可以多写一条初始化语句, breakcontinue 接收一个标签以确定跳出什么或继续什么(给语句label, goto也可以用),新的控制结构:对类型的switch, 多路通信复用器(无法想象黑你打出这几个字时的表情hhhh) select. 没有括号,但是代码块必须被花括号包围

If

强制花括号会使得即使在书写简单的if语句时也需要分多行书写,这是一种好的风格,尤其是代码块内包括控制语句时
利用 if switch的初始化语句设置一个局部变量,很常见的行为

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}
//In the Go libraries, you'll find that when an if statement doesn't flow into the next statement—that is, the // body ends in break, continue, goto, or return—the unnecessary else is omitted.
f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)

好像是个 legacy 语法,也没看懂,感觉就是,利用 if的初始化语句,能不写else就不写, 可读性会很好

重声明与重赋值

使用:=运算符声明变量时,可以重复声明已经存在的变量(名),如果:

  1. 想声明的变量与已经存在的变量在同一个作用域中
  2. 必须有对应值被赋给想声明的变量
  3. 至少另一个新创建的变量

简直是为err值量身打造,纯粹出于实用主义的奇怪设计,而且还确实很常用

值得注意的一点是,在词法上,函数返回值与函数参数不在包括函数体的花括号内(返回值也?),但实际上两者都在函数体的作用域内
(应该是指,就返回值本身而言,确实定义在外面的,命名返回值很好理解,但即使是未命名,返回的机制是,先初始化一个返回类型的变量,把return的参数赋给这个没名字变量,再返回,也是defer不能修改未命名返回值的原因)

For

与C类似, 但没有 whiledo``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
}

对数组, 切片, 映射, 字符串, 缓存管道进行访问, 可以用 range 辅助迭代
默认会返回索引和值(对于映射是键和值), 可以都取, 可以只取索引忽略值, 可以只取值忽略索引(使用_表示索引)

for key, value := range oldMap {
    newMap[key] = value
}
for key := range m {
    if key.expired() {
        delete(m, key)
    }
}
sum := 0
for _, value := range array {
    sum += value
}

对于字符串, 在迭代时Go会自动将非 UTF-8字符替换为一字节的rune类型U+FFFD

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+FFFD '�' starts at byte position 6

Go没有逗号运算符, ++--是语句而不是expressions.
如果你想在一个 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]
}
Switch

Go的switch比C更常用, 待匹配表达和 case表达不一定要是常数甚至整数; 自上而下进行匹配;
如果没有设置待匹配表达, 则待匹配表达被设置为true, 使得用switch实现if``else链成为可能

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
}

不会自动下沉(手动设置fallthrough关键字), 但是 cases用逗号隔开, 以用于同时匹配多个值执行相同操作

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

使用 Label机制加 break 跳出外界循环和switch:

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)
        }
    }

示例, 比较两个字节切片(字典序):

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

对类型的switch, 用来确定接口变量的动态类型
使用 类型断言的语法 .(type)
如果匹配到某类型, 这个case的语句内会被转换为该类型
很自然地用相同的命名来表示不同类型的接口变量(就是说初始化语句不需要重新命名, 直接赋给她类型断言的值, 后面相当于直接转了类型)

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
}

函数

多个返回值

函数和方法可以返回多个值,主要应用就在于,函数可以返回一个异常的标识,异常处理更方便,另一个是不需要刻意传入指针形参来作为返回值(意思是,C里你想返回多个值,简单的用数组,复杂的需要在参数里传入返回值(结构体居多)的指针或地址,在函数体里访问返回值修改这样,写起来比较麻烦)

为返回值命名

给返回值命名之后,可以将返回值当作常规变量在函数作用域内使用,一开始会被初始化为对应类型的零值,然后你返回语句没有参数的话,就会自动返回被命名的返回值变量。不是强制的,但是可以让代码更短更清晰,就像简单的函数文档,告诉你函数参数和返回值,命名合适甚至作用都比较清楚,总之就是更明确

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 关键字可以将语句延迟至其所在函数返回后再执行
说是返回后,但是return分两步,defer在这两步之间进行,所以如果是使用命名返回值,在defer里对返回值的更改会生效,如果是直接返回,实际上Go会生成一个没名字的变量先存了结果,defer执行,然后返回没名字结果,所以defer访问不到,修改不了
defer解析

优点是防止你忘了关文件,而且关闭语句放在开启语句下面,更清楚

按照后进先出的原则执行

defer 延迟的语句中,函数参数的计算在defer当前位置进行,函数的执行在所谓当前所处函数的返回时运行
也就是,还不能直接理解为把defer的语句直接扔到函数体最后执行,更像是把defer后面的语句冻结了参数扔到最后执行
下面这个例子看懂了大概就懂了

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}
func un(s string) {
    fmt.Println("leaving:", s)
}
func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}
func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}
func main() {
    b()
}
// entering: b
// in b
// entering: a
// in a
// leaving: a
// leaving: b

典型应用就是解锁互斥锁或者关闭文件

数据

使用 new 分配

Go有两种分配原语,内置函数 new make
new函数在初始化时,只是创建一片内存的地址,作为被创建类型的指针返回,用于初始化对象,初始化的对象的每个属性都是零值
make只用于初始化slice,map,channel,new多用于自定义结构

当然除了用new创建对象也可以用var name type创建

构造函数和字面量初始化

有时你想在初始化对象时指定一些值,而不是全是零值,就需要自己给对象绑定一个构造函数,惯例命名为NewObject
一般是在函数里用new实例化,然后给想改的属性赋值,然后返回实例地址,如下1
但是,也可以通过字面量直接初始化,如下2,然后还是返回地址

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
}
func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
	return &File{fd, name, nil, 0}
}

你可以指定参数名进行字面量初始化 return &File{fd: fd, name: name},否则需要有序传入所有参数
极端情况下,new(Object)&Object{} 实际上是一样的
字面量初始化实际上内部结构就经常用

可能说,那我直接字面量初始化就好了写什么构造函数,但是构造函数还可以检查参数之类,封装成函数更合理

使用 make分配

make(T, args)返回类型T的一个实例,而不是指针,之所以只用于slices,maps,channels,是因为这三种类型只有在被指定参数初始化了才能使用,slice要指定类型,容量,长度,map指定key和value的类型,channel指定传输类型和缓存大小。所以make直接初始化了更方便,相当于给你自定义了构造函数,不然就得像下面这样初始化

// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
数组

与 C的不同点:

  1. 数组是值,被赋给另一个数组会赋值所有元素,而不是传地址
  2. 给函数传递数组作为参数的话,函数体内使用的是该数组的复制,不是其指针,所以无法修改
  3. 数组的大小是她类型的一部分,[10]int[20]int是两种不同的类型
    不同点1很有用,但是很昂贵,因为复制可能开销很大,所以一般想要C的效果就传数组的指针或地址
func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // Note the explicit address-of operator

但是这样不是很符合Go, 想修改数组传数组的切片就可以

切片

把数组包装为更通用强大便利的数据序列接口。除了某些需要明确维度的操作,比如矩阵变换,Go里的大多数数组编程可以用切片完成
切片持有对依赖数组的引用,把切片赋给另一个切片,引用会是相同的( 切片A的切片a 引用与A相同,如果A没有重分配过),修改切片会修改依赖数组

切片的长度就是它所包含的元素个数。
但是切片的容量是从它的第一个元素开始数,到其依赖数组或切片元素末尾的个数

append函数给slice添加元素,超过容量会重分配,注意此时会丢失对依赖数组或切片的引用

二维切片

初始化二维数组,二维数组初始化必须用常量规定大小,用法类似C
但是二维切片只能append,不难用吗

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte     // A slice of byte slices.

对于二维切片,每一“行” 可以是不等长的,比如一个多行文本,每一行就不一定等长

text := LinesOfText{
    []byte("Now is the time"),
    []byte("for all good gophers"),
    []byte("to bring some fun to the party."),
}

预分配二维切片有两种方式,比如你要构建图像(h * w pixels),第一种是先初始化一个 h容量的二维切片,然后独立分配每一行为 w容量的切片;另一种是先初始化一个 h容量的二维切片,再初始化一个 h*w 容量的一维切片,然后把这个一维切片一行一行放到二维切片里。如果每一行可能会收缩或扩张,使用前者避免覆写下一行,否则后者会更有效率。
(为什么会覆写?可能是因为像下面这样写,按照slice的结构,每一行slice的内存地址都在原来分配给pixels的内存区域内,都是cap长,不能随便修改容量,我猜是这样,建立在 slice这个操作可能就是修改内存地址(开始地址)和偏移量(总长度cap)和可访问长度len 的理解之上,看了slice结构应该就懂了)
那黑第二种方法每一行岂不是一种类似数组的存在?为什么不直接用数组?索引不太方便吧

// 1
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
    picture[i] = make([]uint8, XSize)
}
//2 
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
    picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
映射

便利强大的内置数据结构,可以建立键值对
键的类型可以是任何定义了等号运算符==,!=(即实现了可比较接口)的类型,整数,浮点数,复数,字符串,指针(指向同一个地址或都是nil),通道(本质是地址,地址相同或都是nil),接口(实现接口的对象实例类型相同值相同),结构(只包含以上类型元素的结构,不能带函数),数组(只包含以上结构的数组)。但是切片, 函数,映射不可以,因为没实现那个接口

持有对依赖数据结构的引用,直接作为参数传给函数,函数体内的更改会修改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,
}

赋值和获取都可以通过键来完成,若不存在会返回对应值类型的零值


attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}
if attended[person] { // will be false if person is not in the map
    fmt.Println(person, "was at the meeting")
}
attended[Ann] = false
state = attended[Ann]

但是不存在返回零值可能会导致,比如我这里的值就是零值呢?那这个键到底存不存在?是存在且值为0?还是不存在?
所以再通过键获取值时可能需要多一个ok变量来确认到底这个键有没有被设置,类似处理异常,判断语句的初始化语句作用就来了:

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

同样地,只想确认也没有被设置的话,用下划线代替值

_, present := timeZone[tz]

根据键删除:

delete(timeZone, "PDT")  // Now on Standard Time
打印


( ?黑你好懒)

Append

签名:

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

接收多个参数,作用是给slice添加元素,但是为什么要有返回值?因为如果cap不够导致扩容的话,依赖数组的地址就变了,你可能需要赋给另一个值

如果需要在一个slice添加另一个slice, 可以在append里使用类似解包的...来把y的元素都添加到x里:

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

初始化

常量

Go里的常量只是常量,在编译时被创建,只能是数字,字符,字符串,布尔值,为减少编译时间,常量必须是编译器就可以执行的常量表达式,比如1<<3是常量表达式,但 math.Sin(math.Pi/4)不是因为math.Sin的调用应该发生在运行时

iota

使用iota计数器创建枚举常量
不指定常量的表达式,每一行的表达会隐式重复上一行,只有iota加了1,所以可以这样创建字节单位

const (
	n1 = iota //0
	n2        //1
	n3        //2
	n4        //3
)

const (
	n1 = iota //0
	n2        //1
	_
	n4        //3
)

const (
	n1 = iota //0
	n2 = 100  //100
	n3 = iota //2
	n4        //3
)

const n5 = iota //0
const (
	_  = iota
	KB = 1 << (10 * iota)
	MB = 1 << (10 * iota)
	GB = 1 << (10 * iota)
	TB = 1 << (10 * iota)
	PB = 1 << (10 * iota)
)

const (
	a, b = iota + 1, iota + 2 //1,2
	c, d                      //2,3
	e, f                      //3,4
)

type ByteSize float64
const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)
变量

变量可以像常量一样被初始化,但是初始化值可以是在运行时进行计算的表达式:

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)
初始化函数

每个源文件都可以定义自己的 init函数来设置一些需要的状态,实际上可以写好几个(?)
首先等待所有引入的包被初始化,然后等待所有当前包的变量声明都被初始化,然后才调用 init函数
除了一些不能被表达为声明语句的初始化行为之外(我理解是,因为Go要求文件级的语句必须以关键字开头,有的初始化行为做不到),初始化函数的一个常用操作是,在真正的执行开始前检查或者修复程序状态

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

方法

指针绑定 VS 值绑定

方法可以可以绑定在任何已命名类型或接口上(除了指针和接口),接收者不一定是结构
(不能绑定接口和指针是因为这两者不是所谓的defined type?是吧,不知道为什么)

两种绑定的区别在于,指针绑定的方法只能用指针访问,值绑定则两者都可(意思是,如果真正严格按照定义方法,指针绑定需要用地址去调用,值绑定可以用值调用,也可以用地址调用,因为Go会自动对地址解引用,就是自动在前面加星号)
指针绑定的方法可以修改接收器(被绑定者)的属性,值绑定只能对接收器的值的复制进行操作,所以任何在值绑定方法内进行的修改都会被弃置。
对于指针绑定,如果这个对象是可寻址的,Go会自动添加取地址,相当于在前面自动加取地址符,这样使用指针绑定方法写法上更简洁(用值绑定的理由是什么?)

b.write // 指针绑定直接这样调方法本质是下面这样
(&b).write

接口和其他类型

接口

Go里的接口提供一种定义对象行为的途径: 你能做什么, 你就是什么
在Go中有很多只有一两个方法的接口, 通常被命名为由所提供方法的动作(verb动词)加 er 的施动者名词
一个类型可以实现多个接口

转换

Go里的一个行为是, 不创建新值地转换类型以使用另一套方法
实践中不常用但是可以很有效率
比如下面这样,如果你想有序打印,不转换类型的话,String方法写起来很长
但是转换的话效率会高

type Sequence []int
// 实现了sort接口(len,less,swap方法)
func (s Sequence) String() string {
    s = s.Copy() // Make a copy; don't overwrite argument.
    sort.Sort(s)
    str := "["
    for i, elem := range s { // Loop is O(N²); will fix that in next example.
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}
func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
    s = s.Copy()
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}
接口转换与类型断言

大意就是利用type switch对类型进行匹配转换,使用 type assertion 类型断言的语法 value.(typename)进行直接转换

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()
}

使用类型断言时,可以同时接收一个表示断言是否成功的ok变量
断言失败会返回需求类型的零值与值为false的状态

if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}
通用性

如果一个类型的存在只是为了得到某个接口值,并且除了导出这个接口值外没有别的目的,就没有必要在生成函数中返回这个类型本身(的值)。只返回接口值可以清楚地表明,除了返回这个接口值外,这个类型没有什么别的用处。
大概意思是,隐藏细节,返回接口值,来表示你希望别人只把你看作这个接口用就ok,(就不做自己,就要做别人)

「程序要依赖于抽象接口,不要依赖于具体实现」

比如,crc32.NewIEEE``adler32.New 这两种哈希算法,她们的存在都只是为了用某种算法算出一个32位hash值,具体算法细节无关紧要,就只需要在生成函数里返回一个实现了hash.Hash32接口的接口值,而不是返回她们本身,你只需要把她们看成类似hash.Hash32类型的构造函数之类的东西,这样你想求hash值就只需要调用这两种算法的生成函数

在一个,你可以用类型断言去判断,可能意思就是编程时多用抽象接口,具体实例可以用的时候再转换的意思吗(嗯黑听不懂)

接口和方法

几乎一切都可以绑定方法,几乎一切都可以实现接口
除了指针和接口不能

空白标识符

空白标识符 _可以被用来弃置任何类型的任何值

多赋值中的空白标识符

rangemap中使用空白标识符进行弃置实际上属于一个更通用的情况:多赋值,即在赋值语句左侧需要多个变量进行接收,但是其中之一需要被弃置, 就可使用该标识符代替,避免创建新的变量且清楚表明该值被弃置
一种应用场景是,调用了返回值和异常状态的函数时,如果只有异常状态是重要的,就把值弃置掉,但是弃置异常状态是一种不好的行为,你应该总是处理异常,因为异常状态往往是有目的提供的

未使用的引入和变量

引入未使用的包或声明未使用的变量属于异常,未使用的引入会让程序变得臃肿,拖慢编译速度,未使用的变量至少会导致不必要的计算,甚至可能代表更大的bug
但是如果程序正在开发中,未使用的引入和变量可能经常出现,为了通过编译每次把她们删掉之后再重写听起来就很烦人,空白标识符_可以用来解决这个问题,简单说就是你不是说没使用吗,那就使用一下,然后赋给空白标识符弃置掉
为了方便,未使用引入的空白占用应该就放在引入语句之下,并且注释一下来提醒之后删掉

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
}
特殊需求引入

有时可能为了一些特殊需求需要引入一些未命名的包,并不显式使用,只是为了一些附加功能,一样通过空白标识符完成
例子:

 For example, during its init function, the net/http/pprof package registers HTTP handlers that provide debugging information. It has an exported API, but most clients need only the handler registration and access the data through a web page. To import the package only for its side effects, rename the package to the blank identifier:
import _ "net/http/pprof"
接口检查

一个类型不需要显式声明她实现了某个接口,实现了这个接口的所有方法,就实现了这个接口
实践中,大多数接口转换是静态的因此在编译时可进行检查,比如给一个接收 实现了io.Reader的参数 的函数传一个
*os.File,如果*os.File没有实现io.Reader,编译就不能通过

但是也有一些接口检查发生在运行时(类型断言),

嵌入

Go没有典型的,类型驱动的子类的概念,但是可以通过嵌入来在一些结构或接口里借用一些实现
接口嵌入比较简单,注意只有接口可以被嵌入接口,接口嵌入只需要在 “更大的” 接口声明中加入 “小的” 接口的名称

// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}

同样的逻辑适用于结构,但是更复杂一点
结构嵌入结构(名字为son)时,可以为子结构命名(sonname)(这样叫组合?),但是调用子结构的方法就需要father.sonname.method()来调用
所以建议直接嵌入,不给子结构命名,这样子结构的所有方法会直接绑定在父结构上,但是,在调用子结构方法时,实际上还是调用子结构的方法,类似上面的带名字调用
(可以混合使用吗?)
直接嵌入时,有时你需要直接指代子结构的域,以明确调用子结构的方法,比如定义一个与子结构某一方法同名的方法(会覆盖掉子结构方法),可以直接用father.son.method调用,就是子结构的结构名

关于命名冲突问题,命名自上而下进行覆盖,外层命名覆盖内层命名
但是可能会出现同一层次命名冲突的问题,往往会导致异常,但是只要不用的话就OK,或者用的时候用全称去使用

type Job struct {
    Command string
    *log.Logger
}
func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

func (job *Job) Printf(format string, args ...interface{}) {
    job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

并发

通过交流共享

并发编程是一个很大的话题,文章只讲了一些Go独有的亮点
许多环境下的并发编程难点在于,正确获取共享变量的 实现细节。Go鼓励通过channels管道传递共享数据,事实上,从不由单独的执行线程主动共享。给定时间内只能有一个goroutine进行访问,从设计上避免了数据竞争。为了鼓励这种思考方式他们总结为一句标语:

不要通过共享内存进行通信;相反,通过通信共享内存。

Goroutines

之所以被称为goroutine是因为现有单词不能准确表达其含义,一个与其他goroutine在同一命名空间运行的函数,很轻量化,只需要分配堆栈空间,堆栈一开始很小,所以成本比较低,然后根据需要分配(和释放)堆存储来增长。

Goroutines可以复用在多个系统线程上,其中之一被堵塞时,其余继续执行。其设计隐藏了创建与管理线程的复杂性

在函数或方法调用前缀一个 go关键字来开启一个新goroutine,调用结束时,goroutines静默退出(效果类似在Unix shell希望后台执行命令时加的 &符号)

使用Goroutines调用函数字面量(类似匿名函数的东西)很方便
Go里的函数字面量是闭包,这样设计确保该函数引用的变量不会被回收

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // Note the parentheses - must call the function.
}

实际上这样写不常用,因为没有返回信号机制,所以用来配合使用channels

Channels

使用make创建,返回值是对依赖数据类型的引用,提供整数参数时设置为缓存的大小,缺省为0,表示无缓存的同步管道
接收者阻塞直到有数据可以接收,对于无缓存管道,发送者阻塞直到接收者获取到值,对于有缓存管道,发送者阻塞直到值被存入缓存中,缓存满了的话,就等待接收者获取值

缓存管道可以被用来当作信标,比如限制吞吐量

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    process(r)  // May take a long time.
    <-sem       // Done; enable next request to run.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // Don't wait for handle to finish.
    }
}

但是这样的话,如果需求来的太快,一下子创建大量grts等待运行,可能会消耗大量系统资源
所以可能需要控制grts的创建:

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
        }()
    }
}

或者限制grts数量

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

管道是可以被分配和传递的一级值,所以可以用来实现安全的并行的解复用
用来创建限制频率的,并行的,无阻塞的RPC系统,并且没有互斥锁(看不懂了)

并行化

进行多核之间的并行计算,如果该计算可以被分为好几个独立运行的部分的话,然后再用一个信号管道传递状态

type Vector []float64

// Apply the operation to v[i], v[i+1] ... up to v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // signal that this piece is done
}
const numCPU = 4 // number of CPU cores

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

这里设置了一个常量指定CPU数量,但是可以通过runtime.NumCPU() 获取或者runtime.GOMAXPROCS设置或获取并行计算所用的CPU数

但是谨记并发(组织各部分独立运行的程序)和并行(多CPU平行执行计算)是不同的概念,Go是一门并发语言,也许可以做一些并行,但是不是所有并行计算都适合

缓存泄露

异常

利用多返回值特性返回异常信息
应该尽可能利用这一点返回详细的异常信息
异常信息应该注明来源,比如前缀造成异常的包或操作
关注特定类型异常的处理细节的调用可以使用type switch或者type assertion查找特定异常并提取细节

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()  // Recover some space.
        continue
    }
    return
}
panic

返回异常给调用者时通常是返回一个异常状态值,但是当这个异常可能导致程序不能运行时,可以使用panic函数创建一个运行时异常来终止程序,该函数接收一个字符串并在程序结束后打印,可以用来表示一些不可能的事发生了,比如退出了无限循环
但是应尽可能避免使用 panic,如果问题能被跳过或解决的话,就不用终止程序
一个特例是初始化时:如果这个库真的初始化不了,使用panic是合理的

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}
recover

panic函数执行,包括隐式的执行(切片索引越界,类型断言失败)时,她会立即停止当前函数的运行,并去 展开 grts的堆栈,顺便执行所有延迟的函数,展开完成后程序就完全终止,但是可以使用内置函数recover重新控制grts并且继续正常执行程序
调用recover会停止展开并且返回传递给panic的参数,因为展开时唯一能执行的就是延迟的函数,recover只在延迟函数里才有用
recover的作用应该是在不关闭其他执行中的 grts的情况下关闭服务内异常的 grt

你可能感兴趣的:(Golang,golang,go,笔记,其他)