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或者爬虫的部分。
最后感慨一下,寒假回家比在学校还累(主要是心累),起码学校是自由的,在家有些人你越是不想见到他还越是要往你脸上凑,真的烦死了。