【Java转Go】快速上手学习笔记(四)之基础篇三

目录

  • 泛型
    • 内置泛型的使用
    • 切片泛型和泛型函数
    • map泛型
    • 泛型约束
    • 泛型完整代码
  • 接口
  • 反射
  • 协程
    • 特点
    • WaitGroup
    • goroutine的调度模型:MPG模型
  • channel
    • 介绍
    • 语法:
    • 举例:
    • channel遍历
    • 基本使用
    • 和协程一起使用
      • 案例一
      • 案例二
    • select...case
    • main.go 完整代码
  • 文件

go往期文章笔记:

【Java转Go】快速上手学习笔记(一)之环境安装篇

【Java转Go】快速上手学习笔记(二)之基础篇一

【Java转Go】快速上手学习笔记(三)之基础篇二


这篇主要是泛型、接口、反射、协程、管道、文件操作的笔记,因为我的笔记都是按照视频说的来记的,可能大家光看的话会有些看不明白,所以建议大家可以把代码复制下来,配合里面的注释一起,自己去运行一遍,加深理解


泛型

定义泛型:func 函数名 [泛型参数类型] (函数参数) {}

内置泛型的使用

Go内置的两个泛型:any 和 comparable

  • any:表示go里面所有的内置基本类型,等价于 interface{}
  • comparable:表示go里面所有内置的可比较类型:int、uint、float、booi.struct、指针 等一切可以比较的类型
func printArr[T any](arr []T) {
	for _, item := range arr {
		fmt.Println(item)
	}
}

// 泛型的使用
func 泛型的基本使用() {
	strArr := []string{"白夜", "炽翎"}
	intArr := []int{1, 2}
	printArr(strArr)
	printArr(intArr)
}

切片泛型和泛型函数

// 自定义一个切片泛型
type mySlice[T int | float64] []T

// 给自定义的切片泛型添加一个求和方法
func (s mySlice[T]) sum() T {
	var sum T
	for _, v := range s {
		sum += v
	}
	return sum
}

// 定义一个泛型函数
func add[T int | float64 | float32 | string](a T, b T) T {
	return a + b
}

// 泛型函数与方法
func 切片泛型的使用() {
	// 往自定义的切片泛型里,添加int类型的值
	var i mySlice[int] = []int{1, 2, 3, 4}
	fmt.Println(i.sum()) // 可以直接调用为切片泛型添加的一个求和方法
	// 往自定义的切片泛型里,添加float64类型的值
	var f mySlice[float64] = []float64{1.5, 2.7, 3.89, 4.55}
	fmt.Println(f.sum())

	//fmt.Println(add[int](1, 2))
	fmt.Println(add(1, 2)) // 调用时,可以自动推导传入的参数的类型
	//fmt.Println(add[string]("hh", "66"))
	fmt.Println(add("hh", "66"))
	//fmt.Println(add[float64](1.6, 2.8))
	fmt.Println(add(1.6, 2.8))
}

map泛型

// 泛型map
type myMap[K string | int, V any] map[K]V
type User struct {
	Name string
}

func map泛型的使用() {
	m1 := myMap[string, string]{
		"key": "符华",
	}
	fmt.Println(m1)
	m2 := myMap[int, User]{
		0: User{"符华"},
	}
	fmt.Println(m2)
}

泛型约束

// 自定义一个类型别名(将int8类型设置一个别名)
type int8A int8

// 自定义一个泛型约束
type myInt interface {
	// 当类型设置了别名,在使用的时候要么在后面把这个别名约束也加进去
	//int | int8 | int16 | int32 | int64 | int8A
	// 要么在这个类型前面,加一个 ~ 符号,因为类型的别名是这个类型的衍生类型,在类型前面加 ~ 号就可以适配这个类型的全部衍生类型了
	int | ~int8 | int16 | int32 | int64
}

