go语言几个最重要知识点的总结(1)

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语言的开发者都封装好了,那我们一开始还不用那么关注原理,等日后进阶一点再去看看实现的原理也更方便理解一点,所以还是先来几个例子。

扫描二维码关注公众号,回复: 10178366 查看本文章
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或者爬虫的部分。

最后感慨一下,寒假回家比在学校还累(主要是心累),起码学校是自由的,在家有些人你越是不想见到他还越是要往你脸上凑,真的烦死了。

发布了85 篇原创文章 · 获赞 55 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/shelgi/article/details/103913894