一日一学_Go从错误中学习基础二

静静看动漫的武晓 · · 855 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

上一篇(一日一学_Go从错误中学习基础一)讲了部分Golang容易出错地方,为了让读者清晰学习,我决定分开。

new()与make()使用

数组、结构体和所有的值类型都可以使用new,切片、映射和通道,使用make。


多么简单的概念


为了能更深刻的理解,不混淆使用new和make下面开展内存的分析,防止跳坑

type Person struct {
    name string
    age  int
}

func main() {
    p1 := Person{"wuxiao", 10}
    p2 := &PersonP{"wuxiao", 15} // == new(Person)
}

p1内存状态图


可以看出p1在内存中是以连续的内存块存在


p2内存状态图

十六进制值表示指针地址0x ..,并引用实际数据。
在一些博客中,经常提到如下例子:

p1 := Person{"wuxiao", 10}

func setName(p Person) {
    p.name = "xiaobai"
}

上面code进行了值拷贝


值拷贝
p2 := &PersonP{"wuxiao", 15}

func setName(p *Person) {
    p.name = "dabai"
}

指针拷贝


从上面分析看出,第一种情况在函数setName() 中修改p不会修改原始数据,但是在第二种情况下肯定会修改,因为存储在p中的地址引用了原始数据块。
总结: 将一个值类型作为一个参数传递给函数或者作为一个方法的接收者,似乎是对内存的滥用,因为值类型一直是传递拷贝。但是另一方面,值类型的内存是在栈上分配,内存分配快速且开销不大。如果你传递一个指针,而不是一个值类型,go编译器大多数情况下会认为需要创建一个对象,并将对象移动到堆上,所以会导致额外的内存分配:因此当使用指针代替值类型作为参数传递时,需要根据自己需求来使用。
接下来我以切片理解make。
我们创建一个具有6个元素的底层数组的切片,使用make([] int,len,cap)语法来指定容量。如下:

    arr = make([]int, 5, 6)
    arr[3] = 44
    arr[4] = 333

切片内存状态


我们创建另一个子切片并更改一些元素,会发生什么?

        arr := make([]int, 5, 6)
    arr[3] = 44
    arr[4] = 333
    subArr := arr[1:4]
    subArr[1] = 77

切片


修改了subArr同样修改了底层数组,这就是为什么在Golang中切片使用广泛的原因。

在使用内置函数append()对切片进行添加元素时,但在内部它做了很多复杂的工作,来进行内存分配。

    arr := make([]int, 5, 6)
    subArr := arr[:]
    arr = append(arr, 1, 2)

使用append() 时会检查该切片是否有未使用的容器个数,如果没有,则分配更多的内存。 分配内存是一个相当昂贵的操作,因此append尝试对该操作进行预估,一次增加原始容量的两倍。 一次分配较多的内存通常比多次分配较少的内存更高效和更快。
分配更多的内存通常意味着分配新内存并从旧数组拷贝数据到新数组(导致地址值的改变)。


append后的内存变化

可以看出会有两个不同的底层数组,这对初学者来说可能不经意中出错。

协程与通道使用

协程与通道我认为是Golang的核心,为了能大家通俗易懂了解,防止在编写代码出错,我接下来会以一些例子来讲解核心部分。


看这里重点

无缓冲与有缓冲channel有什么区别?
一个是同步。
一个是非同步。
简单点理解:
无缓冲 使用通道发送数据,必须有此通道类型的协程接收数据,才能继续发送数据,要不然进入永久阻塞(死锁)。

有缓冲 如果缓冲大小是1,在通道发送数据时,只有当放第二个值的时候,第一个还没被人拿走,这时候才会阻塞。

下面三个例子更好说明了同步与非同步:

    data := make(chan string)

    //因为data没有值,所以会选择default执行(发送接收数据都
           //是一样的道理),这里就达成看一个无阻塞的效果。
    select {
    case msg := <-data:
        fmt.Println("received data", msg)
    default:
        fmt.Println("no data.....")
    }

