Go 语言 在线

2228Go 语言函数闭包

Golang 匿名函数和闭包

Go语言支持匿名函数 即函数可以像普通变量一样 使用

 

#main.go

package main

import (

    "fmt"

)

func main() {

   //两种写法

    var v func(a int) int

    v = func(a int) int {

        return a * a

    }

    fmt.Println(v(6)) //36    

    

    v1 := func(i int) int {

        return i * i

    }

    fmt.Println(v1(7))//49

}

GO语言 匿名函数就是闭包 《GO语言编程》 对闭包的解释

基本概念

闭包是可以包含自由(未绑定到特定对象)变量的代码块 这些变量不在这个代码块内 或者 任何全局上下文中定义 而是在定义代码块的环境中定义 要执行的代码块(由于自由变量包含

在代码块中 所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)

闭包的价值

在于可以作为函数对象 或 匿名函数 对于类型系统而言 这意味着不仅要表示 数据还要表示代码 支持闭包的多数语言都将函数作为第一级对象 就是说这些函数可以存储到

变量中作为参数传递给其他函数 能够被函数动态创建和返回

一个函数和与其相关的引用环境 组合而成的实体

package main

import "fmt"

func main() {

    var f = Adder()

    fmt.Println(f(1), "-")//1 -

    fmt.Println(f(20), "-")//21 -

    fmt.Println(f(300), "-")//321 -

}

 

func Adder() func(int) int {

    var x int

    return func(delta int) int {

        x += delta

        return x

    }

}

测试文件后缀的闭包  

package main

import (

    "fmt"

    "strings"

)

func makeSuffix(suffix string) func(string) string {

    return func(name string) string {

        if strings.HasSuffix(name, suffix) == false {

            return name + suffix

        }

        return name

    }

}

func main() { //判断字符串 以bmp结尾

    f1 := makeSuffix(".bmp")

    fmt.Println(f1("test"))

    fmt.Println(f1("pic"))

    f2 := makeSuffix(".jpg")

    fmt.Println(f2("test"))

    fmt.Println(f2("pic"))

}

2227Go 并发

Go语言从语言层面上 支持 并发

 

Go语言 goroutines 信道 和 死锁

Go语言 有个 goroutine 概念  类似 线程 但是更轻

串行 执行两次loop函数

func loop() {

    for i := 0; i < 10; i++ {

        fmt.Printf("%d " i)

    }

}

func main() {

    loop()

    loop()

}

// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

把 loop 放在 goroutine 里跑 使用 go 定义 启动一个goroutine

func main() {

    go loop() // 启动一个goroutine

    loop()

}

//0 1 2 3 4 5 6 7 8 9

明明 主线跑了一趟  goroutine 为什么只输出了一趟呢?

原来 在 goroutine  没来得及跑loop 主函数已经退出了

我们要想办法阻止 main函数 过早地退出

func main() {

    go loop()

    loop()

    time.Sleep(time.Second) // 停顿一秒 让main等待一下

}

目的达到了 可办法并不好

如果goroutine 结束 时 告诉下主线说“Hey 我跑完了!” 就好了  即所谓阻塞主线

信道是什么?

简单说 是 goroutine 间互相通讯的东西 类似 Unix 管道(可以在进程间传递消息)  用来 goroutine 间发/接收消息 就是在做 goroutine 间的内存共享

使用make来建立一个信道

var channel chan int = make(chan int)// 或

channel := make(chan int)//just oney in func ,otherwise non-declaration statement outside function body

如何向 信道存/取消息呢

func main() {

   messages := make(chan string)

    go func(message string) {

        messages <- message // 存消息

    }("Ping!")

    fmt.Println(<-messages) // 取消息 Ping!

}

默认的 信道的存消息和取消息都是阻塞的 (叫  无缓冲 信道 )

无缓冲 信道在取消息和存消息的时候 会挂起当前 goroutine 除非另一端已经准备好

main函数和 foo 函数

var ch chan int = make(chan int)

func foo() {

    ch <- 0  // 向 ch 加数据 如果没有其他goroutine来取走这个数据 那么挂起foo 直到main函数把0这个数据拿走

}

func main() {

    go foo()

    <- ch // 从 ch 取数据 如果ch中还没放数据 那就挂起 main线 直到 foo 函数中放数据为止

}

既然信道可以阻塞当前 goroutine 如何让goroutine告诉主线 执行完毕了 使用一个信道来告诉主线即可

var complete chan int = make(chan int)

///complete := make(chan int)//non-declaration statement outside function body

func loop() {

    for i := 0; i < 10; i++ {

        fmt.Printf("%d ", i)

    }

    complete <- 0 // 执行完毕了 发个消息

}

func main() {

    go loop()

    <- complete // main在此阻塞住 // 直到线程跑完  取到消息

    fmt.Printf("ok" )

}

如果不用信道来阻塞主线的话 主线就会过早跑完 loop线都没有机会执行   

无缓冲的信道永远不会存储数据 只负责数据的流通 为什么这么讲呢?

