4.1 函数

图片说明

2、函数声明
图片说明

3、函数实现可变参数
可变参数分为几种:

  1. 多个类型一致的参数
    图片说明

  2. 多个类型不一致的参数
    上面那个例子中,我们的参数类型都是 int,如果你希望传多个参数且这些参数的类型都不一样,可以指定类型为 ...interface{}
    图片说明

4、多个可变参数函数传递参数
上面提到了可以使用 ... 来接收多个参数,除此之外,它还有一个用法,就是用来解序列,将函数的可变参数(一个切片)一个一个取出来,传递给另一个可变参数的函数,而不是传递可变参数变量本身。

同样这个用法,也只能在给函数传递参数里使用。
图片说明

5、函数的返回值
1、没有返回值
如果没有指定返回值,则函数体不能出现return,否则会报错

2、单个或多个返回值

//go支持一个函数多个返回值
func double(a int) (int, int) {
 b := a * 2
 return a, b
}
func main() {
    // 接收参数用逗号分隔
 a, b := double(2)
 fmt.Println(a, b)
}

3、如何返回
go支持返回带有变量名的值

func double(a int) (b int) {
    // 不能使用 := ,因为在返回值哪里已经声明了为int
 b = a * 2
    // 不需要指明写回哪个变量,在返回值类型那里已经指定了
 return
}
func main() {
 fmt.Println(double(2))
}
// output: 4

6、方法与函数
方法和函数有什么区别:方法是一种特殊的函数,当一个函数和对象/结构体进行绑定时,我们称此函数是一个方法

匿名函数
没有名字,即只有函数体没有函数名

func(参数列表)(返回参数列表){
    函数体
}

匿名函数一般都是定义后立即使用

func(data int) {
        fmt.Println("hello", data)
    }(100)//这里括号里的是data参数的值

或者也可以将匿名函数作为回调函数使用

// 第二个参数为函数
func visit(list []int, f func(int)) {
    for _, v := range list {
        // 执行回调函数
        f(v)
    }
}
func main() {
    // 使用匿名函数直接做为参数
    visit([]int{1, 2, 3, 4}, func(v int) {
        fmt.Println(v)
    })
}

4.2 Go协程:goroutine

在 Golang 里,你不需要学习如何创建进程池/线程池,也不需要知道什么情况下使用多线程,什么时候使用多进程。因为你没得选,也不需要选,它原生提供的 goroutine (也即协程)已经足够优秀,能够自动帮你处理好所有的事情,而你要做的只是执行它,就这么简单。

图片说明

协程的初步使用

图片说明

import "fmt"

func mytest() {
    fmt.Println("hello, go")
}

func main() {
    // 启动一个协程
    go mytest()
    fmt.Println("hello, world")
}

当我在代码中加入一行 time.Sleep 输出就符合预期了(此方法并不推荐)

import (
    "fmt"
    "time"
)

func mytest() {
    fmt.Println("hello, go")
}

func main() {
    go mytest()
    fmt.Println("hello, world")
    time.Sleep(time.Second)
}
//output  先执行主函数里面的,后又执行协程内的
hello, world
hello, go

多个协程

并发效果:

import (
    "fmt"
    "time"
)

func mygo(name string) {
    for i := 0; i < 10; i++ {
        fmt.Printf("In goroutine %s\n", name)
        // 为了避免第一个协程执行过快,观察不到并发的效果,加个休眠
        time.Sleep(10 * time.Millisecond)
    }
}

func main() {
    go mygo("协程1号") // 第一个协程
    go mygo("协程2号") // 第二个协程
    time.Sleep(time.Second)
}

图片说明
Go强大的并发特性,将同步代码转为异步代码,只要一个关键字就可以,不需要使用其他库,简单方便

4.3 信道/通道

golang现在之所以比较流行,很大以部分原始是因为它自带的并发机制。

如果说 goroutine 是 Go语言程序的并发体的话,那么 channel(信道/通道) 就是它们之间的通信机制。channel,是一个可以让一个 goroutine 与另一个 goroutine 传输信息的通道

channel是一个管道,连接多个goroutine程序,是一种队列式的数据结构,遵循先进先出的规则

channel的定义与使用

每个channel只能传递一种数据类型的数据,所以在声明的时候,必须指定数据类型(string int ...)

var channel实例 chan 信道类型

// 定义容量为10的信道
var channel实例 [10]chan 信道类型

声明后的channel,其零值是nil,无法直接使用,必须配合make函进行初始化。

信道实例 = make(chan 信道类型)