// 给泛型约束定义一个比较大小的泛型函数
func getMaxNum[T myInt](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func 泛型约束的使用() {
	//var a int = 10
	var a int8A = 10
	//var b int = 20
	var b int8A = 20
	fmt.Println(getMaxNum(a, b))
}

泛型完整代码

package main

import "fmt"

/*
泛型:内置的泛型类型 any 和 comparable
any:表示go里面所有的内置基本类型,等价于 interface{}
comparable:表示go里面所有内置的可比较类型:`int、uint、float、booi.struct、指针` 等一切可以比较的类型
*/
func printArr[T any](arr []T) {
	for _, item := range arr {
		fmt.Println(item)
	}
}

// 自定义一个切片泛型
type mySlice[T int | float64] []T

// 给自定义的切片泛型添加一个求和方法
func (s mySlice[T]) sum() T {
	var sum T
	for _, v := range s {
		sum += v
	}
	return sum
}

// 泛型函数
func add[T int | float64 | float32 | string](a T, b T) T {
	return a + b
}

// 泛型map
type myMap[K string | int, V any] map[K]V
type User struct {
	Name string
}

// 自定义一个类型别名(将int8类型设置一个别名)
type int8A int8

// 自定义一个泛型约束
type myInt interface {
	// 当类型设置了别名,在使用的时候要么在后面把这个别名约束也加进去
	//int | int8 | int16 | int32 | int64 | int8A
	// 要么在这个类型前面,加一个 ~ 符号,因为类型的别名是这个类型的衍生类型,在类型前面加 ~ 号就可以适配这个类型的全部衍生类型了
	int | ~int8 | int16 | int32 | int64
}

// 给泛型约束定义一个比较大小的泛型函数
func getMaxNum[T myInt](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	//泛型的基本使用()
	//切片泛型的使用()
	//map泛型的使用()
	泛型约束的使用()
}

// 泛型的使用
func 泛型的基本使用() {
	strArr := []string{"白夜", "炽翎"}
	intArr := []int{1, 2}
	printArr(strArr)
	printArr(intArr)
}

// 泛型函数与方法
func 切片泛型的使用() {
	// 往自定义的切片泛型里,添加int类型的值
	var i mySlice[int] = []int{1, 2, 3, 4}
	fmt.Println(i.sum()) // 可以直接调用为切片泛型添加的一个求和方法
	// 往自定义的切片泛型里,添加float64类型的值
	var f mySlice[float64] = []float64{1.5, 2.7, 3.89, 4.55}
	fmt.Println(f.sum())

	//fmt.Println(add[int](1, 2))
	fmt.Println(add(1, 2)) // 调用时,可以自动推导传入的参数的类型
	//fmt.Println(add[string]("hh", "66"))
	fmt.Println(add("hh", "66"))
	//fmt.Println(add[float64](1.6, 2.8))
	fmt.Println(add(1.6, 2.8))
}

func map泛型的使用() {
	m1 := myMap[string, string]{
		"key": "符华",
	}
	fmt.Println(m1)
	m2 := myMap[int, User]{
		0: User{"符华"},
	}
	fmt.Println(m2)
}

func 泛型约束的使用() {
	//var a int = 10
	var a int8A = 10
	//var b int = 20
	var b int8A = 20
	fmt.Println(getMaxNum(a, b))
}

接口

接口:用 type 和 interface 关键字定义

定义格式:

type 接口名 interface {
	接口方法1(参数1 参数类型.....) [返回类型]
	接口方法2() [返回类型]
	接口方法3()
	...
	接口方法n() [返回类型]
}

接口可以将不同的类型绑定到一组公共的方法上,从而实现多态。(提高代码的复用率)

Go中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。(不用像Java一样,用implements关键字指定实现哪个接口)

因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态。

// 定义一个 寸劲 接口
type 寸劲 interface {
	// 这个接口里面有这几个方法
	寸劲开天(days int) string // 有参数,有返回值的方法
	寸劲山崩() string         // 无参数,有返回值的方法
	寸劲岩破()                // 无参数,无返回值的方法
}

// 定义一个 太虚剑气 接口
type 太虚剑气 interface {
	太虚剑神(days int) string
}

// 定义一个函数,以空接口作为参数(可以传任何类型的参数)
func dataPrint(datas ...interface{}) {
	for i, x := range datas {
		switch x.(type) {
		case bool:
			fmt.Printf("参数 #%d 是一个bool类型,它的值是:%v\n", i, x)
		case string:
			fmt.Printf("参数 #%d 是一个string类型,它的值是:%v\n", i, x)
		case int:
			fmt.Printf("参数 #%d 是一个int类型,它的值是:%v\n", i, x)
		case float64:
			fmt.Printf("参数 #%d 是一个float64类型,它的值是:%v\n", i, x)
		case nil:
			fmt.Printf("参数 #%d 是一个nil类型,它的值是:%v\n", i, x)
		default:
			fmt.Printf("参数 #%d 是其他类型,它的值是:%v\n", i, x)
		}
	}
}

// 定义一个用户学习结构体,来实现接口所有个方法(一个类型实现了接口的所有方法,即实现了该接口)
type 学习 struct {
	name string
}

// 定义一个结构体特有的方法
func (x 学习) 开始学习() string {
	return fmt.Sprint(x.name, "现在要开始学习了.....")
}

// 实现 寸劲开天 接口(这里也可以用指针 x *学习,用了指针后,那么赋值的时候也需要传指针类型:&学习{"符华"})
func (x *学习) 寸劲开天(days int) string {
	return fmt.Sprint(x.name, "学了", days, "天,学完了寸劲开天")
}

// 实现 寸劲山崩 接口
func (x 学习) 寸劲山崩() string {
	return fmt.Sprint(x.name, "学完了寸劲山崩")
}

// 实现 寸劲岩破 接口
func (x 学习) 寸劲岩破() {
	fmt.Println(x.name, "学完了寸劲岩破")
}

// 实现 太虚剑神 接口
func (x *学习) 太虚剑神(days int) string {
	return fmt.Sprint(x.name, "学了", days, "天,学完了太虚剑神")
}

func main() {
	接口的使用()
}

func 接口的使用() {
	u := 学习{"符华"}
	var cj 寸劲
	//cj = u // 接口赋值为 学习 结构体,只有当实现了接口的全部方法才能赋值给接口,否则无法赋值
	cj = &u                     // 只要接口方法有一个指针实现,则此处必须是指针
	if u1, ok := cj.(*学习); ok { // 通过类型断言,来调用 结构体 独有的方法
		fmt.Println(u1.开始学习())
	}
	cj.寸劲岩破()
	fmt.Println(cj.寸劲山崩())
	fmt.Println(cj.寸劲开天(2))

	/*
		类型断言:由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
		语法:接口.(类型),类型不是什么类型都可以传,必须要 接口 原先指向什么类型,那么就传什么类型
		返回两个值,可以通过返回的 true、false 来判断断言(转换)是否成功
	*/
	//var jq 太虚剑气
	//jq, ok := cj.(太虚剑气)
	if jq, ok := cj.(太虚剑气); ok { // 如果转换成功,ok为true
		fmt.Println(jq.太虚剑神(10))
	} else {
		fmt.Println("转换失败")
	}

	var a interface{}
	a = u // 将 u 赋值给a,然后将 a 重新赋值给一个 学习 类型的变量,这就需要用 类型断言
	var u1 学习
	//u1 = a // 这里不可以直接赋值,需要使用类型断言
	u1 = a.(学习) // a 原先指向 学习 类型,所以传类型时也必须要传 学习 类型
	fmt.Println(u1)

	// 空接口
	dataPrint(u, "空接口", 123, 12.65, []int{1, 2, 3}, make(map[string]string, 2))
}

反射

Go中,使用反射需要导入 reflect

使用反射时,主要有两个很重要的方法:

  • reflect.TypeOf(变量名),获取变量的类型,返回 reflect.Type 类型(是一个接口)
  • reflect.ValueOf(变量名),获取变量的值,返回 reflect.Value 类型(是一个结构体类型)

变量、interface{} 和 reflect.Value 是可以相互转换的,如下图:

【Java转Go】快速上手学习笔记(四)之基础篇三_第1张图片

package main

import (
	"fmt"
	"reflect"
)

/*
反射:需要导入 reflect 包
主要有两个函数:
	reflect.TypeOf(变量名),获取变量的类型,返回 reflect.Type 类型(是一个接口)
	reflect.ValueOf(变量名),获取变量的值,返回 reflect.Value 类型(是一个结构体类型)
	变量、interface{}和 reflect.Value 是可以相互转换的
*/

type student struct {
	Name string `json:"name"`
	Age  int
}

// 给 student 结构体绑定方法
func (s student) PrintStu() {
	fmt.Println(s)
}
func (s student) GetSum(a, b int) {
	fmt.Println(a + b)
}

// 基本数据类型、interface{}、reflect.Value 相互转换
func reflectTest01(a interface{}) {
	// 通过反射获取传入的变量的 type
	rTyp := reflect.TypeOf(a)
	fmt.Println("rTyp=", rTyp)

	// 获取到 reflect.Value
	rVal := reflect.ValueOf(a)
	n1 := 10 + rVal.Int() // 通过反射来获取变量的值,要求数据类型匹配:reflect.Value.Int()、reflect.Value.String()、reflect.Value.Float()......
	fmt.Println(n1)
	fmt.Printf("rVal=%v , rVal的类型=%T\n", rVal, rVal)

	// 将 reflect.Value 转回 interface{}
	iV := rVal.Interface()
	// 将 interface{} 通过断言转回 需要的类型
	n2 := iV.(int)
	fmt.Println(n2)
}

// 对结构体的反射
func reflectTest02(a interface{}) {
	// 通过反射获取传入的变量的 type
	rTyp := reflect.TypeOf(a)
	fmt.Println("rTyp=", rTyp)

	// 获取到 reflect.Value
	rVal := reflect.ValueOf(a)
	fmt.Printf("rVal=%v , rVal的类型=%T\n", rVal, rVal)
	fmt.Println("kind=", rVal.Kind(), rTyp.Kind())

	// 将 reflect.Value 转回 interface{}
	iV := rVal.Interface()
	// 通过反射来获取结构体的值,需要先断言
	// 将 interface{} 通过断言转回 需要的类型
	stu := iV.(student)
	fmt.Println(stu)
}

// 通过反射改变值(必须传入指针,才能改变值)
func reflectTest03(a interface{}) {
	rTyp := reflect.TypeOf(a) // 通过反射获取传入的变量的 type
	fmt.Println("rTyp=", rTyp)
	rVal := reflect.ValueOf(a) // 获取到 reflect.Value
	switch a.(type) {          // 判断传入的参数的类型
	case *int:
		n1 := 10 + rVal.Elem().Int() // 通过反射来获取变量的值,因为传入的是指针,所以要先用 Elem() 再获取值
		fmt.Println(n1)
		fmt.Printf("rVal=%v , rVal的类型=%T\n", rVal.Elem(), rVal)
		rVal.Elem().SetInt(200) // 通过反射改变值
	case *student:
		e := rVal.Elem()
		e.FieldByName("Name").SetString("白夜") // 给指定的字段名改变值
	}
}

// 通过反射遍历结构体的方法和属性
func reflectTest04(a interface{}) {
	rTyp := reflect.TypeOf(a)
	if rTyp.Kind() != reflect.Struct { // 判断传入的参数是否是结构体
		return
	}
	rVal := reflect.ValueOf(a)
	// 遍历结构体字段
	numField := rTyp.NumField() // 获取结构体字段的数量
	fmt.Println("numField =", numField)
	for i := 0; i < numField; i++ {
		// 打印字段的类型、字段名、字段值、字段标签
		fmt.Println(rTyp.Field(i).Type, rTyp.Field(i).Name, "=", rVal.Field(i), rTyp.Field(i).Tag.Get("json"))
	}
	// 遍历结构体方法
	numMethod := rTyp.NumMethod() // 获取结构体方法的数量
	// 关于方法遍历时,方法的索引:是根据方法名称的ACSII码来排序的
	for i := 0; i < numMethod; i++ {
		// 打印方法的类型、方法名
		//fmt.Println(rTyp.Method(i).Type, rTyp.Method(i).Name)
		if i == 0 {
			var params []reflect.Value
			params = append(params, reflect.ValueOf(10))
			params = append(params, reflect.ValueOf(20))
			rVal.Method(i).Call(params)
		} else {
			rVal.Method(i).Call(nil)
		}
	}
}

func main() {
	// 基本数据类型、interface{}、reflect.Value 相互转换
	//var num int = 100
	//reflectTest01(num)
	//reflectTest03(&num) // 修改值必须传指针
	//fmt.Println("通过反射改变num的值", num)

	stu := student{"符华", 20}
	//reflectTest02(stu)
	//reflectTest03(&stu) // 修改值必须传指针
	//fmt.Println("通过反射改变stu的值", stu)

	reflectTest04(stu)
}

协程

接下来我们讲协程

协程:一个进程有多个线程,一个线程可以起多个协程

特点

  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程

主线程结束后,协程会被中断,这时需要一个有效的阻塞机制。

WaitGroup

如果主线程退出了,即使协程还没有执行完毕,也会退出。这时,我们可以使用WaitGroup,它用于等待一组协程的结束。

  • 父线程调用Add方法来设定应等待的协程的数量。
  • 每个被等待的协程在结束时应调用Done方法。
  • 同时,主线程里可以调用Wait方法阻塞至所有协程结束。

goroutine的调度模型:MPG模型

  • M:操作系统的主线程(是物理线程)
  • P:协程执行需要的上下文
  • G:协程

使用 goroutine 效率高,但是会出现并发/并行安全问题,需要加锁解决这个问题。

如果协程发生异常,可以用recover来捕获异常,进行除了。这样主函数不会受到影响,可以继续执行。

package main

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

// 一个函数,每隔1秒输出
func goroutineTest01() {
	for i := 0; i < 10; i++ {
		fmt.Println("test() hello,world " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

var (
	myMap = make(map[int]int, 10)
	// 定义一个全局的互斥锁
	lock sync.Mutex     // sync 同步的意思,Mutex 互斥的意思
	wg   sync.WaitGroup // 用于等待一组线程的结束
)

func goroutineTest02(n int) {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	lock.Lock()    // 写之前加锁
	myMap[n] = res // concurrent map writes 并发写入问题
	lock.Unlock()  // 写完之后解锁
	//wg.Add()中有20个需要执行的协程,每执行完一个后调用wg.Done(),让协程数量-1,直到协程数量为0,表示全部协程执行完毕
	wg.Done() // 这里表示每执行完一个协程,wg.Add()里面的数量-1
}

func main() {
	协程()
}

func 协程() {
	//goroutineTest01() // 如果这样调用,这里是先执行完goroutineTest01,再执行main里面的打印
	//go goroutineTest01() // 开启了一个线程,这样goroutineTest01和main就是同时执行
	//for i := 0; i < 10; i++ {
	//	fmt.Println("main() hello,world " + strconv.Itoa(i))
	//	time.Sleep(time.Second)
	//}

	//cpuNum := runtime.NumCPU() //获取电脑的cpu数量
	//fmt.Println("cpu个数:", cpuNum)
	// 可以自己设置使用多少个cpu
	//runtime.GOMAXPROCS(cpuNum - 1) // 预留一个cpu

	wg.Add(20) // 这里表示有20个协程需要执行
	// 开启多个协程
	for i := 1; i <= 20; i++ {
		go goroutineTest02(i)
	}
	wg.Wait() // 告诉主线程要等一下,等协程全部执行完了载退出
	fmt.Println("全部协程执行完毕")
	// 遍历输出map结果
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
}

func 协程异常捕获() {
	// 这里我们可以使用defer + recover来捕获异常
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("发生错误,错误信息:", err)
		}
	}()
	var myMap map[int]string
	myMap[0] = "Go" // map没有make,出现error
}

channel

channel也就是管道,一般情况下,我们是配合协程一起使用的。

channel管道:本质就是一个数据结构——队列

介绍

  • 数据是先进先出:FIFO:first in first out
  • 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
  • channel是有类型的,一个string的channel只能存放string类型数据
  • channel必须是引用类型,必须初始化才能写入数据,也就是需要make后才能使用

语法:

var 变量名 chan 数据类型变量名 = make(chan 数据类型, 容量) (使用make进行初始化)

举例:

var intChan chan int // 用于存放int数据
var mapChan chan map[int]string // 用于存放map[int]string数据
var perChan chan Pserson // 用于存放Pserson结构体数据
var perChan1 chan *Pserson //用于存放Pserson结构体指针数据
var perChan1 chan interface{} //可以存放任何类型数据,但是取的时候要注意用类型断言

channel遍历

  • 通常使用for-range方式进行遍历,不用取长度的方式来遍历管道是因为管道每取一次,长度就会变。

  • 在遍历时,如果管道没有关闭,会出现deadlock的错误;如果管道已经关闭,则正常遍历数据,遍历完后,退出遍历。

管道可以声明为只读或只写(默认情况下是双向的,也就是可读可写)

  • 只写:var intChan chan<- int
  • 只读:var intChan <-chan int

基本使用

func 管道() {
	// 创建一个可以存放3个int类型的管道
	var intChan chan int
	// 因为channel是引用类型,它的值其实是一个地址,然后这个地址指向的就是管道队列;然后intChan本身也有一个地址
	intChan = make(chan int, 3)
	fmt.Printf("intChan 的值=%v intChan 本身的地址=%p\n", intChan, &intChan) // intChan 的值=0xc00006e080 intChan 本身的地址=0xc00004c020
	// 向管道写入数据,写入、读取管道数据时,用 <- 表达式
	intChan <- 10
	num := 200
	intChan <- num
	// 设置的管道容量是3,最多只能往里面写入3条数据(长度不能超过容量)
	intChan <- 100
	// 管道的长度和cap(容量)
	fmt.Printf("管道 长度 = %v 容量 = %v\n", len(intChan), cap(intChan)) // 3,3
	// 读取管道的数据。从管道中取出了数据,可以再往里面放数据
	//<-intChan // 可以直接这么写,也是取出数据;不用变量接收,把取出的数据扔了不要
	n1 := <-intChan                                                // 这里取出来的是最先写入到管道里的数据(先进先出)
	fmt.Println("n1=", n1)                                         // 10
	fmt.Printf("管道 长度 = %v 容量 = %v\n", len(intChan), cap(intChan)) // 2,3
	// 取出了一条数据后再往里面放一条数据
	intChan <- 500
	close(intChan)                                                 // 关闭管道,这时就不能再往管道里面写入数据了,但是读取没问题
	fmt.Printf("管道 长度 = %v 容量 = %v\n", len(intChan), cap(intChan)) // 3,3
	// 在没有使用协程的情况下,如果管道数据已经全部取出,再取会报错
	n2 := <-intChan
	n3 := <-intChan
	fmt.Printf("n2 = %v n3 = %v\n", n2, n3)                        // 200,100
	fmt.Printf("管道 长度 = %v 容量 = %v\n", len(intChan), cap(intChan)) // 1,3

	// 遍历管道
	intChan2 := make(chan int, 100)
	for i := 0; i < 100; i++ {
		intChan2 <- i * 2
	}
	close(intChan2) // 管道写完数据后,先将管道关闭,再进行遍历
	// 不能用取长度的方式来遍历管道,因为管道每取一次,长度就会变,要用 range 方式遍历
	for v := range intChan2 { // 这里只返回一个数据,管道里面没有下标
		fmt.Println("v =", v)
	}
}

和协程一起使用

案例一

package main

import (
	"fmt"
	"sync"
)

// 全局 WaitGroup 变量
var wg sync.WaitGroup // 用于等待一组线程的结束

// 管道写入数据
func writeData(intChan chan int) {
	for i := 0; i < 50; i++ {
		intChan <- i
		fmt.Printf("writeData 写入数据=%v\n", i)
	}
	close(intChan)
	wg.Done() // 执行完一个线程后,调用这个方法,主线程中需要等待执行的协程数量-1
}

// 管道读取数据
func readData(intChan chan int) {
	for {
		v, ok := <-intChan
		if !ok {
			break
		}
		fmt.Printf("readData 读到数据=%v\n", v)
	}
	wg.Done() // 执行完一个线程后,调用这个方法,主线程中需要等待执行的协程数量-1
}

func main() {
	协程和管道应用1()
}

func 协程和管道应用1() {
	// 创建两个管道
	intChan := make(chan int, 10)
	wg.Add(2) // 说明开启了两个线程
	// 开启了两个协程,writeData和readData应该是交叉执行的
	go writeData(intChan) // 开启一个协程,往 intChan 中写入数据
	go readData(intChan)  // 开启一个协程,读取 intChan 的数据
	wg.Wait()             // 告诉主线程需要等待协程执行完毕
	fmt.Println("程序执行完毕!")
}

案例二

  • 需求:要求统计1-8000的数字中,哪些是素数

  • 将统计素数的任务,分配给4个协程去完成

// 判断是否为素数
func isPrime(intChan, primeChan chan int) {
	var isPrime bool // 标识是否是素数
	for v := range intChan {
		isPrime = true
		for i := 2; i < v; i++ {
			if v%i == 0 {
				isPrime = false
				break
			}
		}
		if isPrime { // 如果为素数,则往primeChan中写入数据
			primeChan <- v
		}
	}
	fmt.Println("isPrime 读取素数完毕")
	wg.Done() // 执行完一个线程后,调用这个方法,主线程中需要等待执行的协程数量-1
}

func 协程和管道应用2() {
	// 需求:要求统计1-8000的数字中,哪些是素数
	// 将统计素数的任务,分配给4个协程去完成
	intChan := make(chan int, 1000)   // 读写1-8000数字的管道
	primeChan := make(chan int, 2000) // 存储素数的管道
	wg.Add(5)                         // 下面开启了5个协程
	// 开启写入 1-8000 数字的协程
	go func() {
		for i := 1; i <= 8000; i++ {
			intChan <- i
		}
		close(intChan)
		wg.Done()
	}()

	// 开启4个读取 1-8000 数字,并统计素数的协程
	for i := 0; i < 4; i++ {
		go isPrime(intChan, primeChan)
	}

	wg.Wait()        // 等待协程执行完毕
	close(primeChan) // 关闭 primeChan 管道
	// 遍历primeChan,把结果取出来
	for v := range primeChan {
		fmt.Printf("素数是 = %v\n", v)
	}
}

select…case

传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock。

在实际开发中,可能不好确定什么时候关闭管道,这时可以使用select方式解决。

func 管道注意细节() {
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}
	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}
	// 传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
	// 在实际开发中,可能不好确定什么时候关闭管道,这时可以使用select方式解决
	for {
		select {
		// 这里如果intChan一直没有关闭,也不会一直阻塞而导致deadlock
		// 如果一个case取不到数据,会自动到下一个case中取
		case v := <-intChan:
			fmt.Println("从intChan读取的数据=", v)
		case v := <-stringChan:
			fmt.Println("从stringChan读取的数据=", v)
		default:
			fmt.Println("都取不到了")
			return
		}
	}
}