从无缓冲信道取数据 必须要有数据流进来才可以 否则当前线阻塞

数据流入无缓冲信道 如果没有其他goroutine来拿走这个数据 那么当前线阻塞

所以 可以测试下 无论如何 测试到的无缓冲信道的大小都是0 (len(channel))

如果信道正有数据在流动 还要加入数据 或者信道干涩 一直向无数据流入的空信道取数据 就会引起死锁

死锁

func main() {

    ch := make(chan int)

    <- ch // 阻塞main goroutine 信道c被锁

}

执行这个程序 报这样的错误 fatal error: all goroutines are asleep - deadlock!

何谓死锁

所有的线程 或 进程都在等待资源的释放 只有一个 goroutine 所以当 向里面加数据或者存数据 都会锁死信道  并且阻塞当前 goroutine

也就是所有的 goroutine(其实就main线一个) 都在等待信道的开放(没人拿走数据信道是不会开放的) 也就是死锁咯

只在单一的 goroutine 里操作无缓冲信道 一定死锁 比如 只在main函数里操作信道

func main() {

    ch := make(chan int)

    ch <- 1 // 1流入信道 堵塞当前线 没人取走数据信道不会打开

    fmt.Println("This line code wont run") //在此行执行之前Go就会报死锁

}

如下也是一个死锁的例子

var ch1 chan int = make(chan int)

var ch2 chan int = make(chan int)

func say(s string) {

    fmt.Println(s)

    ch1 <- <- ch2 // ch1 等待 ch2流出的数据

}

func main() {

  go say("hello")

  <- ch1  // 堵塞主线

}

其中主线等 ch1 中的数据流出 ch1等ch2的数据流出 但是ch2等待数据流入 两个goroutine都在等 也就是死锁

为什么会死锁?

这样理解 Go启动的所有goroutine里的 非缓冲信道 一定要

一个线里存数据

一个线里取数据

要成对才行

所以下面的示例一定死锁:

c, quit := make(chan int), make(chan int)

go func() {

   c <- 1  // c通道的数据没有被其他goroutine读取走 堵塞当前goroutine

   quit <- 0 // quit始终没有办法写入数据

}()

<- quit // quit 等待数据的写

是由于 主线等待quit信道的数据流出 quit等待数据写入 而func被c通道堵塞 所有goroutine都在等 所以死锁

简单来看的话 一共两个线 func线中流入c通道的数据并没有在main线中流出 肯定死锁

但是 是否果真 所有不成对向信道存取数据的情况都是死锁?

func main() {

c := make(chan int)

go func() {

   c <- 1

}()

}

程序正常退出了 很简单 并不是我们那个总结不起作用了 还是因为一个让人很囧的原因 main又没等待其它goroutine 自己先跑完了

所以没有数据流入c信道 一共执行了一个goroutine 并且没有发生阻塞 所以没有死锁错误

那么死锁的解决办法呢?

最简单的 把没取走的数据取走 没放入的数据放入  因为无缓冲信道不能承载数据 那么就赶紧拿走!

具体来讲 就死锁例子3中的情况 可以这么避免死锁:

c quit := make(chan int) make(chan int)

go func() {

    c <- 1

    quit <- 0

}()

<- c // 取走c的数据!

<-quit

另一个解决办法是缓冲信道 即设置c有一个数据的缓冲大小:

c := make(chan int, 1)

这样的话 c可以缓存一个数据 也就是说 放入一个数据 c并不会挂起当前线 再放一个才会挂起当前线直到第一个数据被其他goroutine取走 也就是只阻塞在容量一定的时候 不达容量不阻塞

无缓冲信道的数据进出顺序

我们已经知道 无缓冲信道从不存储数据 流入的数据必须要流出才可以

观察以下的程序

var ch chan int = make(chan int)

func foo(id int) { //id: 这个routine的标号

    ch <- id

}

func main() {

    // 开启5个routine

    for i := 0; i < 5; i++ {

        go foo(i)

    }

    // 取出信道中的数据

    for i := 0; i < 5; i++ {

        fmt.Print(<- ch)

    }

}

开了5个goroutine 然后又依次取数据 其实整个的执行过程细分的话 5个线的数据

依次流过信道ch main打印之 而宏观上我们看到的即 无缓冲信道的数据是 先到先出 但是 无缓冲信道并不存储数据 只负责数据的流通

缓冲信道

缓存信道 buffered channel

缓冲信道 不仅可以流通数据 还可以缓存数据 是有容量的 存入一个数据的话  可以先放在信道里 不必阻塞当前线而等待该数据取走

当缓冲信道达到满的状态的时候 就会表现出阻塞了 因为这时再也不能承载更多的数据了 「你们必须把 数据拿走 才可以流入数据」

在声明一个信道的时候 给make以第二个参数来指明它的容量(默认为0 即无缓冲)

var ch chan int = make(chan int, 2)// 写入2个元素都不会阻塞当前goroutine 存储个数达到2的时候会阻塞

如下的例子 缓冲信道ch可以无缓冲的流入3个元素

