Go语言笔记(下) | 青训营

90 阅读7分钟

6.1 方法声明

func (p Point) Distance (q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// p 为方法的接收器

x := Point{1, 2}
y := Point{3, 4}
fmt.Println(Distance(x, y)) // 函数
fmt.Println(x.Distance(y)) // 方法

6.2 基于指针对象的方法

  • 声明
func (p *Point) ScaleBy (factor float64) {
	p.X *= factor
	p.Y *= factor
}
  • 使用
// 1
r := &Point{1, 2}
r.ScaleBy(2)
// 2
p := Point{1, 2}
p.ScaleBy(2)
  • 不管方法的接收器是指针还是非指针, 都可以直接通过指针或非指针进行调用, 会自动类型转换
  • 如果对象本身不是很大, 当接收器是非指针时, 会调用一次拷贝
  • 如果使用指针类型接收器, 始终指向内存地址

6.3 通过嵌入结构体来扩展类型

type Point struct {
	X, Y float64
}

type ColoredPoint struct {
	Point
	Color color.RGBA
}

  • 调用 Point 的方法
p := ColoredPoint{Point{1, 2}, red}
q := ColoredPoint{Point{3, 4}, blue}
p.Distance(q.Point) // 不能直接写q, 因为q不是Point类型
p.ScaleBy(2)

// 实现方式
func (p ColoredPoint) Distance (q Point) float64 {
	return p.Point.Distance(q)
}

6.4 方法值和方法表达式

  • 方法值
p := Point{1, 2}
q := Point{3, 4}
distanceFromP := p.Distance // 方法值
distanceFromP(q) // 相当于p.Distance(q)

time.AfterFunc(time.Second, func(){ r.Launch()})
time.AfterFunc (time. Second, r.Launch)
  • 方法表达式
p := Point{1, 2}
q := Point{3, 4}

distance := Point.Distance // 方法表达式
distance(p, q)

// 用处
// 当根据一个变量决定调用同一个类型的哪个函数时

func (path Path) TranslateBy(offset Point, add bool) {
    var op func(p, q Point) Point
    if add {
        op = Point.Add
    } else {
        op = Point.Sub
    }
    for i := range path {
        // Call either path[i].Add(offset) or path[i].Sub(offset).
        path[i] = op(path[i], offset)
    }
}

7.1 接口约定

  • 定义
type Writer interface {
	Write(p []byte) (n int, err error)
}
  • 实现
type ByteCounter int

func (c *ByteCounter) Write (p []byte) (int, error) {
	*c += ByteCounter(len(p))	
	return len(p), nil
}

7.2 接口类型

  • 接口可以像结构体一样嵌套
type ReadWriter interface {
	Reader
	Writer
}

7.3 实现接口的条件

  • 如果一个类型实现了接口中的所有方法, 那这个类型就实现了这个接口
  • 对于接口, 变量和指针要区分. 对于方法, 只是编译器隐式转换了
  • 接口类型对具体类型进行了封装和隐藏。即使具体类型有其他的方法,在使用该实例时,只有实现了接口类型暴露出来的方法才会被调用到,其他方法都会被隐藏。
os.Stdout.Write([]byte("hello")) // OK, *os.File has Write method
os.Stdout.Close() // OK, *os.File has Close method

var w io.Writer
w = os.Stdout
w.Write([]byte("hello"))
w.Close() // error
  • 空接口
var any interface{}
any = true
any = 1.2
any = "hello"
  • 断言

    • 类型断言
    var v interface{}
    // ...
    str, ok := v.(string) // v是接口值, ()是类型断言操作符, string是期望的类型
    
    • 接口断言
    var _ io.Writer = (*bytes.Buffer)(nil)
    // 把*bytes.Buffer类型的nil赋值给io.Writer类型的变量, 如果*bytes.BUffer没有实现io.Writer, 则编译时会报错
    

7.5 接口值

  • 由两部分组成, type 和 value
var w io.Writer
w = os.Stdout // type: *os.File, value: os.Stdout
  • 接口的比较
    • 动态类型相同且动态值可以比较, 那就可以比较
    • 动态类型相同但值不可以比较 (比如切片), 那比较就 panic

7.6 sort. Interface 接口

  • 接口定义
package sort

type Interface interface {
	Len() int
	Less (i, j int) bool // indices
	Swap(i, j int) 
}
  • 使用
sort.Sort(x)
sort.Sort(sort.Reverse(x))

7.8 error 接口

  • error 类型是接口类型
type error interface {
	Error() string
}
  • errors 包
package errors

func New(text string) error { return &errorString{text} }

// 使用结构体, 防止修改错误信息
type errorString struct { text string }

// 使用指针类型, 保证每个New函数都分配了一个独一无二的实例, errors.New("EOF") == errors.New("EOF") false
func (e *errorString) Error() string { return e.text }

7.10 类型断言

  • 第一种, 判断 x 的动态类型是否与具体类型 T 一致
var w io.Writer
w = os.Stdout
f := w.(*os.File) // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic
  • 第二种, 判断 x 的动态类型是否满足接口 T
var w io.Writer
w = os.Stdout // w的接口类型是io.Writer
rw := w.(io.ReadWriter) // rw 也是 os.Stdout, 但是接口类型是 io.ReadWriter

w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic, 因为*ByteCounter没有Read方法
  • 不 panic, 而是检验类型
var w io.Writer = os.Stdout
f, ok := w.(*os.File)
b, ok := w.(*bytes.Buffer)

7.13 类型分支

  • 原理
func sqlQuote(x interface{}) string {
    if x == nil {
        return "NULL"
    } else if _, ok := x.(int); ok {
        return fmt.Sprintf("%d", x)
    } else if _, ok := x.(uint); ok {
        return fmt.Sprintf("%d", x)
    } else if b, ok := x.(bool); ok {
        if b {
            return "TRUE"
        }
        return "FALSE"
    } else if s, ok := x.(string); ok {
        return sqlQuoteString(s) // (not shown)
    } else {
        panic(fmt.Sprintf("unexpected type %T: %v", x, x))
    }
}
  • 简化版本
switch x.(type) {
case nil:       // ...
case int, uint: // ...
case bool:      // ...
case string:    // ...
default:        // ...
}

8.1 Goroutines

  • go f ()

8.4 Channels

  • 创建 channel
ch := make(chan int)
  • 发送和接收
ch <- x
x = <-ch
<-ch
  • 关闭
close(ch)
  • 关闭 channel 后, 再向 ch 中发送会导致 panic, 但仍可以接收到之前成功发送的数据, 如果没有数据的话会产生一个零值数据
  • 判断 channel 是否被关闭
// 1
x, ok := <-ch
if !ok{
	break // closed
}

// 2 当ch被关闭并且没有值时会跳出循环
for x := range ch {
	fmt.Println(x)
}
  • 单方向 channel
ch1 := chan<- int // 只能发送
ch2 := <-chan int // 只能接收
  • 带缓存的 channel
ch := make(chan string, 3)
fmt.Println(cap(ch))
fmt.Println(len(ch))

8.5 并发的循环

  • 使用计数器来等待 goroutine 完成
func makeThumbnails3(filenames []string) {
    ch := make(chan struct{})
    for _, f := range filenames {
        go func(f string) {
            thumbnail.ImageFile(f) // NOTE: ignoring errors
            ch <- struct{}{}
        }(f)
    }
    // Wait for goroutines to complete.
    for range filenames {
        <-ch
    }
}
  • 并发中匿名函数要显式传参
// 错误方式, 因为传入goroutine中的是f的引用, 等goroutine开始执行并读取f时, 这个for循环可能已经结束了, 结果f就只是最后一个值
for _, f := range filenames {
	go func () {
		thumbnail.ImageFile(f)	
	}()
}
  • 使用计数器 sync. WaitGroup 为 goroutines 计数
func makeThumbnails6(filenames <-chan string) int64 {
    sizes := make(chan int64)
    var wg sync.WaitGroup // number of working goroutines
    for f := range filenames {
        wg.Add(1)
        // worker
        go func(f string) {
            defer wg.Done()
            thumb, err := thumbnail.ImageFile(f)
            if err != nil {
                log.Println(err)
                return
            }
            info, _ := os.Stat(thumb) // OK to ignore error
            sizes <- info.Size()
        }(f)
    }

    // closer
    go func() {
        wg.Wait()
        close(sizes)
    }()

	var total int64
	for size := range sizes {
		total += size	
	}
	return total
  • Add 和 Done 不对称, Add 在外部是为了确保 Add 在 closer goroutine 调用 wg. Wait ()之前调用
  • 为什么 closer 要创建新 routine:
    • 如果放在 main routine 当前位置, 将一直处于 wg. Wait ()状态, 因为无法到达 sizes 的循环, 无法接收通道, 形成阻塞
    • 如果放在 for 循环后面, 会一直在循环内部, 因为关闭循环的动作在后面了

8.7 基于 select 的多路复用

  • 语法
select {
case <-ch1:
    // ...
case x := <-ch2:
    // ...use x...
case ch3 <- y:
    // ...
default:
    // ...
}
  • 作用
    • 当需要从多个 channel 接收信息时, 如果直接接收的话, 当第一个 channel 中没有事件发过来时会形成阻塞
    • 多路复用, 每个 case 代表一个通信操作 (接收或发送)
  • 示例, 打印偶数
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
    select {
    case x := <-ch:
        fmt.Println(x) // "0" "2" "4" "6" "8"
    case ch <- i:
    }
}
  • 当多个 case 同时就绪时, 随机选择一个执行.
  • 利用 select 避免发送或接收导致的阻塞
select {
cast <- abort:
	fmt.Printf("Launch aborted\n")
	return
default:
	// do nothing
}

8.9 并发的退出

  • 当关闭了一个 channel 并浪费掉了所有已发送的值, 操作 channel 之后的代码可以立即被执行, 并且会产生零值
  • 广播机制, 利用关闭一个 channel 进行广播
var done = make(chan struct{})

func cancelled() bool {
   select {
   case <-done:
       return true
   default:
       return false
   }
}

func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
   defer n.Done()
   if cancelled() {
       return
   }
   for _, entry := range dirents(dir) {
       // ...
   }
}

