1.首先要说的:每个人学习都会有自己的想法和见解,所以我的总结可能只是对于我来说好理解,请见谅。
2.大概会总结的内容
- 1.go语言的接口(interface)
- 2.goroutine
- 3.channel
下面两个会留在明天或者后天写,一天写太多字也没人愿意看 - 4.并发安全和锁
- 5.两个简单的入门web代码(不使用框架)
3.正式开始
1.go语言的接口
接口这个词从开始学java就有接触到,所以先说说什么是接口。说的正式一点,接口(Interface)是一些方法特征的集合,而在go语言中接口就是一种抽象的数据类型。可能这样很不好理解,那就写点例子结合着讲。
package main
import "fmt"
type dog struct{}
func (d dog) say() {
fmt.Println("汪汪汪")
}
type cat struct{}
func (c cat) say() {
fmt.Println("喵喵喵")
}
type person struct {
name string
}
func (p person) say() {
fmt.Println("啊啊啊")
}
//接口不注重类型,只注重实现的方法
//只要实现了say()方法的类型就能成为sayer这个接口类型
type sayer interface {
say()
}
func hit(arg sayer) {
arg.say()
}
func main() {
c1 := cat{}
hit(c1)
d1 := dog{}
hit(d1)
p1 := person{name: "123"}
hit(p1)
}
这个例子里面我们首先有三个结构体,狗、猫、人,并且对应的有他们叫的方法,比如狗会汪汪汪的叫。这个时候我们再定义一个接口sayer,只要实现了say()方法的数据类型就能成为sayer这个接口类型。这个时候再写个函数hit(),传入的参数就是接口类型的变量,其实也就是所有实现了say()方法的类型,然后会调用传入参数对应的say()方法。
经过这个例子再来看看接口到底有什么用: 如果像往常一样,hit()函数传入的参数是特定的某个结构体,那就只能实现某一种结构体的方法,对于其他两个结构体就要写hit2,hit3来分别实现。但是有了接口,我们可以把所有实现同一个方法的类型变成一种集合,从而可以在一个函数调用不同数据类型下的同一方法。
然后接口大概作用知道了,再来说说接口一些进阶点的东西
在这里强调两句话:
1.一个类型可以实现多个接口
2.不同的类型也可以实现同一个接口
刚才的例子就属于不同类型同一接口,下面再来一个同一类型不同接口以及接口嵌套的例子
package main
import "fmt"
type mover interface {
move()
}
type sayer2 interface {
say()
}
type Person struct {
name string
age int
}
//指针接收者实现接口,赋值的时候只能传入指针!!!而使用值接收者则可以传入值和指针
func (p *Person) move() {
fmt.Printf("%s在跑\n", p.name)
}
func (p *Person) say() {
fmt.Printf("%s在叫\n", p.name)
}
//一个类型可以实现多个接口
//不同的类型也可以实现同一个接口
//接口的嵌套
type animal interface {
mover
sayer2
}
func main() {
var m animal
p1 := &Person{
name: "hahah",
age: 18,
}
m = p1
m.move()
m.say()
fmt.Println(m)
}
我定义一个人的结构体,有姓名和年纪,同时还实现了人的动和叫两个方法。此外我还有两个针对动和叫的接口mover和sayer2,这个时候我再把用animal这个接口把这两个接口嵌套,也就是说我的人这个数据类型因为实现了动和叫,也就可以是animal这个接口类型。
在这里还有个细节要注意,如果我把我的实现方法写成
func (p Person) move() {
fmt.Printf("%s在跑\n", p.name)
}
func (p Person) say() {
fmt.Printf("%s在叫\n", p.name)
}
那么主函数实例化的时候无论传入指针还是值都可以赋值给animal这个接口,但是如果像上面那样,我们的接口是指针接收者那实例化Person的时候赋值的p1只能是指针。也就是我代码中的那句指针接收者实现接口,赋值的时候只能传入指针!!!而使用值接收者则可以传入值和指针
go语言其实在指针这块已经很友好了,通常你传入指针或者值都能帮你自动转换,只是特殊的几个地方需要自己注意。
然后就是空接口的应用以及接口的断言
//空接口的应用
//1.可以作为函数的参数,例子:fmt.Println()的参数就是空接口
//2.作为map的value
var x = make(map[string]interface{}, 10)
x["name"] = "xjj"
x["age"] = 20
x["hobby"] = []string{"篮球", "唱歌", "敲代码", "玩游戏", "摄影"}
fmt.Println(x)
//接口的值由两部分组成:具体的类型+具体类型的值
//接口的断言
//开始猜一下接口的类型
var i interface{}
i = 123
ret, ok := i.(string)
if !ok {
fmt.Println("不是字符串类型")
} else {
fmt.Println("是字符串类型", ret)
}
//使用switch进行断言
switch t := i.(type) {
case string:
fmt.Println("是字符串类型", t)
case bool:
fmt.Println("是bool类型", t)
case int:
fmt.Println("是int类型", t)
}
空接口可以传入任何数据类型,所以它经常被用作传入函数的参数或者是map的value
上面所有代码运行的结果
2.goroutine
谈到goroutine,最先想到的肯定是并发,但是要谈并发又得从大的进程、线程说起,所以这里我就简单的说一下这些概念,毕竟我自己学操作系统的时候没认真就没特别清晰,呜呜呜。
当我们运行一个应用的时候,那操作系统就会为这个应用程序启动一个进程。而每个进程至少包含一个线程,也就是主线程,线程呢就是执行空间,也就可以用来运行我们所写的代码。那go语言它有什么特别的呢,一般操作系统都是在物理处理器上调用线程来运行,而go它是在逻辑处理器上调用goroutine来运行。
再就是谈谈并发和并行:用下面的例子来理解
并发和并行
并发:同一时间段,我同时和两个人聊天
并行:同一时刻,我和朋友都在和老师聊天
其实这些既然go语言的开发者都封装好了,那我们一开始还不用那么关注原理,等日后进阶一点再去看看实现的原理也更方便理解一点,所以还是先来几个例子。
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
runtime.GOMAXPROCS(1)
var wg sync.WaitGroup
wg.Add(2)
fmt.Println("Start Goroutines")
//声明一个匿名函数创建goroutine
go func() {
defer wg.Done()
//显示小写字母表3次
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c ", char)
}
fmt.Println()
}
}()
//声明一个匿名函数创建goroutine
go func() {
defer wg.Done()
//显示大写字母表3次
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c ", char)
}
fmt.Println()
}
}()
//等待goroutine结束
fmt.Println("Waiting to finish")
wg.Wait()
fmt.Println("Finish!")
}
这个例子是并发显示大小写字母表,其实程序是并发的,只不过第一个goroutine完成的太快,所以每次看到的都是先大写再小写。这个简单的例子里面也有些小细节需要注意。
1.runtime.GOMAXPROCS(),这个是指定调度器的逻辑处理器数量,1.5版本之前默认是1,之后默认是全部核数,所以需要的话可以调用这个函数进行配置。
2.sync这个包主要是用来记录维护goroutine,sync.WaitGroup是一个计数的信号量,可以记录运行的goroutine数,我们代码中Add(2)就说明我们用了两个goroutine,然后需要使用goroutine也很简单,只需要前面加上go关键字。wg.Done()就是表示任务完成,此时会把之前WaitGroup的计数量-1.
3.wg.Wait()会等所有任务结束才停止等待,也就是等计数量为0
还是上面那个代码的例子,为了实现上面说的“同时和两个人聊天的效果”,我们把指定的处理器数量增大,再来看看并发的效果
当我们把处理器设置为5,就可以看到大小写交替的情况了,由于随机性,尝试的时候可以多试几次。
还有一个简单的例子也可以说明这个
package main
import (
"fmt"
"runtime"
"sync"
)
//并发和并行
//并发:同一时间段,我同时和两个人聊天
//并行:同一时刻,我和朋友都在和老师聊天
var wg sync.WaitGroup
//goroutine类似于线程(用户态线程)
func hello(i int) {
fmt.Println("hello goroutine", i)
wg.Done() //计数器-1
}
func main() {
runtime.GOMAXPROCS(3) //占用的cpu核数,1.5+默认使用全部核数
wg.Add(100) //计数器
for i := 0; i < 100; i++ {
go hello(i)
}
fmt.Println("hello main")
//time.Sleep(time.Second)
wg.Wait() //等待计数器为0才退出
}
3.channel
channel通道主要是为了进行同步,当一个资源需要共享时用channel就可以在goroutine之间确保同步交换数据。
channel有两种:无缓冲通道和有缓冲通道,区别还得从它的创建开始讲。
unbuf:=make(chan int)//无缓冲通道
buf:=make(chan int,10)//有缓冲通道
我们创建channel需要使用make函数,参数呢第一个chan不可少,然后是需要被传递的数据类型,最后是通道容量,没有的就是无缓冲通道。
btw,在go中使用到make函数的地方主要就是:
1.slice的创建
2:map的创建
3:channel的创建
然后传递的时候我们需要用到<-操作符,传入通道的时候是channel<-,而从通道取出是:=<-channel,写个例子看一下。
package main
import "fmt"
//使用并发是为了协同工作,但是交换数据时会发生数据竞态(竞争状态)
//为了保证数据交换,go使用csp并发模型,通过通信共享内存
//channel的操作
//1.发送:<- 2.接收:<- 3.关闭:close()
func main() {
var ch1 chan int
//无缓冲区通道:同步通道
//带缓冲区的通道:异步通道
ch1 = make(chan int, 1)
//ch1:=make(chan int,1)
ch1 <- 10
x := <-ch1
fmt.Println(x)
close(ch1)
}
这个代码很简单,就是实现了把10放进通道然后再取出来打印
再来看看goroutine和channel结合的例子
package main
import "fmt"
/*
两个goroutine:
1.生成0-100的数字发送到ch1
2.从ch1取出数字并计算平方,把结果发送到ch2
*/
//单向通道:chan<-只能发送,不能取出;<-chan只能取出,不能发送
func f1(ch chan<- int) {
for i := 0; i < 100; i++ {
ch <- i
}
close(ch)
}
func f2(ch1 <-chan int, ch2 chan<- int) {
//从通道中循环取值方式1
for {
tmp, ok := <-ch1
if !ok {
break
}
ch2 <- tmp * tmp
}
close(ch2)
}
func main() {
ch1 := make(chan int, 100)
ch2 := make(chan int, 200)
go f1(ch1)
go f2(ch1, ch2)
//从通道中循环取值方式2
for ret := range ch2 {
fmt.Println(ret)
}
}
这个例子就是一个goroutine从0-100生成数字并且发送到通道一,另一个goroutine从通道一取出计算平方后再发送到通道二。这里除了结合使用之外还要注意单向通道的使用。
单向通道:chan<-只能发送,不能取出;<-chan只能取出,不能发送
在函数定义的时候把单向通道定义好,那就固定了通道的方向。另外无缓冲通道只有在发送,接受同时准备好的时侯才能实现操作,否则会导致先执行的操作阻塞等待。剩下就再来几个例子理解一下。
package main
import (
"fmt"
"time"
)
//work pool
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("worker:%d start job:%d\n", id, job)
results <- job * 2
time.Sleep(time.Millisecond * 500)
fmt.Printf("worker:%d stop job:%d\n", id, job)
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
//开启三个goroutine
for j := 0; j < 3; j++ {
go worker(j, jobs, results)
}
//发送五个任务
for i := 0; i < 5; i++ {
jobs <- i
}
close(jobs)
for i := 0; i < 5; i++ {
ret := <-results
fmt.Println(ret)
}
}
package main
import "fmt"
//select多路复用
/*
select的使用类似于switch,满足多个条件时会随机取一个任务
*/
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
default:
fmt.Println("什么都不干")
}
}
}
最后的这个例子是channel的多路复用,当满足多个条件的时候使用select会随机选择一个任务,上面由于通道容量是1,所以只能存1个就必须取出,所以只能得到偶数。
总结:
今天说的都是go语言和其他语言不同的地方,也是最吸引人的地方,所以还是需要好好消化的。明天呢会把剩下的两部分写完,然后后续写go语言相关内容的话会写些关于web或者爬虫的部分。
最后感慨一下,寒假回家比在学校还累(主要是心累),起码学校是自由的,在家有些人你越是不想见到他还越是要往你脸上凑,真的烦死了。