Golang学习笔记四-接口、Golang包和并发

Golang接口

接口像是一个公司里面的领导,他会定义一些通用规范,只设计规范,而不实现规范。

go语言的接口,是一种新的类型定义 ,它把所有的具有共性的方法 定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

语法格式和方法非常类似。

接口的语法格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
/* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}

在接口定义中定义,若干个空方法。这些方法都具有通用性。

接口实例

下面我定义一个USB接口,有读read和写write两个方法,再定义一个电脑Computer和一个手机Mobile来实现这个接口。

USB接口

1
2
3
4
type USB interface {
read()
write()
}

Computer结构体

1
2
type Computer struct {
}

Mobile结构体

1
2
type Mobile struct {
}

Computer实现USB接口方法

1
2
3
4
5
6
7
func (c Computer) read() {
fmt.Println("computer read...")
}

func (c Computer) write() {
fmt.Println("computer write...")
}

Mobile实现USB接口方法

1
2
3
4
5
6
7
func (c Mobile) read() {
fmt.Println("mobile read...")
}

func (c Mobile) write() {
fmt.Println("mobile write...")
}

测试

1
2
3
4
5
6
7
8
9
func main() {
c := Computer{}
m := Mobile{}

c.read()
c.write()
m.read()
m.write()
}

运行结果

1
2
3
4
5
6
7
8
9
func main() {
c := Computer{}
m := Mobile{}

c.read()
c.write()
m.read()
m.write()
}

实现接口必须实现接口中的所有方法

下面我们定义一个OpenClose接口,里面有两个方法open和close,定义个Door结构体,实现其中一个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type OpenClose interface {
open()
close()
}

type Door struct {
}

func (d Door) open() {
fmt.Println("open door...")
}

func main() {
var oc OpenClose
oc = Door{} // 这里编译错误,提示只实现了一个接口
}

Golang接口值类型接收者和指针类型接收者

这个话题,本质上和方法的值类型接收者和指针类型接收者,的思考方法是一样的,值接收者是一个拷贝,是一个副本,而指针接收者,传递的是指针。

实例演示

定义一个Pet接口

1
2
3
type Pet interface {
eat()
}

定义一个Dog结构体

1
2
3
type Dog struct {
name string
}

实现Pet接口(接收者是值类型)

1
2
3
4
5
func (dog Dog) eat() {
fmt.Printf("dog: %p\n", &dog)
fmt.Println("dog eat..")
dog.name = "黑黑"
}

测试

1
2
3
4
5
6
func main() {
dog := Dog{name: "花花"}
fmt.Printf("dog: %p\n", &dog)
dog.eat()
fmt.Printf("dog: %v\n", dog)
}

运行结果

1
2
3
4
dog: 0xc000046240
dog: 0xc000046250
dog eat..
dog: {花花}

从运行结果,我们看出dog的地址变了,说明是复制了一份,dog的name没有变说明,外面的dog变量没有被改变。

将Pet接口改为指针接收者

1
2
3
4
5
func (dog *Dog) eat() {
fmt.Printf("dog: %p\n", dog)
fmt.Println("dog eat..")
dog.name = "黑黑"
}

再测试

1
2
3
4
5
6
func main() {
dog := &Dog{name: "花花"}
fmt.Printf("dog: %p\n", dog)
dog.eat()
fmt.Printf("dog: %v\n", dog)
}

运行结果

1
2
3
4
dog: 0xc00008c230
dog: 0xc00008c230
dog eat..
dog: &{黑黑}

Golang接口和类型的关系

  1. 一个类型可以实现多个接口
  2. 多个类型可以实现同一个接口(多态)

一个类型实现多个接口

一个类型实现多个接口,例如:有一个Player接口可以播放音乐,有一个Video接口可以播放视频,一个手机Mobile实现这两个接口,既可以播放音乐,又可以播放视频。

定义一个Player接口

1
2
3
type Player interface {
playMusic()
}

定义一个Video接口

1
2
3
type Video interface {
playVideo()
}

定义Mobile结构体

1
2
type Mobile struct {
}

实现两个接口

1
2
3
4
5
6
7
func (m Mobile) playMusic() {
fmt.Println("播放音乐")
}

func (m Mobile) playVideo() {
fmt.Println("播放视频")
}

测试

1
2
3
4
5
func main() {
m := Mobile{}
m.playMusic()
m.playVideo()
}

运行结果

1
2
播放音乐
播放视频

多个类型实现同一个接口

比如,一个宠物接口Pet,猫类型Cat和狗类型Dog都可以实现该接口,都可以把猫和狗当宠物类型对待,这在其他语言中叫做多态

定义一个Pet接口

1
2
3
type Pet interface {
eat()
}

定义一个Dog结构体

1
2
type Dog struct {
}

定义一个Cat结构体

1
2
type Cat struct {
}

实现接口

1
2
3
4
5
6
7
func (cat Cat) eat() {
fmt.Println("cat eat...")
}

func (dog Dog) eat() {
fmt.Println("dog eat...")
}

测试

1
2
3
4
5
6
7
func main() {
var p Pet
p = Cat{}
p.eat()
p = Dog{}
p.eat()
}

运行结果

1
2
cat eat...
dog eat...

Golang接口嵌套

接口可以通过嵌套,创建新的接口。例如:飞鱼,既可以飞,又可以游泳。我们创建一个飞Fly接口,创建一个游泳接口Swim,飞鱼接口有这两个接口组成。

飞Flyer接口

1
2
3
type Flyer interface {
fly()
}

创建Swimmer接口

1
2
3
type Swimmer interface {
swim()
}

组合一个接口FlyFish

1
2
3
4
type FlyFish interface {
Flyer
Swimmer
}

创建一个结构体Fish

1
2
type Fish struct {
}

实现这个组合接口

1
2
3
4
5
6
7
func (fish Fish) fly() {
fmt.Println("fly...")
}

func (fish Fish) swim() {
fmt.Println("swim...")
}

测试

1
2
3
4
5
6
func main() {
var ff FlyFish
ff = Fish{}
ff.fly()
ff.swim()
}

运行结果

1
2
fly...
swim...

Golang 通过接口实现OCP设计原则

而面向对象的可复用设计 的第一块基石,便是所谓的”开-闭“原则(Open-Closed Principle,常缩写为OCP)。虽然,go不是面向对象语言,但是也可以模拟实现这个原则。对扩展 是开放的,对修改 是关闭的。

OCP设计原则实例

下面通过一个人养宠物的例子,来解释OCP设计原则。

定义一个宠物接口Pet

1
2
3
4
type Pet interface {
eat()
sleep()
}

该接口有吃和睡两个方法。

定义个Dog结构体

1
2
3
4
type Dog struct {
name string
age int
}

Dog实现接口方法

1
2
3
4
5
6
7
func (dog Dog) eat() {
fmt.Println("dog eat...")
}

func (dog Dog) sleep() {
fmt.Println("dog sleep...")
}

定义一个Cat结构体

1
2
3
4
type Cat struct {
name string
age int
}

Cat实现接口方法

1
2
3
4
5
6
7
func (cat Cat) eat() {
fmt.Println("cat eat...")
}

func (cat Cat) sleep() {
fmt.Println("cat sleep...")
}

定义个Person结构体

1
2
3
type Person struct {
name string
}

为Person添加一个养宠物方法

1
2
3
4
func (per Person) care(pet Pet) {
pet.eat()
pet.sleep()
}

最后测试一下

1
2
3
4
5
6
7
8
9
10
func main() {

dog := Dog{}
cat := Cat{}
per := Person{}

per.care(dog)
per.care(cat)

}

运行结果

1
2
3
4
dog eat...
dog sleep...
cat eat...
cat sleep...

使用接口的这种设计方法,可以很好的解耦合代码,实现软件设计的OCP原则(即开闭原则)

这样设计,如果再添加一个宠物,例如:一个鸟 Bird,原有的代码不用修改,直接添加就可以。

Golang继承

Golang本质上没有oop的概念,也没有继承的概念,但是可以通过结构体嵌套 实现这个特性。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import "fmt"

type Animal struct {
name string
age int
}

func (a Animal) eat() {
fmt.Println("eat...")
}

func (a Animal) sleep() {
fmt.Println("sleep")
}

type Dog struct {
Animal
}

type Cat struct {
Animal
}

func main() {
dog := Dog{
Animal{
name: "dog",
age: 2,
},
}

cat := Cat{
Animal{name: "cat",
age: 3},
}

dog.eat()
dog.sleep()

cat.eat()
cat.sleep()

}

Golang模拟OOP的属性和方法

golang没有面向对象的概念,也没有封装的概念,但是可以通过结构体 struct和函数绑定 来实现OOP的属性和方法等特性。接收者 receiver 方法

例如 ,想要定义一个Person类,有name和age属性,有eat/sleep/work方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import "fmt"

type Person struct {
name string
age int
}

func (per Person) eat() {
fmt.Println("eat...")
}

func (per Person) sleep() {
fmt.Println("sleep...")
}

func (per Person) work() {
fmt.Println("work...")
}

func main() {
per := Person{
name: "tom",
age: 20,
}

fmt.Printf("per: %v\n", per)

per.eat()
per.sleep()
per.work()
}

Golang构造函数

Golang没有构造函数的概念,可以使用函数来模拟构造函数的的功能。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

type Person struct {
name string
age int
}

func NewPerson(name string, age int) (*Person, error) {
if name == "" {
return nil, fmt.Errorf("name 不能为空")
}
if age < 0 {
return nil, fmt.Errorf("age 不能小于0")
}
return &Person{name: name, age: age}, nil
}

func main() {
person, err := NewPerson("tom", 20)
if err == nil {
fmt.Printf("person: %v\n", *person)
}
}

Golang包

包可以区分命令空间(一个文件夹中不能有两个同名文件),也可以更好的管理项目。go中创建一个包,一般是创建一个文件夹,在该文件夹里面的go文件中,使用package关键字声明包名称,通常,文件夹名称和包名称相同。并且,同一个文件下面只有一个包

创建包

  1. 创建一个名为dao的文件夹。
  2. 创建一个dao.go文件。
  3. 在该文件中声明包。
    1
    2
    3
    4
    5
    6
    package dao
    import "fmt"

    func Test1() {
    fmt.Println("test package")
    }

导入包

要使用某个包下面的变量或者方法,需要导入该包,导入包时,要导入从 GOPATH开始的包路径,例如,在 service.go中导入 dao

1
2
3
4
5
6
7
package main

import "dao"

func main() {
dao.Test1()
}

包注意事项

  • 一个文件夹下只能有一个package import后面的其实是 GOPATH开始的相对目录路径,包括最后一段。但由于一个目录下只能有一个package,所以 import一个路径就等于是 import了这个路径下的包。

    注意,这里指的是“直接包含”的go文件。如果有子目录,那么子目录的父目录是完全两个包。

  • 比如你实现了一个计算器package,名叫 calc,位于 calc目录下;但又想给别人一个使用范例,于是在calc下可以建个example子目录(calc/example/),这个子目录里有个example.go(calc/example/example.go)。此时,example.go可以是main包,里面还可以有个main函数。

  • 一个package的文件不能在多个文件夹下如果多个文件夹下有重名的package,它们其实是彼此无关的package。

    如果一个go文件需要同时使用不同目录下的同名package,需要在 import这些目录时为每个目录指定一个package的别名。

Golang 包管理工具go module

Go module简介

go modules 是 golang 1.11 新加的特性,用来管理模块中包的依赖关系

Go mod 使用方法

  • 初始化模块go mod init <项目模块名称>
  • 依赖关系处理 ,根据go.mod文件go mod tidy
  • 将依赖包复制到项目下的 vendor目录。go mod vendor如果包被屏蔽(墙),可以使用这个命令,随后使用go build -mod=vendor编译
  • 显示依赖关系go list -m all
  • 显示详细依赖关系go list -m -json all
  • 下载依赖go mod download [path@version][path@version]是非必写的

Golang并发编程之协程

Golang 中的并发是函数 相互独立运行的能力。Goroutines 是并发运行的函数。Golang 提供了 Goroutines 作为并发处理操作的一种方式。

创建一个协程非常简单,就是在一个任务函数前面添加一个go关键字:

1
go task()

实例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func show(msg string) {
for i := 1; i < 5; i++ {
fmt.Printf("msg: %v\n", msg)
time.Sleep(time.Millisecond * 100)
}
}

func main() {
go show("java")
show("golang") // 在main协程中执行,如果它前面也添加go,程序没有输出
fmt.Println("end...")
}

查看go关键字去掉的运行效果

实例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)

func responseSize(url string) {
fmt.Println("Step1: ", url)
response, err := http.Get(url)
if err != nil {
log.Fatal(err)
}

fmt.Println("Step2: ", url)
defer response.Body.Close()

fmt.Println("Step3: ", url)
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println("Step4: ", len(body))
}

func main() {
go responseSize("https://www.xiaobaibk.com")
go responseSize("https://baidu.com")
go responseSize("https://jd.com")
time.Sleep(10 * time.Second)
}

Golang并发编程之通道channel

Go 提供了一种称为通道的机制,用于在 goroutine 之间共享数据 。当您作为 goroutine 执行并发活动时,需要在 goroutine 之间共享资源或数据,通道充当 goroutine 之间的管道(管道)并提供一种机制来保证同步交换。

需要在声明通道时指定数据类型 。我们可以共享内置、命名、结构和引用类型的值和指针。数据在通道上传递:在任何给定时间只有一个 goroutine 可以访问数据项:因此按照设计不会发生数据竞争。

根据数据交换的行为,有两种类型的通道:无缓冲通道和缓冲通道。无缓冲通道用于执行 goroutine 之间的同步通信,而缓冲通道用于执行异步通信。无缓冲通道保证在发送和接收发生的瞬间执行两个 goroutine 之间的交换。缓冲通道没有这样的保证。

通道由 make 函数创建,该函数指定 chan 关键字和通道的元素类型。

这是创建无缓冲和缓冲通道的代码块:

语法

1
2
Unbuffered := make(chan int) // 整型无缓冲通道
buffered := make(chan int, 10) // 整型有缓冲通道

使用内置函数 make创建无缓冲和缓冲通道。make的第一个参数需要关键字 chan,然后是通道允许交换的数据类型。

这是将值发送到通道的代码块需要使用 <- 运算符:

语法

1
2
goroutine1 := make(chan string, 5) // 字符串缓冲通道
goroutine1 <- "Australia" // 通过通道发送字符串

一个包含 5 个值的缓冲区的字符串类型的 goroutine1 通道。然后我们通过通道发送字符串“Australia”。

这是从通道接收值的代码块:

语法

1
data := <-goroutine1 // 从通道接收字符串

<- 运算符附加到通道变量(goroutine1)的左侧,以接收来自通道的值。

无缓冲通道

在无缓冲通道中,在接收到任何值之前没有能力保存它。在这种类型的通道中,发送和接收 goroutine 在任何发送或接收操作完成之前的同一时刻都准备就绪。如果两个 goroutine 没有在同一时刻准备好,则通道会让执行其各自发送或接收操作的 goroutine 首先等待。同步是通道上发送和接收之间交互的基础。没有另一个就不可能发生。

缓冲通道

在缓冲通道中,有能力在接收到一个或多个值之前保存它们。在这种类型的通道中,不要强制 goroutine 在同一时刻准备好执行发送和接收。当发送或接收阻塞时也有不同的条件。只有当通道中没有要接收的值时,接收才会阻塞。仅当没有可用缓冲区来放置正在发送的值时,发送才会阻塞。

通道的发送和接收特性

  1. 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。
  2. 发送操作和接收操作中对元素值的处理都是不可分割的。
  3. 发送操作在完全完成之前会被阻塞。接收操作也是如此。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"math/rand"
"time"
)

// 创建int类型通道,只能传入int类型值
var values = make(chan int)

func send() {
rand.Seed(time.Now().UnixNano())
value := rand.Intn(10)
fmt.Printf("send: %v\n", value)
// time.Sleep(time.Second * 5)
values <- value
}

func main() {
// 从通道接收值
defer close(values)
go send()
fmt.Println("wait...")
value := <-values
fmt.Printf("receive: %v\n", value)
fmt.Println("end...")
}

Golang并发编程之WaitGroup实现同步

实例演示

查看添加 WaitGroup和不添加 WaitGroup的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
var wg sync.WaitGroup

func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}

Golang并发编程之runtime包

runtime包里面定义了一些协程管理相关的api

runtime.Gosched()

让出CPU时间片,重新等待安排任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"runtime"
)

func show(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}

func main() {
go show("java")
// 主协程
for i := 0; i < 2; i++ {
// 切一下,再次分配任务
runtime.Gosched() // 注释掉查看结果
fmt.Println("golang")
}
}

runtime.Goexit()

退出当前协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"runtime"
"time"
)

func show() {
for i := 0; i < 10; i++ {
if i >= 5 {
runtime.Goexit()
}
fmt.Printf("i: %v\n", i)
}
}

func main() {
go show()
time.Sleep(time.Second)
}

runtime.GOMAXPROCS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"runtime"
"time"
)

func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}

func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}

func main() {
fmt.Printf("runtime.NumCPU(): %v\n", runtime.NumCPU())
runtime.GOMAXPROCS(2) // 修改为1查看效果
go a()
go b()
time.Sleep(time.Second)
}

Golang并发编程之Mutex互斥锁实现同步

除了使用channel实现同步之外,还可以使用Mutex互斥锁的方式实现同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
"sync"
"time"
)

var m int = 100

var lock sync.Mutex

var wt sync.WaitGroup

func add() {
defer wt.Done()
lock.Lock()
m += 1
time.Sleep(time.Millisecond * 10)
lock.Unlock()
}

func sub() {
defer wt.Done()
lock.Lock()
time.Sleep(time.Millisecond * 2)
m -= 1
lock.Unlock()
}

func main() {

for i := 0; i < 100; i++ {
go add()
wt.Add(1)
go sub()
wt.Add(1)
}

wt.Wait()
fmt.Printf("m: %v\n", m)
}

Golang并发编程之channel的遍历

方法1 for循环+if判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {

c := make(chan int)

go func() {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}()

for {
if data, ok := <-c; ok {
fmt.Printf("data: %v\n", data)
} else {
break
}
}
}

方法2 for range

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {

c := make(chan int)

go func() {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}()

for v := range c {
fmt.Printf("v: %v\n", v)
}
}

注意:如果通道关闭,读多写少,没有了就是默认值,例如,int 就是0,如果没有关闭就会死锁。

Golang并发编程之select

  1. select是Go中的一个控制结构,类似于switch语句,用于处理异步IO操作。select会监听case语句中channel的读写操作,当case中channel读写操作为非阻塞状态(即能读写)时,将会触发相应的动作。

    select中的case语句必须是一个channel操作

    select中的default子句总是可运行的。

  2. 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行。
  3. 如果没有可运行的case语句,且有default语句,那么就会执行default的动作。
  4. 如果没有可运行的case语句,且没有default语句,select将阻塞,直到某个case通信可以运行

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"time"
)

var chanInt = make(chan int)
var chanStr = make(chan string)

func main() {

go func() {
chanInt <- 100
chanStr <- "hello"
close(chanInt)
close(chanStr)
}()

for {
select {
case r := <-chanInt:
fmt.Printf("chanInt: %v\n", r)
case r := <-chanStr:
fmt.Printf("chanStr: %v\n", r)
default:
fmt.Println("default...")
}
time.Sleep(time.Second)
}
}

Golang并发编程之Timer

Timer顾名思义,就是定时器的意思,可以实现一些定时操作,内部也是通过channel来实现的。

实例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"fmt"
"time"
)

func main() {
timer1 := time.NewTimer(time.Second * 2)
t1 := time.Now()
fmt.Printf("t1:%v\n", t1)

t2 := <-timer1.C
fmt.Printf("t2:%v\n", t2)

//如果只是想单纯的等待的话,可以使用 time.Sleep 来实现
timer2 := time.NewTimer(time.Second * 2)
<-timer2.C
fmt.Println("2s后")

time.Sleep(time.Second * 2)
fmt.Println("再一次2s后")

<-time.After(time.Second * 2) //time.After函数的返回值是chan Time
fmt.Println("再再一次2s后")

timer3 := time.NewTimer(time.Second)
go func() {
<-timer3.C
fmt.Println("Timer 3 expired")
}()

stop := timer3.Stop() //停止定时器
////阻止timer事件发生,当该函数执行后,timer计时器停止,相应的事件不再执行
if stop {
fmt.Println("Timer 3 stopped")
}

fmt.Println("before")
timer4 := time.NewTimer(time.Second * 5) //原来设置5s
timer4.Reset(time.Second * 1) //重新设置时间,即修改NewTimer的时间
<-timer4.C
fmt.Println("after")
}

Golang并发编程之Ticker

Timer只执行一次,Ticker可以周期的执行。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func main() {
ticker := time.NewTicker(time.Second)
counter := 1
for _ = range ticker.C {
fmt.Println("ticker 1")
counter++
if counter >= 5 {
break
}
}
ticker.Stop()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"time"
)

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

ticker := time.NewTicker(time.Second)

go func() {
for _ = range ticker.C {
select {
case chanInt <- 1:
case chanInt <- 2:
case chanInt <- 3:
}
}
}()

sum := 0
for v := range chanInt {
fmt.Printf("接收: %v\n", v)
sum += v
if sum >= 10 {
fmt.Printf("sum: %v\n", sum)
break
}
}
}

Golang并发编程之原子变量的引入

先看一个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"sync"
"time"
)

var i = 100

var lock sync.Mutex

func add() {
lock.Lock()
i++
lock.Unlock()
}

func sub() {
lock.Lock()
i--
lock.Unlock()
}

func main() {
for i := 0; i < 100; i++ {
go add()
go sub()
}

time.Sleep(time.Second * 3)

fmt.Printf("i: %v\n", i)
}

这是我们之前的写法,使用锁实现协程的同步

下面使用原子操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"sync/atomic"
"time"
)

var i int32 = 100

func add() {
atomic.AddInt32(&i, 1)
}

func sub() {
atomic.AddInt32(&i, -1)
}

func main() {
for i := 0; i < 100; i++ {
go add()
go sub()
}

time.Sleep(time.Second * 3)
fmt.Printf("i: %v\n", i)
}

Golang并发编程之原子操作详解

atomic 提供的原子操作能够确保任一时刻只有一个goroutine对变量进行操作,善用 atomic 能够避免程序中出现大量的锁操作。

atomic常见操作有:

  • 增减
  • 载入 read
  • 比较并交换 cas
  • 交换
  • 存储 write

下面将分别介绍这些操作。

增减操作

atomic 包中提供了如下以Add为前缀的增减操作:

1
2
3
4
5
- func AddInt32(addr *int32, delta int32) (new int32)
- func AddInt64(addr *int64, delta int64) (new int64)
- func AddUint32(addr *uint32, delta uint32) (new uint32)
- func AddUint64(addr *uint64, delta uint64) (new uint64)
- func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

载入操作

atomic 包中提供了如下以Load为前缀的增减操作:

1
2
3
4
5
6
- func LoadInt32(addr *int32) (val int32)
- func LoadInt64(addr *int64) (val int64)
- func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
- func LoadUint32(addr *uint32) (val uint32)
- func LoadUint64(addr *uint64) (val uint64)
- func LoadUintptr(addr *uintptr) (val uintptr)

载入操作能够保证原子的读变量的值,当读取的时候,任何其他CPU操作都无法对该变量进行读写,其实现机制受到底层硬件的支持。

比较并交换

该操作简称 CAS(Compare And Swap)。 这类操作的前缀为 CompareAndSwap :

1
2
3
4
5
6
- func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
- func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
- func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
- func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
- func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
- func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)

该操作在进行交换前首先确保变量的值未被更改,即仍然保持参数 old 所记录的值,满足此前提下才进行交换操作。CAS的做法类似操作数据库时常见的乐观锁机制。

交换

此类操作的前缀为 Swap

1
2
3
4
5
6
- func SwapInt32(addr *int32, new int32) (old int32)
- func SwapInt64(addr *int64, new int64) (old int64)
- func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
- func SwapUint32(addr *uint32, new uint32) (old uint32)
- func SwapUint64(addr *uint64, new uint64) (old uint64)
- func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)

相对于CAS,明显此类操作更为暴力直接,并不管变量的旧值是否被改变,直接赋予新值然后返回背替换的值。

存储

此类操作的前缀为 Store

1
2
3
4
5
6
- func StoreInt32(addr *int32, val int32)
- func StoreInt64(addr *int64, val int64)
- func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
- func StoreUint32(addr *uint32, val uint32)
- func StoreUint64(addr *uint64, val uint64)
- func StoreUintptr(addr *uintptr, val uintptr)

此类操作确保了写变量的原子性,避免其他操作读到了修改变量过程中的脏数据。