func main() {

    ch := make(chan int, 3)

    ch <- 1

    ch <- 2

    ch <- 3

}

如果 再试图流入一个数据的话 信道ch会阻塞main线 报死锁

也就是说 缓冲信道会在满容量的时候加锁

其实 缓冲信道是先进先出的 我们可以把缓冲信道看作为一个线程安全的队列:

func main() {

    ch := make(chan int 3)

    ch <- 1

    ch <- 2

    ch <- 3

    fmt.Println(<-ch) // 1

    fmt.Println(<-ch) // 2

    fmt.Println(<-ch) // 3

}

信道数据读取和信道关闭

上面的代码一个一个地去读取信道简直太费事了 Go语言允许 使用range来读取信道

func main() {

    ch := make(chan int,3)

    ch <- 1

    ch <- 2

    ch <- 3

    for v := range ch {

        fmt.Println(v)

    }

}//fatal error: all goroutines are asleep - deadlock!

报死锁错误的 原因是 range 不等到信道关闭 是不会结束读取的 也就是如果 缓冲信道干涸了 那么range就会阻塞当前 goroutine 所以死锁咯

试着避免这种情况 比较容易想到的是读到信道为空的时候就结束读取

func main() {

ch := make(chan int,3)

ch <- 1

ch <- 2

ch <- 3

for v := range ch {

    fmt.Print(v,",")

    if len(ch) <= 0 { // 如果现有数据量为0 跳出循环

        break

    }

}

}//1,2,3,

 注意检查信道大小的方法 不能在信道 存 取 都在发生的时候 用于取出所有数据 这个例子 是因为我们只在ch中存了数据 现在一个一个往外取 信道大小是递减的

另一个方式是显式地关闭信道

ch := make(chan int,3)

ch <- 1

ch <- 2

ch <- 3

close(ch)// 显式地关闭信道

for v := range ch {

    fmt.Print(v,",")

}//1,2,3,

被关闭的信道会禁止数据流入 只读的  仍然可以从关闭的信道中取出数据 但是不能 写入数据了

等待多gorountine的方案

使用 信道堵塞主线 等待开出去的所有 goroutine 跑完

开出很多小goroutine  各自跑各自的 最后跑完了向主线报告

如下2个版本的方案

 只使用单个无缓冲信道阻塞主线

 使用容量为goroutines数量的缓冲信道

对于方案1  代码

var quit chan int // 只开一个信道

func foo(id int) {

    fmt.Print (id,",")

    quit <- 0 // ok finished

}

func main() {

    count := 10

    quit = make(chan int) // 无缓冲

   // quit = make(chan int, count) // // 容量10

    for i := 0; i < count; i++ {

        go foo(i)

    }

    for i := 0; i < count; i++ {

        <- quit

    }

}//0,1,2,3,4,5,6,7,8,9,

对于方案2

把信道换成缓冲10的

quit = make(chan int, count) // // 容量10

区别仅仅在于一个是缓冲的 一个是非缓冲的

对于这个场景而言 两者都能完成任务 都是可以的

无缓冲的信道是一批数据一个一个的「流进流出」

缓冲信道则是一个一个存储 然后一起流出去

2226Go 语言变量

Go 语法错误 Non-declaration statement outside function body

package main

import (

    "fmt"

)

b := 1

func main() {

    fmt.Println(b)

}

syntax error: non-declaration statement outside function body

原因 := 只能用于方法内 当定义全局变量时 只能通过 var 关键字来定义

var b int = 1

func main() {

    fmt.Println(b)

}

2225Go 语言函数

函数

函数的格式是固定的 func 函数名 参数  返回值(可选) 函数体

func main(){

fmt.Println("Hello go")

}

在golang中 两个特殊的函数 main函数 init函数 main函数 所有语言中都 作为 程序的入口 只能有一个

init函数在每个package 可选 可以有多个( 建议一个package中一个init函数) init函数在 导入该package时 会自动调用init函数 只会被调用一次 一个package被多次引用时 只会被导入一次

参数传递

普通变量

使用普通变量作为函数参数 在传递参数时 是对变量值得拷贝 即将实参的值复制给变参 当函数对变参进行处理时  不会影响原来实参的值

指针

函数的变量 可以使用指针变量 在进行参数传递时 是一个地址  即将实参的内存地址复制给变参 对变参的修改 影响 实参的值

数组

go语言在将数组名作为函数参数的时候 参数传递 是对数组的复制 在形参中对数组元素的修改都不会影响到数组元素原来的值

slice, map, chan

使用slice map chan 作为函数参数时 进行参数传递将是一个地址拷贝 即将底层数组的内存地址复制给 参数slice  map  chan  这时 对slice map chan 元素的操作就是对底层数组元素的操作

函数名字

go语言 函数也作为一种数据类型 函数也可以作为函数的参数来使用

返回值

go语言可以返回局部变量的指针 因为go语言的回收机制是 发现有被引用的栈上临时变量时 会自动存在堆上

2224Go 语言数据类型


不经历风雨怎能见彩虹没有人能随随便便成功