//或者
信道实例 := make(chan 信道类型)

//例如创建一个可以传输int类型的channel
pipeline :=make(chan int)

channel的数据操作一共两种:发送数据和接收数据

// 往信道中发送数据
pipline<- 200

// 从信道中取出数据,并赋值给mydata
mydata := <-pipline

信道用完了,可以对其进行关闭,避免有人一直在等待。但是你关闭信道后,接收方仍然可以从信道中取到数据

close(pipline)

对一个已关闭的信道再关闭,是会报错的,所以需要判断信道是否被关闭
图片说明

channel的容量和长度

图片说明

缓冲信道和无缓冲信道

图片说明
图片说明

双向信道 和 单向信道

通常情况下,我们定义的信道都是双向通道,可发送数据,也可以接收数据。

但有时候,我们希望对信道的数据流向做一些控制,比如这个信道只能接收数据或者这个信道只能发送数据。

因此,就有了 双向信道 和** 单向信道 **两种分类。

双向信道:默认情况下定义的信道都是双向的

import (
    "fmt"
    "time"
)

func main() {
    pipline := make(chan int)

    go func() {
        fmt.Println("准备发送数据: 100")
        pipline <- 100
    }()

    go func() {
        num := <-pipline
        fmt.Printf("接收到的数据是: %d", num)
    }()
    // 主函数sleep,使得上面两个goroutine有机会执行
    time.Sleep(1)
}
//output
准备向channel中发送数据:100
从channel中读取的数据是: 100

单向信道:分为只读信道和只写信道
图片说明
图片说明
图片说明

要先声明一个双向信道,再定义单向通道,信道的存在是为了传输数据,如果只接收或者只发送,信道就没有用处了,所以只读和只写信道缺一不可,如果你往一个只读信道中写入数据 ,或者从一个只写信道中读取数据 ,都会出错。

import (
    "fmt"
    "time"
)
 //定义只写信道类型
type Sender = chan<- int

//定义只读信道类型
type Receiver = <-chan int

func main() {
    var pipline = make(chan int)

    go func() {
        var sender Sender = pipline
        fmt.Println("准备发送数据: 100")
        sender <- 100
    }()

    go func() {
        var receiver Receiver = pipline
        num := <-receiver
        fmt.Printf("接收到的数据是: %d", num)
    }()
    // 主函数sleep,使得上面两个goroutine有机会执行
    time.Sleep(1)
}

遍历信道

遍历信道,可以使用 for 搭配 range关键字,在range时,要确保信道是处于关闭状态,否则循环会阻塞。

import "fmt"

func fibonacci(mychan chan int) {
    n := cap(mychan)
    x, y := 1, 1
    for i := 0; i < n; i++ {
        mychan <- x
        x, y = y, x+y
    }
    // 记得 close 信道
    // 不然主函数中遍历完并不会结束,而是会阻塞。
    close(mychan)
}

func main() {
    pipline := make(chan int, 10)

    go fibonacci(pipline)

    for k := range pipline {
        fmt.Println(k)
    }
}

用信道来做锁

当信道里的数据量已经达到设定的容量时,此时再往里发送数据会阻塞整个程序。

利用这个特性,可以用当其来当程序的锁。

package main

import {
    "fmt"
    "time"
}

// 由于 x=x+1 不是原子操作
// 所以应避免多个协程对x进行操作
// 使用容量为1的信道可以达到锁的效果
func increment(ch chan bool, x *int) {
    ch <- true
    *x = *x + 1
    <- ch
}

func main() {
    // 注意要设置容量为 1 的缓冲信道
    pipline := make(chan bool, 1)

    var x int
    for i:=0;i<1000;i++{
        go increment(pipline, &x)
    }

    // 确保所有的协程都已完成
    // 以后会介绍一种更合适的方法(Mutex),这里暂时使用sleep
    time.Sleep(3)
    fmt.Println("x 的值:", x)
}
//output
x 的值:1000 

这里如果不加锁,输出值会小于1000

注意事项

  1. 关闭一个未初始化的 channel 会产生 panic
  2. 重复关闭同一个 channel 会产生 panic
  3. 向一个已关闭的 channel 发送消息会产生 panic
  4. 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已被读取,则会读取到该类型的零值。
  5. 从已关闭的 channel 读取消息永远不会阻塞,并且会返回一个为 false 的值,用以判断该 channel 是否已关闭(x,ok := <- ch)
  6. 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
  7. channel 在 Golang 中是一等公民,它是线程安全的,面对并发问题,应首先想到 channel。