执行结果:no data.....

     data := make(chan string, 1) //给通道加上缓冲的话,会怎么选择?
     data <- "wuxiao"

    //猜测:data已经有值,所以会选择 received data 执行。
    select {
    case msg := <-data:
        fmt.Println("received data", msg)
    default:
        fmt.Println("no data.....")
    }

执行结果:received data wuxiao

  • 如上面所说,在缓冲未装满时,给一个带缓冲的缓存发送数据是不会阻塞的,而从缓冲读取数据也不会阻塞。
    data := make(chan string)//没有缓冲了
    data <- "wuxiao" //因为该channels没有缓冲,发送数据,导致死锁(有的书上写为永远阻塞)

        select {
    case msg := <-data:
        fmt.Println("received data", msg)
    default:
        fmt.Println("no data.....")
    }

执行结果: fatal error: all goroutines are asleep - deadlock!
上面为什么会报错?
官方解释到: Unbuffered channels combine communication—the exchange of a value—with synchronization—guaranteeing that two calculations (goroutines) are in a known state
无缓冲的信道进行通信,保证两个协程处于已知状态。

流水线模式
开发中我们可以使用协程与通道达到并发的效果,而且还可以根据自己需求使用并发设计模式来提高效率,并发设计模式有扇入和扇出,流水线等。
流水线模式可以简单理解由多个阶段组成的,相邻的两个阶段由 channel 进行连接;每个阶段是都有自己goroutine 。每个阶段都会执行下面三个操作(除了第一个和最后一个阶段):

1. 通过 channel 接收数据上流的数据
2. 对接收到的数据进行操作
3. 将新生成的数据通过 channels 发送数据给下游

显然,第一个阶段只有发送管道,而最后一个阶段只有接收管道.
通常称第一个阶段可以理解为"生产者",称最后一个阶段理解为"消费者"。

//第一阶段是为gen 函数,首先启动一个 goroutine,
//通过goroutine 把数字发送到 channel,当数字发送完时关闭channel。
func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}
//第二阶段是 sq 函数,它从第一阶段返回的通道来接受整数,把
//所接收的整数发送自己创建的通道中并返回给下游,并且等第一
//阶段通道数字全部发给下游会关闭了上流通道,然后在关闭自己创建的管道。
func sq(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

//main 函数为流水线的最后一个阶段。
//会从第二阶段接收数字,并逐个打印出来,直到来自于上游的接收管道关闭
func main() {
        //由于 sq 函数的channel 类型一样,所以组合任意个 sq 函数
    for n := range sq(sq(gen(2, 4, 5)) ){
        fmt.Println(n)
    }
}

如果我把上面最后一个阶段改成

    out := range sq(sq(gen(2, 4, 5)) )
    fmt.Println(<-out) // 16 or 256 , 625

这里存在资源泄漏。一方面goroutine 消耗内存和运行时资源,另一方面goroutine 栈中的堆引用会阻止 gc 执行回收操作。 既然goroutine 不能被回收,那么他们必须自己退出。
那么如何解决这个问题?
使用显式取消。
在Go语言中,我们可以通过关闭一个channel 实现,因为在一个已关闭 channel 上执行接收操作数据总是能够立即返回,返回值是对应类型的零值。
简单讲,对一个管道的关闭操作事实上是对所有接收者进行广播信号。

func main() {
    // 当关闭 done channel时
    //给所有 goroutine发送信号
    // 接收到后都会正常退出。
    done := make(chan struct{})
    defer close(done)

    in := gen(done, 2, 4, 5)
    c1 := sq(done, in)
    out := sq(done, c1)
    fmt.Println(<-out) 
}
func sq(done <-chan struct{}, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * n:
            case <-done:
                return
            }
        }
    }()
    return out
}

其实讲上面的模式就是为了,提醒我们使用协程和通道的时候小心资源泄漏后,
发送完数据以后进行close关闭,以防死锁。
其他一些模式感兴趣可以点击查看模式学习地址一模式学习地址二

并发这块我还很多不足,希望大家能多多讨论.共同进步


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:静静看动漫的武晓

查看原文:一日一学_Go从错误中学习基础二

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

855 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传