main.go 完整代码

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup // 用于等待一组线程的结束

// 管道写入数据
func writeData(intChan chan int) {
	for i := 0; i < 50; i++ {
		intChan <- i
		fmt.Printf("writeData 写入数据=%v\n", i)
	}
	close(intChan)
	wg.Done() // 执行完一个线程后,调用这个方法,主线程中需要等待执行的协程数量-1
}

// 管道读取数据
func readData(intChan chan int) {
	for {
		v, ok := <-intChan
		if !ok {
			break
		}
		fmt.Printf("readData 读到数据=%v\n", v)
	}
	wg.Done() // 执行完一个线程后,调用这个方法,主线程中需要等待执行的协程数量-1
}

// 判断是否为素数
func isPrime(intChan, primeChan chan int) {
	var isPrime bool // 标识是否是素数
	for v := range intChan {
		isPrime = true
		for i := 2; i < v; i++ {
			if v%i == 0 {
				isPrime = false
				break
			}
		}
		if isPrime { // 如果为素数,则往primeChan中写入数据
			primeChan <- v
		}
	}
	fmt.Println("isPrime 读取素数完毕")
	wg.Done() // 执行完一个线程后,调用这个方法,主线程中需要等待执行的协程数量-1
}

func main() {
	//管道()
	//协程和管道应用1()
	//协程和管道应用2()
	//管道注意细节()
}

