go学习笔记2

| 分类 go  | 标签 go  golang  interface  浏览次数: -

接口值

有两部分组成

  • 具体的类型,也称接口的动态类型
  • 那个类型的值,也称接口的动态值

变量总是被一个定义明确的值初始化,接口类型也不例外。

对于一个接口的零值就是它的类型和值的部分都是nil

package main

import (
    "io"
    "fmt"
    "os"
    "bytes"
)

func main() {
    var w io.Writer       // interface 不管 `w` 保存了什么值,它的类型都是 `io.Writer`
    fmt.Printf("%T\n", w) // print nil

    w = os.Stdout
    fmt.Printf("%T\n", w)      // print *os.File
    w.Write([]byte("hello\n")) // print hello

    w = new(bytes.Buffer)
    fmt.Printf("%T\n", w) // print *bytes.Buffer

    b := new(bytes.Buffer) // struct
    fmt.Printf("%T\n", b)  // print *bytes.Buffer
    b.Write([]byte("world"))
    fmt.Println(b.String()) // print world
}

接口值可以使用==和!=来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。

然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic.

interface接口

package main

import (
    "fmt"
    "strings"
)

type Interface interface {
    Len() int
    Less(i, j int) bool // i小于j的比较结果
    Swap(i, j int)
}

type StringSlice []string

func (p StringSlice) Len() int {
    return len(p)
}

func (p StringSlice) Less(i, j int) bool {
    return p[i] < p[j]
}

func (p StringSlice) Swap(i, j int) {
    p[i], p[j] = p[j], p[i]
}

func main() {
    var names = StringSlice{"Python", "Go", "Java", "C"}

    fmt.Println(strings.Join(names, " "))
    for i := 0; i < names.Len()-1; i++ {
        if names.Less(i, i+1) {
            names.Swap(i, i+1)
        }
    }
    for _, item := range names[:names.Len()-1] {
        fmt.Println(item)
    }
    fmt.Println(strings.Join(names, " "))
}

实现排序的基础三个方法Len, Less, Swap

类型断言

类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

第一种

如果断言的类型T是一个具体类型,具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败,接下来这个操作会抛出panic.

第二种

如果断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。如果这个检查成功了,动态值没有获取到;这个结果仍然是一个有相同类型和值部分的接口值,但是结果有类型T。换句话说,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保护了接口值内部的动态类型和值的部分。

package main

import (
    "fmt"
    "io"
    "os"
    "bytes"
)

func main() {
    var w io.Writer

    w = os.Stdout

    f := w.(*os.File)
    fmt.Println(f)
    
    c, ok := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
    fmt.Println(c)
    fmt.Println(ok)

    var v interface{}
    v = 1234
    switch v.(type) {
    case string:
        fmt.Printf("The string is %s", v.(string))
    case int, uint, int8:
        fmt.Printf("The integer is %d", v)
    default:
        fmt.Printf("Unknown value: type=%T", v)
    }
}

接口设计

  • 1.接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要
  • 2.当一个接口只被一个单一的具体类型时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在相同的包中,这种情况下,一个接口是解耦这两个包的一个好方式。

在Go语言中只有两个或更多的类型实现一个接口时才使用接口,它们必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法的更小的接口。当新的类型出现时,小的接口更容易满足。

接口设计一个好的标准就是: ask only for what you need(只考虑你需要的东西)

Goroutines

主函数返回时,所有的goroutine都会被直接打断,程序退出。 除了从主函数退出或者直接终止程序外,没有其它编程方法能够让一个goroutine来打断另个一个的执行。

Channels

一个channel是一个通信机制,gorotine之间可以通过channel进行通信。

一个channel有发送和接收两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。

发送和接收操作都是使用<-运算符。在发送语句中,<-分割channel和要发送的值。在接收语句中,<-写在channel对象之前。一个不使用接收结果的接收操作也是合法的。

ch <- x // 发送一个值
y := <- ch // 接收一个值
<-ch //将要接收的值丢弃

channel支持close操作,用于关闭channel,关闭后对该channel的任何发送操作都会导致panic异常。

对于一个已经被close过的channel进行接收操作依然可以接收到之前已经成功发送的数据,没有数据则产生一个零值的数据。

无缓存的channels

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的channels上执行发送操作。

基于无缓存channels的发送和接收操作将导致两个goroutine做一次同步操作。所以无缓存channels也被称为同步channels。

range 可以处理channel没有值时退出循环,和python中的for循环可以处理 StopIteration 类似。

试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制.

单方向的channels

当一个channel参数作为一个函数参数时,它一般总是被专门用于只发送只接收

  • chan <- int: 表示一个只发送int的channel,只能发送,不能接收
  • <- chan int: 表示一个只接收int的channel,只能接收,不能发送

因为关闭操作只用于断言不再想channel中发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。

任何双向channel向单向channel变量的赋值操作都将导致该隐式转换。这里并没有反向转换的语法:也就是不能将一个类似 chan<- int 类型的单向型的channel转换为 chan int 类型的双向型的channel。

带缓存的channels

带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。

向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。

channel的功能和C中链表是一致的,区别在于Go中的语法更加简单。

Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用,虽然语法看似简单,但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的,一个发送操作——或许是整个程序——可能会永远阻塞。如果你只是需要一个简单的队列,使用slice就可以了。

