Golang基础学习笔记(二)

Golang基础学习笔记(二)
强烈推介IDEA2020.2破解激活,IntelliJ IDEA 注册码,2020.2 IDEA 激活码

文章目录

12.Golang中的包

12.1 Go中的包的介绍和定义

包(package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt、strconv、strings、sort、errors、time、encoding/json、os、io等。

Golang中的包可以分为三种:1、系统内置包 2、自定义包 3、第三方包

  • 系统内置包:Golang 语言给我们提供的内置包,引入后可以直接使用,如fmt、strconv、strings、sort、errors、time、encoding/json、os、io等。
  • 自定义包:开发者自己写的包
  • 第三方包:属于自定义包的一种,需要下载安装到本地后才可以使用,如前面给大家介绍的 "github.com/shopspring/decimal"包解决float精度丢失问题。

12.2 Go包管理工具 go mod

在Golang1.11版本之前如果我们要自定义包的话必须把项目放在GOPATH目录。Go1.11版本之后无需手动配置环境变量,使用go mod 管理项目,也不需要非得把项目放到GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目,Go1.13以后可以彻底不要GOPATH了。

12.2.1 go mod init初始化项目

实际项目开发中我们首先要在我们项目目录中用go mod命令生成一个go.mod文件管理我们项目的依赖。

比如我们的golang项目文件要放在了itying这个文件夹,这个时候我们需要在itying文件夹里面使用go mod命令生成一个go.mod文件

go mod init goProject

在这里插入图片描述
然后会生成一个 go.mod 的文件,里面的内容是go版本,以及以后添加的包

module goProject

go 1.14

12.2.2 引入其它项目的包

首先我们创建一个 calc,然后里面有一个calc的文件

package calc

// 自定义包,最好和文件夹统一起来

// 公有变量
var age = 10
// 私有变量
var Name = "张三"

// 首字母大写,表示共有方法
func Add(x, y int)int  {
   
	return x + y
}
func Sub(x, y int)int  {
   
	return x - y
}

在其它地方需要引用的话,就是这样

package main
import (
	"fmt"
	"goProject/calc"
)
func main() {
   
	fmt.Printf("%v", calc.Add(2, 5))
}

12.3 Golang中自定义包

包(package)是多个Go源码的集合,一个包可以简单理解为一个存放多个.go文件的文件夹。该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。

package 包名

注意事项

  • 一个文件夹下面直接包含的文件只能归属一个package,同样一个package的文件不能在多个文件夹下。
  • 包名可以不和文件夹的名字一样,包名不能包含-符号。
  • 包名为main的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。

12.4 Go中init()初始化函数

12.4.1 init函数介绍

在Go 语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是:init() 函数没有参数也没有返回值。init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。 包初始化执行的顺序如下图所示:

包初始化执行的顺序如下图所示:

在这里插入图片描述

12.4.2 init函数执行顺序

Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。

在运行时,被最后导入的包会最先初始化并调用其init()函数,如下图示:
在这里插入图片描述
也就是父类中的init先执行。

12.5 Go中的第三方包

我们可以在 https://pkg.go.dev/ 查找看常见的golang第三方包

例如,前面找到前面我们需要下载的第三方包的地址

https://github.com/shopspring/decimal

然后安装这个包

12.5.1 方法1:go get 包全名 (全局)

go get github.com/shopspring/decimal

12.5.2 方法2:go mod download (全局)

go mod download

依赖包会自动下载到 $GOPATH/pkg/mod目录,并且多个项目可以共享缓存的mod,注意使用go mod download的时候,需要首先在你的项目中引入第三方包

12.5.3 方法3:go mod vendor 将依赖复制到当前项目的vendor(本项目)

go mod vendor

将依赖复制到当前项目的vendor下

注意:使用go mod vendor的时候,首先需要在你的项目里面引入第三方包

12.5.4 go mod常见命令

  • go download:下载依赖的module到本地cache
  • go edit:编辑go.mod文件
  • go graph:打印模块依赖图
  • go init:在当前文件夹下初始化一个新的module,创建go.mod文件
  • tidy:增加丢失的module,去掉未使用的module
  • vendor:将依赖复制到vendor下
  • verify:校验依赖,检查下载的第三方库有没有本地修改,如果有修改,则会返回非0,否则校验成功

12.6 安装依赖

首先我们先去官网找到这个包,https://github.com/shopspring/decimal

然后在我们的项目中引入

import (
	"fmt"
	"github.com/shopspring/decimal"
	"goProject/calc"
)
func main() {
	fmt.Printf("%v \n", calc.Add(2, 5))
	// 打印公有变量
	fmt.Println(calc.Name)

	_, err := decimal.NewFromString("136.02")
	if err != nil {
		panic(err)
	}
}

引入后,我们运行项目,就会去下载了,下载完成后,我们到 go.mod文件夹,能够看到依赖被引入了

module goProject

go 1.14

require github.com/shopspring/decimal v1.2.0 // indirect

同时还生成了一个 go.sum文件

github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=

这样我们就可以使用第三包开始具体的使用了~,我们实现一个Float类型的加法

package main

import (
	"fmt"
	"github.com/shopspring/decimal"
)

func main() {
   
	var num1 float64 = 3.1
	var num2 float64 = 4.2
	d1 := decimal.NewFromFloat(num1).Add(decimal.NewFromFloat(num2))
	fmt.Println(d1)
}

12.7 完整案例

首先我们需要去 依赖官网,类似于我们的 maven repository

在这里插入图片描述
然后我们搜索gJson的包,这个包主要是用于json相关的操作

在这里插入图片描述
我们进去后,找到它的https://github.com/tidwall/gjson,然后提供了完整的教程

# 下载依赖
go get -u github.com/tidwall/gjson

使用

package main

import "github.com/tidwall/gjson"

const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`

func main() {
   
	value := gjson.Get(json, "name.last")
	println(value.String())
}

13.Go中的接口

13.1 接口的介绍

现实生活中手机、相机、U盘都可以和电脑的USB接口建立连接。我们不需要关注usb卡槽大小是否一样,因为所有的USB接口都是按照统一的标准来设计的。

Golang中的接口是一种抽象数据类型,Golang中接口定义了对象的行为规范,只定义规范不实现。接口中定义的规范由具体的对象来实现。

通俗的讲接口就一个标准,它是对一个对象的行为和规范进行约定,约定实现接口的对象必须得按照接口的规范。

13.2 Go接口的定义

在Golang中接口(interface)是一种类型,一种抽象的类型。接口(interface)是一组函数method的集合,Golang中的接口不能包含任何变量。

在Golang中接口中的所有方法都没有方法体,接口定义了一个对象的行为规范,只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想N Golang中的接口也是一种数据类型,不需要显示实现。只需要一个变量含有接口类型中的所有方法,那么这个变量就实现了这个接口。

Golang中每个接口由数个方法组成,接口的定义格式如下:

type 接口名 interface {
   
    方法名1 (参数列表1) 返回值列表1
    方法名2 (参数列表2) 返回值列表2
}

其中

  • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等,接口名最好突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名是可以省略

演示:定义一个Usber接口让Phone 和 Camera结构体实现这个接口

首先我们定义一个Usber接口,接口里面就定义了两个方法

// 定义一个Usber接口
type Usber interface {
   
	start()
	stop()
}

然后我们在创建一个手机结构体

// 如果接口里面有方法的话,必须要通过结构体或自定义类型实现这个接口

// 使用结构体来实现 接口
type Phone struct {
   
	Name string
}
// 手机要实现Usber接口的话,必须实现usb接口的所有方法
func (p Phone) Start()  {
   
	fmt.Println(p.Name, "启动")
}
func (p Phone) Stop()  {
   
	fmt.Println(p.Name, "关闭")
}

然后我们在创建一个Phone的结构体,来实现这个接口

// 如果接口里面有方法的话,必须要通过结构体或自定义类型实现这个接口

// 使用结构体来实现 接口
type Phone struct {
   
	Name string
}
// 手机要实现Usber接口的话,必须实现usb接口的所有方法
func (p Phone) start()  {
   
	fmt.Println(p.Name, "启动")
}
func (p Phone) stop()  {
   
	fmt.Println(p.Name, "关闭")
}
func main() {
   
	var phone Usber = Phone{
   
		"三星手机",
	}
	phone.start()
	phone.stop()
}

我们在创建一个Camera结构体

// 使用相机结构体来实现 接口
type Camera struct {
   
	Name string
}
// 相机要实现Usber接口的话,必须实现usb接口的所有方法
func (p Camera) start()  {
   
	fmt.Println(p.Name, "启动")
}
func (p Camera) stop()  {
   
	fmt.Println(p.Name, "关闭")
}
func main() {
   
	var camera Usber = Camera{
   
		"佳能",
	}
	camera.start()
	camera.stop()
}

我们创建一个电脑的结构体,电脑的结构体就是用于接收两个实现了Usber的结构体,然后让其工作

// 电脑
type Computer struct {
   

}

// 接收一个实现了Usber接口的 结构体
func (computer Computer) Startup(usb Usber)  {
   
	usb.start()
}

// 关闭
func (computer Computer) Shutdown (usb Usber)  {
   
	usb.stop()
}

最后我们在main中调用方法

func main() {
   
	var camera interfaceDemo.Camera = interfaceDemo.Camera{
   
		"佳能",
	}
	var phone interfaceDemo.Phone = interfaceDemo.Phone{
   
		"苹果",
	}

	var computer interfaceDemo.Computer = interfaceDemo.Computer{
   }
	computer.Startup(camera)
	computer.Startup(phone)
	computer.Shutdown(camera)
	computer.Shutdown(phone)
}

运行结果如下所示:

佳能 启动
苹果 启动
佳能 关闭
苹果 关闭

13.3 空接口(Object类型)

Golang中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。空接口表示没有任何约束,因此任何类型变量都可以实现空接口。

空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型。

// 空接口表示没有任何约束,任意的类型都可以实现空接口
type EmptyA interface {
   

}

func main() {
   
	var a EmptyA
	var str = "你好golang"
	// 让字符串实现A接口
	a = str
	fmt.Println(a)
}

同时golang中空接口也可以直接当做类型来使用,可以表示任意类型。相当于Java中的Object类型

var a interface{
   }
a = 20
a = "hello"
a = true

空接口可以作为函数的参数,使用空接口可以接收任意类型的函数参数

// 空接口作为函数参数
func show(a interface{
   }) {
   
    fmt.println(a)
}

map的值实现空接口

使用空接口实现可以保存任意值的字典

// 定义一个值为空接口类型
var studentInfo = make(map[string]interface{
   })
studentInfo["userName"] = "张三"
studentInfo["age"] = 15
studentInfo["isWork"] = true

slice切片实现空接口

// 定义一个空接口类型的切片
var slice = make([]interface{
   }, 4, 4)
slice[0] = "张三"
slice[1] = 1
slice[2] = true

13.4 类型断言

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言,其语法格式:

x.(T)

其中:

  • X:表示类型为interface{}的变量
  • T:表示断言x可能是的类型

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败

// 类型断言
var a interface{
   }
a = "132"
value, isString := a.(string)
if isString {
   
    fmt.Println("是String类型, 值为:", value)
} else {
   
    fmt.Println("断言失败")
}

或者我们可以定义一个能传入任意类型的方法

// 定义一个方法,可以传入任意数据类型,然后根据不同类型实现不同的功能
func Print(x interface{
   })  {
   
	if _,ok := x.(string); ok {
   
		fmt.Println("传入参数是string类型")
	} else if _, ok := x.(int); ok {
   
		fmt.Println("传入参数是int类型")
	} else {
   
		fmt.Println("传入其它类型")
	}
}

上面的示例代码中,如果要断言多次,那么就需要写很多if,这个时候我们可以使用switch语句来实现:

注意: 类型.(type) 只能结合switch语句使用

func Print2(x interface{
   })  {
   
	switch x.(type) {
   
	case int:
		fmt.Println("int类型")
	case string:
		fmt.Println("string类型")
	case bool:
		fmt.Println("bool类型")
	default:
		fmt.Println("其它类型")
	}
}

13.5 结构体接收者

值接收者

如果结构体中的方法是值接收者,那么实例化后的结构体值类型和结构体指针类型都可以赋值给接口变量

13.6 结构体实现多个接口

实现多个接口的话,可能就同时用两个接口进行结构体的接受

// 定义一个Animal的接口,Animal中定义了两个方法,分别是setName 和 getName,分别让DOg结构体和Cat结构体实现
type Animal interface {
   
	SetName(string)
}

// 接口2
type Animal2 interface {
   
	GetName()string
}

type Dog struct {
   
	Name string
}

func (d *Dog) SetName(name string)  {
   
	d.Name = name
}
func (d Dog)GetName()string {
   
	return d.Name
}

func main() {
   
	var dog = &Dog{
   
		"小黑",
	}
	// 同时实现两个接口
	var d1 Animal = dog
	var d2 Animal2 = dog
	d1.SetName("小鸡")
	fmt.Println(d2.GetName())
}

13.7 接口嵌套

在golang中,允许接口嵌套接口,我们首先创建一个 Animal1 和 Animal2 接口,然后使用Animal接受刚刚的两个接口,实现接口的嵌套。

// 定义一个Animal的接口,Animal中定义了两个方法,分别是setName 和 getName,分别让DOg结构体和Cat结构体实现
type Animal1 interface {
   
	SetName(string)
}

// 接口2
type Animal2 interface {
   
	GetName()string
}

type Animal interface {
   
	Animal1
	Animal2
}

type Dog struct {
   
	Name string
}

func (d *Dog) SetName(name string)  {
   
	d.Name = name
}
func (d Dog)GetName()string {
   
	return d.Name
}

func main() {
   
	var dog = &Dog{
   
		"小黑",
	}
	// 同时实现两个接口
	var d Animal = dog
	d.SetName("小鸡")
	fmt.Println(d.GetName())
}

13.8 Golang中空接口和类型断言

// golang中空接口和类型断言
var userInfo = make(map[string]interface{
   })
userInfo["userName"] = "zhangsan"
userInfo["age"] = 10
userInfo["hobby"] = []string{
   "吃饭", "睡觉"}
fmt.Println(userInfo["userName"])
fmt.Println(userInfo["age"])
fmt.Println(userInfo["hobby"])
// 但是我们空接口如何获取数组中的值?发现 userInfo["hobby"][0] 这样做不行
// fmt.Println(userInfo["hobby"][0])

也就是我们的空接口,无法直接通过索引获取数组中的内容,因此这个时候就需要使用类型断言了

// 这个时候我们就可以使用类型断言了
hobbyValue,ok := userInfo["hobby"].([]string)
if ok {
   
    fmt.Println(hobbyValue[0])
}

通过类型断言返回来的值,我们就能够直接通过角标获取了。

14.Golang goroutine channel 实现并发和并行

14.1 Golang中协程(goroutine)以及主线程

golang中的主线程:(可以理解为线程/也可以理解为进程),在一个Golang程序的主线程上可以起多个协程。Golang中多协程可以实现并行或者并发。

协程:可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的。Golang的一大特色就是从语言层面原生持协程,在函数或者方法前面加go关键字就可创建一个协程。可以说Golang中的协程就是goroutine。

在这里插入图片描述
Golang中的多协程有点类似于Java中的多线程。

多协程和多线程

多协程和多线程:Golang中每个goroutine(协程)默认占用内存远比Java、C的线程少。

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB左右),一个goroutine(协程)占用内存非常小,只有2KB左右,多协程goroutine切换调度开销方面远比线程要少。

这也是为什么越来越多的大公司使用Golang的原因之一。

14.2 goroutine的使用以及sync.WaitGroup

14.2.1 并行执行需求

在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔50毫秒秒输出“你好golang"

在主线程中也每隔50毫秒输出“你好golang",输出10次后,退出程序,要求主线程和goroutine同时执行。

这是时候,我们就可以开启协程来了,通过 go关键字开启

// 协程需要运行的方法
func test()  {
   
	for i := 0; i < 5; i++ {
   
		fmt.Println("test 你好golang")
		time.Sleep(time.Millisecond * 100)
	}
}
func main() {
   

	// 通过go关键字,就可以直接开启一个协程
	go test()

	// 这是主进程执行的
	for i := 0; i < 5; i++ {
   
		fmt.Println("main 你好golang")
		time.Sleep(time.Millisecond * 100)
	}
}

运行结果如下,我们能够看到他们之间不存在所谓的顺序关系了

main 你好golang
test 你好golang
main 你好golang
test 你好golang
test 你好golang
main 你好golang
main 你好golang
test 你好golang
test 你好golang
main 你好golang

但是上述的代码其实还有问题的,也就是说当主进程执行完毕后,不管协程有没有执行完成,都会退出
在这里插入图片描述
这是使用我们就需要用到 sync.WaitGroup等待协程

首先我们需要创建一个协程计数器

// 定义一个协程计数器
var wg sync.WaitGroup

然后当我们开启协程的时候,我们要让计数器加1

// 开启协程,协程计数器加1
wg.Add(1)
go test2()

当我们协程结束前,我们需要让计数器减1

// 协程计数器减1
wg.Done()

完整代码如下

// 定义一个协程计数器
var wg sync.WaitGroup

func test()  {
   
	// 这是主进程执行的
	for i := 0; i < 1000; i++ {
   
		fmt.Println("test1 你好golang", i)
		//time.Sleep(time.Millisecond * 100)
	}
	// 协程计数器减1
	wg.Done()
}

func test2()  {
   
	// 这是主进程执行的
	for i := 0; i < 1000; i++ {
   
		fmt.Println("test2 你好golang", i)
		//time.Sleep(time.Millisecond * 100)
	}
	// 协程计数器减1
	wg.Done()
}

func main() {
   

	// 通过go关键字,就可以直接开启一个协程
	wg.Add(1)
	go test()

	// 协程计数器加1
	wg.Add(1)
	go test2()

	// 这是主进程执行的
	for i := 0; i < 1000; i++ {
   
		fmt.Println("main 你好golang", i)
		//time.Sleep(time.Millisecond * 100)
	}
	// 等待所有的协程执行完毕
	wg.Wait()
	fmt.Println("主线程退出")
}

14.2.2 for循环开启多个协程

类似于Java里面开启多个线程,同时执行

func test(num int)  {
   
	for i := 0; i < 10; i++ {
   
		fmt.Printf("协程(%v)打印的第%v条数据 \n", num, i)
	}
	// 协程计数器减1
	vg.Done()
}

var vg sync.WaitGroup

func main() {
   
	for i := 0; i < 10; i++ {
   
		go test(i)
		vg.Add(1)
	}
	vg.Wait()
	fmt.Println("主线程退出")
}

因为我们协程会在主线程退出后就终止,所以我们还需要使用到 sync.WaitGroup来控制主线程的终止。

14.3 设置Go并行运行的时候占用的cpu数量

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个oS线程上。

Go 语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

func main() {
   
	// 获取cpu个数
	npmCpu := runtime.NumCPU()
	fmt.Println("cup的个数:", npmCpu)
	// 设置允许使用的CPU数量
	runtime.GOMAXPROCS(runtime.NumCPU() - 1)
}

14.4 Channel管道

管道是Golang在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Golang的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

Go语言中的管道(channel)是一种特殊的类型。管道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

14.4.1 channel类型

channel是一种类型,一种引用类型。声明管道类型的格式如下:

// 声明一个传递整型的管道
var ch1 chan int
// 声明一个传递布尔类型的管道
var ch2 chan bool
// 声明一个传递int切片的管道
var ch3 chan []int

14.4.2 创建channel

声明管道后,需要使用make函数初始化之后才能使用

make(chan 元素类型, 容量)

举例如下:

// 创建一个能存储10个int类型的数据管道
ch1 = make(chan int, 10)
// 创建一个能存储4个bool类型的数据管道
ch2 = make(chan bool, 4)
// 创建一个能存储3个[]int切片类型的管道
ch3 = make(chan []int, 3)

14.4.3 channel操作

管道有发送,接收和关闭的三个功能

发送和接收 都使用 <- 符号

现在我们先使用以下语句定义一个管道:

ch := make(chan int, 3)

发送

将数据放到管道内,将一个值发送到管道内

// 把10发送到ch中
ch <- 10

取操作

x := <- ch

关闭管道.

通过调用内置的close函数来关闭管道

close(ch)

完整示例

// 创建管道
ch := make(chan int, 3)

// 给管道里面存储数据
ch <- 10
ch <- 21
ch <- 32

// 获取管道里面的内容
a := <- ch
fmt.Println("打印出管道的值:", a)
fmt.Println("打印出管道的值:", <- ch)
fmt.Println("打印出管道的值:", <- ch)

// 管道的值、容量、长度
fmt.Printf("地址:%v 容量:%v 长度:%v \n", ch, cap(ch), len(ch))

// 管道的类型
fmt.Printf("%T \n", ch)

// 管道阻塞(当没有数据的时候取,会出现阻塞,同时当管道满了,继续存也会)
<- ch  // 没有数据取,出现阻塞
ch <- 10
ch <- 10
ch <- 10
ch <- 10 // 管道满了,继续存,也出现阻塞

14.5 for range从管道循环取值

当向管道中发送完数据时,我们可以通过close函数来关闭管道,当管道被关闭时,再往该管道发送值会引发panic,从该管道取值的操作会去完管道中的值,再然后取到的值一直都是对应类型的零值。那如何判断一个管道是否被关闭的呢?

// 创建管道
ch := make(chan int, 10)
// 循环写入值
for i := 0; i < 10; i++ {
   
    ch <- i
}
// 关闭管道
close(ch)

// for range循环遍历管道的值(管道没有key)
for value := range ch {
   
    fmt.Println(value)
}
// 通过上述的操作,能够打印值,但是出出现一个deadlock的死锁错误,也就说我们需要关闭管道

注意:使用for range遍历的时候,一定在之前需要先关闭管道

思考:通过for循环来遍历管道,需要关闭么?

// 创建管道
ch := make(chan int, 10)
// 循环写入值
for i := 0; i < 10; i++ {
   
    ch <- i
}

for i := 0; i < 10; i++ {
   
    fmt.Println(<- ch)
}

上述代码没有报错,说明通过for i的循环方式,可以不关闭管道

14.6 Goroutine 结合 channel 管道

需求1:定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行。

  • 开启一个fn1的的协程给向管道inChan中写入00条数据
  • 开启一个fn2的协程读取inChan中写入的数据
  • 注意:fn1和fn2同时操作一个管道
  • 主线程必须等待操作完成后才可以退出
func write(ch chan int)  {
   
	for i := 0; i < 10; i++ {
   
		fmt.Println("写入:", i)
		ch <- i
		time.Sleep(time.Microsecond * 10)
	}
	wg.Done()
}
func read(ch chan int)  {
   
	for i := 0; i < 10; i++ {
   
		fmt.Println("读取:", <- ch)
		time.Sleep(time.Microsecond * 10)
	}
	wg.Done()
}
var wg sync.WaitGroup
func main() {
   
	ch := make(chan int, 10)
	wg.Add(1)
	go write(ch)
	wg.Add(1)
	go read(ch)

	// 等待
	wg.Wait()
	fmt.Println("主线程执行完毕")
}

管道是安全的,是一边写入,一边读取,当读取比较快的时候,会等待写入

需求2:goroutine 结合 channel打印素数

在这里插入图片描述

// 想intChan中放入 1~ 120000个数
func putNum(intChan chan int)  {
   
	for i := 2; i < 120000; i++ {
   
		intChan <- i
	}
	wg.Done()
	close(intChan)
}

// cong intChan取出数据,并判断是否为素数,如果是的话,就把得到的素数放到primeChan中
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool)  {
   
	for value := range intChan {
   
		var flag = true
		for i := 2; i <= int(math.Sqrt(float64(value))); i++ {
   
			if  i % i == 0 {
   
				flag = false
				break
			}
		}
		if flag {
   
			// 是素数
			primeChan <- value
			break
		}
	}

	// 这里需要关闭 primeChan,因为后面需要遍历输出 primeChan
	exitChan <- true

	wg.Done()
}

// 打印素数
func printPrime(primeChan chan int)  {
   
	for value := range primeChan {
   
		fmt.Println(value)
	}
	wg.Done()
}


var wg sync.WaitGroup
func main() {
   
	// 写入数字
	intChan := make(chan int, 1000)

	// 存放素数
	primeChan := make(chan int, 1000)

	// 存放 primeChan退出状态
	exitChan := make(chan bool, 16)

	// 开启写值的协程
	go putNum(intChan)

	// 开启计算素数的协程
	for i := 0; i < 10; i++ {
   
		wg.Add(1)
		go primeNum(intChan, primeChan, exitChan)
	}

	// 开启打印的协程
	wg.Add(1)
	go printPrime(primeChan)

	// 匿名自运行函数
	wg.Add(1)
	go func() {
   
		for i := 0; i < 16; i++ {
   
			// 如果exitChan 没有完成16次遍历,将会等待
			<- exitChan
		}
		// 关闭primeChan
		close(primeChan)
		wg.Done()
	}()

	wg.Wait()
	fmt.Println("主线程执行完毕")
	
}

14.7 单向管道

有时候我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中,使用管道都会对其进行限制,比如限制管道在函数中只能发送或者只能接受

默认的管道是 可读可写

// 定义一种可读可写的管道
var ch = make(chan int, 2)
ch <- 10
<- ch

// 管道声明为只写管道,只能够写入,不能读
var ch2 = make(chan<- int, 2)
ch2 <- 10

// 声明一个只读管道
var ch3 = make(<-chan int, 2)
<- ch3

14.8 Select多路复用

在某些场景下我们需要同时从多个通道接收数据。这个时候就可以用到golang中给我们提供的select多路复用。 通常情况通道在接收数据时,如果没有数据可以接收将会发生阻塞。

比如说下面代码来实现从多个通道接受数据的时候就会发生阻塞

这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个管道的操作。

select的使用类似于switch 语句,它有一系列case分支和一个默认的分支。每个case会对应一个管道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

intChan := make(chan int, 10)
intChan <- 10
intChan <- 12
intChan <- 13
stringChan := make(chan int, 10)
stringChan <- 20
stringChan <- 23
stringChan <- 24

// 每次循环的时候,会随机中一个chan中读取,其中for是死循环
for {
   
    select {
   
        case v:= <- intChan:
        fmt.Println("从initChan中读取数据:", v)
        case v:= <- stringChan:
        fmt.Println("从stringChan中读取数据:", v)
        default:
        fmt.Println("所有的数据获取完毕")
        return
    }
}

tip:使用select来获取数据的时候,不需要关闭chan,不然会出现问题

14.9 Goroutine Recover解决协程中出现的Panic

func sayHello()  {
   
	for i := 0; i < 10; i++ {
   
		fmt.Println("hello")
	}
}
func errTest()  {
   
	// 捕获异常
	defer func() {
   
		if err := recover(); err != nil {
   
			fmt.Println("errTest发生错误")
		}
	}()
	var myMap map[int]string
	myMap[0] = "10"
}
func main {
   
    go sayHello()
    go errTest()
}

当我们出现问题的时候,我们还是按照原来的方法,通过defer func创建匿名自启动

// 捕获异常
defer func() {
   
    if err := recover(); err != nil {
   
        fmt.Println("errTest发生错误")
    }
}()

14.10 Go中的并发安全和锁

如下面一段代码,我们在并发环境下进行操作,就会出现并发访问的问题

var count = 0
var wg sync.WaitGroup

func test()  {
   
	count++
	fmt.Println("the count is : ", count)
	time.Sleep(time.Millisecond)
	wg.Done()
}
func main() {
   
	for i := 0; i < 20; i++ {
   
		wg.Add(1)
		go test()
	}
	time.Sleep(time.Second * 10)
}

14.10.1 互斥锁

互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock 进行解锁

// 定义一个锁
var mutex sync.Mutex
// 加锁
mutex.Lock()
// 解锁
mutex.Unlock()

完整代码

var count = 0
var wg sync.WaitGroup
var mutex sync.Mutex

func test()  {
   
	// 加锁
	mutex.Lock()
	count++
	fmt.Println("the count is : ", count)
	time.Sleep(time.Millisecond)
	wg.Done()
	// 解锁
	mutex.Unlock()
}
func main() {
   
	for i := 0; i < 20; i++ {
   
		wg.Add(1)
		go test()
	}
	time.Sleep(time.Second * 10)
}

通过下面命令,build的时候,可以查看是否具有竞争关系

// 通过 -race 参数进行构建
go build -race main.go
// 运行插件
main.ext

14.10.2 读写互斥锁

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。

其实,当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少goroutine同时读取,都是可以的。

所以问题不是出在“读”上,主要是修改,也就是“写”。修改的数据要同步,这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。

因此,衍生出另外一种锁,叫做读写锁。

读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。

GO中的读写锁由结构体类型sync.RWMutex表示。此类型的方法集合中包含两对方法:

15.Go中的反射

15.1 反射

有时我们需要写一个函数,这个函数有能力统一处理各种值类型,而这些类型可能无法共享同一个接口,也可能布局未知,也有可能这个类型在我们设计函数时还不存在,这个时候我们就可以用到反射。

空接口可以存储任意类型的变量,那我们如何知道这个空接口保存数据的类型是什么? 值是什么呢?

  • 可以使用类型断言
  • 可以使用反射实现,也就是在程序运行时动态的获取一个变量的类型信息和值信息。

把结构体序列化成json字符串,自定义结构体Tab标签的时候就用到了反射

后面所说的ORM框架,底层就是用到了反射技术

ORM:对象关系映射(Object Relational Mapping,简称 ORM)是通过使用描述对象和数据库之间的映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。

15.2 反射的基本介绍

反射是指在程序运行期间对程序本身进行访问和修改的能力。正常情况程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

15.3 Go可以实现的功能

  • 反射可以在程序运行期间动态的获取变量的各种信息,比如变量的类型类别
  • 如果是结构体,通过反射还可以获取结构体本身的信息,比如结构体的字段、结构体的方法。
  • 通过反射,可以修改变量的值,可以调用关联的方法

Go语言中的变量是分为两部分的:

  • 类型信息:预先定义好的元信息。
  • 值信息:程序运行过程中可动态变化的。

在Go语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的。

在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个重要函数来获取任意对象的Value 和 Type

15.4 reflect.TypeOf()获取任意值的类型对象

在Go 语言中,使用reflect.TypeOf()函数可以接受任意interface}参数,可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。

通过反射获取空接口的类型

func reflectFun(x interface{
   })  {
   
	v := reflect.TypeOf(x)
	fmt.Println(v)
}
func main() {
   
	reflectFun(10)
	reflectFun(10.01)
	reflectFun("abc")
	reflectFun(true)
}

15.5 type name 和 type Kind

在反射中关于类型还划分为两种:类型(Type)和种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kid)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

Go 语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空。

v := reflect.TypeOf(x)
fmt.Println("类型 ", v)
fmt.Println("类型名称 ", v.Name())
fmt.Println("类型种类 ", v.Kind())

我们之前可以通过类型断言来实现空接口类型的数相加操作

func reflectValue(x interface{
   }) {
   
	b,_ := x.(int)
	var num = 10 + b
	fmt.Println(num)
}

到现在的话,我们就可以使用reflect.TypeOf来实现了

func reflectValue2(x interface{
   }) {
   
	// 通过反射来获取变量的原始值
	v := reflect.ValueOf(x)
	fmt.Println(v)
	// 获取到V的int类型
	var n = v.Int() + 12
	fmt.Println(n)
}

同时我们还可以通过switch来完成

// 通过反射来获取变量的原始值
v := reflect.ValueOf(x)
// 获取种类
kind := v.Kind()
switch kind {
   
    case reflect.Int:
    fmt.Println("我是int类型")
    case reflect.Float64:
    fmt.Println("我是float64类型")
    default:
    fmt.Println("我是其它类型")
}

15.6 reflect.ValueOf

reflect.ValueOf() 返回的是reflect.Value类型,其中包含了原始值的值信息,reflect.Value与原始值之间可以互相转换

reflect.value类型提供的获取原始值的方法如下

方法 说明
interface{} 将值以interface{}类型返回,可以通过类型断言转换为指定类型
Int() int64 将值以int类型返回,所有有符号整型均可以此方式返回
Uint() uint64 将值以uint类型返回,所有无符号整型均可以以此方式返回
Float() float64 将值以双精度(float 64)类型返回,所有浮点数(float 32、float64)均可以以此方式返回

15.7 结构体反射

与结构体相关的方法
任意值通过reflect.Typeof)获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()和Field()方法获得结构体成员的详细信息。

reflect.Type中与获取结构体成员相关的的方法如下表所示。

方法 说明
Field(i int)StructField 根据索引,返回索引对应的结构体字段的信息
NumField() int 返回结构体成员字段数量
FieldByName(name string)(StructField, bool) 根据给定字符串返回字符串赌赢的结构体字段信息
FieldByIndex(index []int)StructField 多层成员访问时,根据[] int 提供的每个结构

示例代码,如下所示 我们修改结构体中的字段和类型

// 学生结构体
type Student4 struct {
   
	Name string `json: "name"`
	Age int `json: "age"`
	Score int `json: "score"`
}

func (s Student4)GetInfo()string  {
   
	var str = fmt.Sprintf("姓名:%v 年龄:%v 成绩:%v", s.Name, s.Age, s.Score)
	return str
}
func (s *Student4)SetInfo(name string, age int, score int)  {
   
	s.Name = name
	s.Age = age
	s.Score = score
}
func (s Student4)PrintStudent()  {
   
	fmt.Println("打印学生")
}
// 打印结构体中的字段
func PrintStructField(s interface{
   })  {
   
	t := reflect.TypeOf(s)
	// 判断传递过来的是否是结构体
	if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
   
		fmt.Println("请传入结构体类型!")
		return
	}

	// 通过类型变量里面的Field可以获取结构体的字段
	field0 := t.Field(0) // 获取第0个字段
	fmt.Printf("%#v \n", field0)
	fmt.Println("字段名称:", field0.Name)
	fmt.Println("字段类型:", field0.Type)
	fmt.Println("字段Tag:", field0.Tag.Get("json"))

	// 通过类型变量里面的FieldByName可以获取结构体的字段中
	field1, ok := t.FieldByName("Age")
	if ok {
   
		fmt.Println("字段名称:", field1.Name)
		fmt.Println("字段类型:", field1.Type)
		fmt.Println("字段Tag:", field1.Tag)
	}

	// 通过类型变量里面的NumField获取该结构体有几个字段
	var fieldCount = t.NumField()
	fmt.Println("结构体有:", fieldCount, " 个属性")

	// 获取结构体属性对应的值
	v := reflect.ValueOf(s)
	nameValue := v.FieldByName("Name")
	fmt.Println("nameValue:", nameValue)

}
func main() {
   

	student := Student4{
   
		"张三",
		18,
		95,
	}
	PrintStructField(student)
}

下列代码是获取结构体中的方法,然后调用

// 打印执行方法
func PrintStructFn(s interface{
   })  {
   
	t := reflect.TypeOf(s)
	// 判断传递过来的是否是结构体
	if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
   
		fmt.Println("请传入结构体类型!")
		return
	}
	// 通过类型变量里面的Method,可以获取结构体的方法
	method0 := t.Method(0)
	// 获取第一个方法, 这个是和ACSII相关
	fmt.Println(method0.Name)

	// 通过类型变量获取这个结构体有多少方法
	methodCount := t.NumMethod()
	fmt.Println("拥有的方法", methodCount)

	// 通过值变量 执行方法(注意需要使用值变量,并且要注意参数)
	v := reflect.ValueOf(s)
	// 通过值变量来获取参数
	v.MethodByName("PrintStudent").Call(nil)

	// 手动传参
	var params []reflect.Value
	params = append(params, reflect.ValueOf("张三"))
	params = append(params, reflect.ValueOf(23))
	params = append(params, reflect.ValueOf(99))
	// 执行setInfo方法
	v.MethodByName("SetInfo").Call(params)

	// 通过值变量来获取参数
	v.MethodByName("PrintStudent").Call(nil)
}

16.Go中的文件和目录操作

16.1 文件的读取

16.1.1 通过os.Open方法读取文件

func main() {
   
	// 读取文件 方法1
	file, err := os.Open("./main/test.txt")
	// 关闭文件流
	defer file.Close();
	if err != nil {
   
		fmt.Println("打开文件出错")
	}
	// 读取文件里面的内容
	var tempSlice = make([]byte, 1024)
	var strSlice []byte
	for {
   
		n, err := file.Read(tempSlice)
		if err == io.EOF {
   
			fmt.Printf("读取完毕")
			break
		}
		fmt.Printf("读取到了%v 个字节 \n", n)
		strSlice := append(strSlice, tempSlice...)
		fmt.Println(string(strSlice))
	}
}

16.1.2 通过bufio的方式读取

func main() {
   
	// 读取文件 方法2
	file, err := os.Open("./main/test.txt")
	// 关闭文件流
	defer file.Close();
	if err != nil {
   
		fmt.Println("打开文件出错")
	}
	// 通过创建bufio来读取
	reader := bufio.NewReader(file)
	var fileStr string
	var count int = 0
	for {
   
		// 相当于读取一行
		str, err := reader.ReadString('\n')
		if err == io.EOF {
   
			// 读取完成的时候,也会有内容
			fileStr += str
			fmt.Println("读取结束", count)
			break
		}
		if err != nil {
   
			fmt.Println(err)
			break
		}
		count ++
		fileStr += str
	}
	fmt.Println(fileStr)
}

16.1.3 通过ioutil读取

文件比较少的时候,可以通过ioutil来读取文件

// 通过IOUtil读取
byteStr, _ := ioutil.ReadFile("./main/test.txt")
fmt.Println(string(byteStr))

16.2 文件的写入

文件的写入,我们首先需要通过 os.OpenFile打开文件

// 打开文件
file, _ := os.OpenFile("./main/test.txt", os.O_CREATE | os.O_RDWR, 777)

这里有三个参数

  • name:要打开的文件名
  • flag:打开文件的模式
    • os.O_WRONLY:只读
      os.O_CREATE:创建
      os.O_RDONLY:只读
      os.O_RDWR:读写
      os.O_TRUNC:清空
      os.O_APPEND:追加
  • perm:文件权限,一个八进制数,r(读)04,w(写)02,x(执行)01

16.2.1 通过OpenFile打开文件写入

// 打开文件
file, _ := os.OpenFile("./main/test.txt", os.O_CREATE | os.O_RDWR | os.O_APPEND, 777)
defer file.Close()
str := "啦啦啦 \r\n"
file.WriteString(str)

16.2.2 通过bufio写入

// 打开文件
file, _ := os.OpenFile("./main/test.txt", os.O_CREATE | os.O_RDWR | os.O_APPEND, 777)
defer file.Close()
str := "啦啦啦 \r\n"
file.WriteString(str)

// 通过bufio写入
writer := bufio.NewWriter(file)
// 先将数据写入缓存
writer.WriteString("你好,我是通过writer写入的 \r\n")
// 将缓存中的内容写入文件
writer.Flush()	

16.2.3 通过ioutil写入

// 第三种方式,通过ioutil
str2 := "hello"
ioutil.WriteFile("./main/test.txt", []byte(str2), 777)

16.3 文件复制

通过ioutil读取和复制文件

// 读取文件
byteStr, err := ioutil.ReadFile("./main/test.txt")
if err != nil {
   
    fmt.Println("读取文件出错")
    return
}
// 写入指定的文件
ioutil.WriteFile("./main/test2.txt", byteStr, 777)

16.3 创建目录

os.Mkdir("./abc", 777)

16.4 删除操作

// 删除文件
os.Remove("aaa.txt")
// 删除目录
os.Remove("./aaa")
// 删除多个文件和目录
os.RemoveAll("./aaa")

16.5 重命名

os.Rename("")
本文来源MrKorbin,由架构君转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处:https://javajgs.com/archives/25250

发表评论