美文网首页一日一学Go语言我的专题
一日一学_Go从错误中学习基础二

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

作者: WuXiao_ | 来源:发表于2017-03-02 23:33 被阅读483次

上一篇(一日一学_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中的地址引用了原始数据块。
总结: 将一个值类型作为一个参数传递给函数或者作为一个方法的接收者,似乎是对内存的滥用,因为值类型一直是传递拷贝。但是另一方面,值类型的内存是在栈上分配,内存分配快速且开销不大。因此当使用指针代替值类型作为参数传递时,需要根据自己需求来使用。
接下来我以切片理解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从错误中学习基础一)讲了部分Golang容易出错地方,为了让读者清晰学习,我决定分开。 ne...

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

    在写Go代码时,多少会出一些错误,我把这些常见错误整理出来。一是再次让自己重新认识Golang,进行不足的学习。二...

  • Go 并发编程:错误处理及错误传递

    一、协程错误管理 我们在基础系列讲过Go程序开发中的错误处理规范,展示了几种函数执行中的错误返回问题,而在Go并发...

  • go 基础学习

    1 go 基础go 语法基础go 官方资料如果由C ,C++ 基础, 学习go 比较容易,本文学习go ,主要是为...

  • Go语言探索 - 3(原创)

    Go语言基础系列博客用到的所有示例代码 在上一篇文章中,我们主要学习了Go语言的编程基础。这些基础内容包括注释、分...

  • Go语言探索 - 12(结局)

    Go语言基础系列博客用到的所有示例代码 上一篇文章文章主要学习了Go语言中的接口、反射以及错误和异常处理。本篇文章...

  • Golang 学习笔记八 错误异常

    一、错误异常 《快学 Go 语言》第 10 课 —— 错误与异常Go 语言的异常处理语法绝对是独树一帜,在我见过的...

  • go语言学习-从基础到实战到源码分析

    收集的一些go语言学习资料,有go基础学习系列,go项目实战,go进阶-go源码分析,还有go的一些书籍,go的架...

  • go学习一·常量constant, iota

    本系列记录的是本人第二次学习go语言的经验,所以如果对于go一点都不了解的可以先去认真的过一遍go的基础,基础教程...

  • go学习五·切片

    本系列记录的是本人第二次学习go语言的经验,所以如果对于go一点都不了解的可以先去认真的过一遍go的基础,基础教程...

网友评论

  • aadfaffd6fde:博主这句话是什么意思:“如果你传递一个指针,而不是一个值类型,go编译器大多数情况下会认为需要创建一个对象,并将对象移动到堆上,所以会导致额外的内存分配:因此当使用指针代替值类型作为参数传递时,需要根据自己需求来使用。”,我的理解是如果传递指针,就会创建一个新的结构体Person{}(例子中)?但是我试了一下,没有创建,如代码:
    package main

    import "fmt"

    func main() {
    p2 := &Person{"wuxiao", 15}
    fmt.Println(&p2)//打印地址
    setName1(p2)
    fmt.Println(&p2)//打印传值以后的地址

    }

    type Person struct {
    name string
    age int
    }

    func setName1(p *Person) {
    p.name = "dabai"
    }
    打印结果是:
    0xc082024020
    0xc082024020
    一样啊,没变,也就是没有创建新的对象啊?不知道我有没有理解错你的意思?
    WuXiao_: @番薯粉v 😂被误解了,我的意思是想表明,有的时候传递值类型的效果要比传指针指向堆内存好很多,值类型是在栈中,回收要比堆快得多
  • 40a08830d6ac:这并发感觉理解好难:smile:
    WuXiao_: @曹江华Arden 😂刚开始认为他是线程就可以了,慢慢深入学习,认识

本文标题:一日一学_Go从错误中学习基础二

本文链接:https://www.haomeiwen.com/subject/vhfxgttx.html