func 管道() {
	// 创建一个可以存放3个int类型的管道
	var intChan chan int
	// 因为channel是引用类型,它的值其实是一个地址,然后这个地址指向的就是管道队列;然后intChan本身也有一个地址
	intChan = make(chan int, 3)
	fmt.Printf("intChan 的值=%v intChan 本身的地址=%p\n", intChan, &intChan) // intChan 的值=0xc00006e080 intChan 本身的地址=0xc00004c020
	// 向管道写入数据,写入、读取管道数据时,用 <- 表达式
	intChan <- 10
	num := 200
	intChan <- num
	// 设置的管道容量是3,最多只能往里面写入3条数据(长度不能超过容量)
	intChan <- 100
	// 管道的长度和cap(容量)
	fmt.Printf("管道 长度 = %v 容量 = %v\n", len(intChan), cap(intChan)) // 3,3
	// 读取管道的数据。从管道中取出了数据,可以再往里面放数据
	//<-intChan // 可以直接这么写,也是取出数据;不用变量接收,把取出的数据扔了不要
	n1 := <-intChan                                                // 这里取出来的是最先写入到管道里的数据(先进先出)
	fmt.Println("n1=", n1)                                         // 10
	fmt.Printf("管道 长度 = %v 容量 = %v\n", len(intChan), cap(intChan)) // 2,3
	// 取出了一条数据后再往里面放一条数据
	intChan <- 500
	close(intChan)                                                 // 关闭管道,这时就不能再往管道里面写入数据了,但是读取没问题
	fmt.Printf("管道 长度 = %v 容量 = %v\n", len(intChan), cap(intChan)) // 3,3
	// 在没有使用协程的情况下,如果管道数据已经全部取出,再取会报错
	n2 := <-intChan
	n3 := <-intChan
	fmt.Printf("n2 = %v n3 = %v\n", n2, n3)                        // 200,100
	fmt.Printf("管道 长度 = %v 容量 = %v\n", len(intChan), cap(intChan)) // 1,3

	// 遍历管道
	intChan2 := make(chan int, 100)
	for i := 0; i < 100; i++ {
		intChan2 <- i * 2
	}
	close(intChan2) // 管道写完数据后,先将管道关闭,再进行遍历
	// 不能用取长度的方式来遍历管道,因为管道每取一次,长度就会变,要用 range 方式遍历
	for v := range intChan2 { // 这里只返回一个数据,管道里面没有下标
		fmt.Println("v =", v)
	}
}