goroutines泄露: goroutines因为没有人接收而被永远卡住

关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。同样,即使我们知道将要发送到一个channel的信息的数量上限,创建一个对应容量大小的带缓存channel也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓冲将导致程序死锁。

select

switch 语句有点类似,可以有多个 case 和最后的 default 选择支。

每一个case代表一个通信操作(在某个channel上进行发送或接收)并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身,或者包含在一个简短的变量声明中。

select {
case <-ch1: // 接收操作,把返回值丢弃
    // ...
case x := <-ch2:  // 接收并把返回值赋给 x
    // ...use x...
case ch3 <- y: // 发送操作
    // ...
default: // 默认操作
    // ...
}

select 会等待 case 中有能够执行的 case 时去执行。当有条件满足时,select才会去通信并执行 case 之后的语句,此时其它通信是不会执行的(若有default则在其它case条件都不满足时会执行)。一个没有任何case的select语句写作 select{} ,会一直等待下去。

当有多个 case 就绪时,select 会随机地选择一个执行,这样来保证每个channel都有平等的被select的计划。当channel的buffer既不为满也不为空时,select语句的执行情况就像是抛硬币的行为一样是随机的。

有时候我们希望能够从channel中发送或者接收值,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。select会有一个default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。

下面的select语句会在abort channel中有值时,从其中接收值;无值时什么都不做。这是一个非阻塞的接收操作;反复地做这样的操作叫做 轮询channel

select {
case <-abort:
    fmt.Printf("Launch aborted!\n")
    return
default:
    // do nothing
}

channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。

context

在go语言中context允许传递一个”context”到我们的程序中。Context如超时或截止日期(deadline)或通道,来指示停止运行和返回。

例如,一个web请求或运行一个系统命令,定义一个超时是非常有必要的。

创建context

  • ctx := context.Background()

创建一个空context,这只能用于高级(在main或顶级请求处理中)、

  • ctx := context.TODO()

这个函数也是创建一个空context,也只能用于高等级或当不确定使用什么context,或函数以后会更新以便接收一个context。意味着计划将来添加context到函数。

静态分析工具可以使用它来验证context是否正确传递。

  • ctx := context.WithValu(context.Background(), key, “test”)

此函数接收context并返回派生context,其中值val与key关联,并通过context树与context一起传递。这意味着一旦获得带有值的context,从中派生的任何context都会获得此值。不建议使用context值传递关键参数,而是函数应接收签名中的那些值,使其显示化。

  • ctx, cancel := context.WithCancel(context.Background())

返回派生 context 和取消函数。只有创建它的函数才能调用取消函数来取消此 context。如果您愿意,可以传递取消函数,但是,强烈建议不要这样做。这可能导致取消函数的调用者没有意识到取消 context 的下游影响。可能存在源自此的其他 context,这可能导致程序以意外的方式运行。简而言之,永远不要传递取消函数。

  • ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))

此函数返回其父项的派生 context,当截止日期超过或取消函数被调用时,该 context 将被取消。例如,您可以创建一个将在以后的某个时间自动取消的 context,并在子函数中传递它。当因为截止日期耗尽而取消该 context 时,获此 context 的所有函数都会收到通知去停止运行并返回。

  • ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)

此函数类似于 context.WithDeadline。不同之处在于它将持续时间作为参数输入而不是时间对象。此函数返回派生 context,如果调用取消函数或超出超时持续时间,则会取消该派生 context。

<-ctx.Done() 一旦 Done 通道被关闭,这个 <-ctx.Done(): 被选择。一旦发生这种情况,此函数应该放弃运行并准备返回。这意味着您应该关闭所有打开的管道,释放资源并从函数返回。有些情况下,释放资源可以阻止返回,比如做一些挂起的清理等等。在处理 context 返回时,您应该注意任何这样的可能性。

缺陷

如果函数接收 context 参数,确保检查它是如何处理取消通知的。例如,exec.CommandContext 不会关闭读取管道,直到命令执行了进程创建的所有分支(Github 问题:https://github.com/golang/go/issues/23019 ),这意味着如果等待 cmd.Wait() 直到外部命令的所有分支都已完成,则 context 取消不会使该函数立即返回。如果您使用超时或截止日期,您可能会发现这不能按预期运行。如果遇到任何此类问题,可以使用 time.After 实现超时。

最佳实践

  • 1.context.Background 只应用在最高等级,作为所有派生 context 的根。
  • 2.context.TODO 应用在不确定要使用什么的地方,或者当前函数以后会更新以便使用 context。
  • 3.context 取消是建议性的,这些函数可能需要一些时间来清理和退出。
  • 4.context.Value 应该很少使用,它不应该被用来传递可选参数。这使得 API 隐式的并且可以引起错误。取而代之的是,这些值应该作为参数传递。
  • 5.不要将 context 存储在结构中,在函数中显式传递它们,最好是作为第一个参数。
  • 6.永远不要传递不存在的 context 。相反,如果您不确定使用什么,使用一个 ToDo context。
  • 7.Context 结构没有取消方法,因为只有派生 context 的函数才应该取消 context。

参考


上一篇 测试驱动的面向对象软件开发读书笔记     下一篇 Oracle更换磁盘步骤
目录导航