10.2 导入路径

import (
    "fmt"
    "math/rand"
    "encoding/json"

    "golang.org/x/net/html"

    "github.com/go-sql-driver/mysql"
)

10.3 包声明

  • package rand
  • 每个 Go 语言源文件必须有包声明语句,用于确定当前包被其他包导入时默认的标识符。默认包名通常为导入路径名的最后一段,但有三种例外情况:main 包、测试的外部扩展包和依赖版本号的包。包名相同的包可以同时导入,但需要用不同的别名来区分。

10.4 导入声明

  • 导入包的重命名
import (
    "crypto/rand"
    mrand "math/rand" // alternative name mrand avoids conflict
)

10.5 包的匿名导入

  • 匿名导入 image/png
    • import _ "image/png"
  • 优点:
    • 侧效应导入: 不使用任何导出的函数变量和类型, 只使用 init 函数. 例如注册驱动, 添加插件
    • 模块化: 通过匿名导入来扩展 img 包的功能, 而不需要修改 image. Decode 的调用代码

10.7 工具

  • 工作区结构

    • GOPATH 工作区目录
    $ export GOPATH=$HOME/gobook
    $ go get gopl.io/...
    
    • GOPATH 子目录
      • $GOPATH/src 存储源代码
      • $GOPATH/pkg 保存编译后的包的目标文件
      • $GOPATH/bin 保存编译后的可执行程序
    • GOROOT 指定 Go 的安装目录以及标准包的位置
      • $GOROOT/src
      • $GOROOT/pkg
      • $GOROOT/bin
    • go env 查看环境变量的值
  • 下载包

    $ go get github.com/golang/lint/golint
    
    • go get 获得的是本地存储仓库, 可以使用 git 比较
    • 默认只会获得本地不存在的包
    • -u 会保证每个包都是最新的
  • 构建包 go build 如果包是一个库, 则忽略输出结果, 可以检测包是否可以被正确编译. 如果包的名字是 main 则会在当前目录创建可执行程序.

    • 默认指定
      $cd $GOPATH/src/gopl.io/ch1/helloworld
      $go build
      
    • 绝对路径
      $cd anywhere
      $go build gopl.io/ch1/helloworld
      
    • 相对路径
      $cd $GOPATH
      go build ./src/gopl.io/ch1/helloworld # 必须以.或者..开头
      
  • 包文档 go doc

    • 函数
    • 方法
  • 内部包

    • 包含 internal 名字的路径段的包就是内部包
    • 一个内部包只可以被和 internal 的父目录下的包导入
    net/http
    net/http/internal/chunked
    net/http/httputil
    net/url
    
  • 查询包

    • go list
    • 对于一次性的交互查询或自动化构建或测试脚本有帮助