func 协程和管道应用1() {
	// 创建两个管道
	intChan := make(chan int, 10)
	wg.Add(2) // 说明开启了两个线程
	// 开启了两个协程,writeData和readData应该是交叉执行的
	go writeData(intChan) // 开启一个协程,往 intChan 中写入数据
	go readData(intChan)  // 开启一个协程,读取 intChan 的数据
	wg.Wait()             // 告诉主线程需要等待协程执行完毕
	fmt.Println("程序执行完毕!")
}

func 协程和管道应用2() {
	// 需求:要求统计1-8000的数字中,哪些是素数
	// 将统计素数的任务,分配给4个协程去完成
	intChan := make(chan int, 1000)   // 读写1-8000数字的管道
	primeChan := make(chan int, 2000) // 存储素数的管道
	wg.Add(5)                         // 下面开启了5个协程
	// 开启写入 1-8000 数字的协程
	go func() {
		for i := 1; i <= 8000; i++ {
			intChan <- i
		}
		close(intChan)
		wg.Done()
	}()

	// 开启4个读取 1-8000 数字,并统计素数的协程
	for i := 0; i < 4; i++ {
		go isPrime(intChan, primeChan)
	}

	wg.Wait()        // 等待协程执行完毕
	close(primeChan) // 关闭 primeChan 管道
	// 遍历primeChan,把结果取出来
	for v := range primeChan {
		fmt.Printf("素数是 = %v\n", v)
	}
}

func 管道注意细节() {
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}
	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}
	// 传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
	// 在实际开发中,可能不好确定什么时候关闭管道,这时可以使用select方式解决
	for {
		select {
		// 这里如果intChan一直没有关闭,也不会一直阻塞而导致deadlock
		// 如果一个case取不到数据,会自动到下一个case中取
		case v := <-intChan:
			fmt.Println("从intChan读取的数据=", v)
		case v := <-stringChan:
			fmt.Println("从stringChan读取的数据=", v)
		default:
			fmt.Println("都取不到了")
			return
		}
	}
}

文件

文件这块没啥好说的,拿到函数直接用就行。需要注意一点就是文件file是一个指针类型。

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	//基本使用读()
	基本使用写()
}

func 基本使用读() {
	// 打开文件
	file, err := os.Open("C:\\Users\\Administrator\\Desktop\\1.txt")
	if err != nil {
		fmt.Println("文件打开错误:", err)
	}
	//fmt.Printf("file=%v", file) // 输出的是一个地址
	defer file.Close() // 当函数退出时,要关闭file,否则会有内存泄露

	// 创建一个Reader,是带缓冲,默认缓冲区为4096(这种方式比较适合大文件读取)
	reader := bufio.NewReader(file)
	for {
		str, err := reader.ReadString('\n')
		if err == io.EOF { // io.EOF表示读到了文件末尾,这时就可以退出循环了
			break
		}
		fmt.Print(str)
	}
	fmt.Println("文件读取完成")

	// ioutil.ReaderFile,一次性将文件读取到位 这种方法适合读取比较小的文件
	// 不过新版本 ioutil.ReadFile 已经弃用了,这个函数其实调用的就是 os.ReadFile
	content, err := os.ReadFile("C:\\Users\\Administrator\\Desktop\\学习计划.txt")
	if err != nil {
		fmt.Printf("文件读取失败:%v", err)
	}
	//fmt.Println(content) // content是一个 []byte
	fmt.Println(string(content)) // 所以要转成string

}

func 基本使用写() {
	filePath := "C:\\Users\\Administrator\\Desktop\\测试.txt"
	/*
		OpenFile 第二个参数:文件打开模式
			O_RDONLY int = syscall.O_RDONLY // 只读
			O_WRONLY int = syscall.O_WRONLY // 只写
			O_RDWR   int = syscall.O_RDWR   // 读写
			O_APPEND int = syscall.O_APPEND // 追加
			O_CREATE int = syscall.O_CREAT  // 如果不存在就创建
			O_EXCL   int = syscall.O_EXCL   // 文件必须不存在
			O_SYNC   int = syscall.O_SYNC   // 同步io
			O_TRUNC  int = syscall.O_TRUNC  // 打开时清空文件(一般用于覆盖写入)
		第三个参数只作用于linux系统,Windows系统不起作用
	*/
	file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		fmt.Printf("文件打开失败:%v", err)
	}
	defer file.Close()
	str := "hello,Golang\n"
	// NewWriter 带缓冲区的写入,写完之后要用flush刷新。
	writer := bufio.NewWriter(file)
	for i := 0; i < 5; i++ {
		writer.WriteString(str)
	}
	writer.Flush()
}

ok,以上就是本篇的全部内容了。下一篇可能是关于网络请求相关的,也有可能是Gorm相关的。

你可能感兴趣的:(Go,golang,go)