Golang学习笔记-魂师篇

八、包和文件

8.1、包

go的源码复用是建立在包(package)上的,包通过package ,import ,GOPATH操作完成

8.1.1、包的组成

  • go语言的包是Go源码的集合

  • 可见性:如果一个包中要应用另外一个包中的标识符(变量、常量、类型、函数等)时,该标识符
    必须是对外可见的(public),首字母必须大写

  • 注意事项:

    • 1、import 导入语句通常放在文件开头的包声明语句的下面
    • 2、导入的包名需要使用双引号包裹起来
    • 3、包名是从$GOPATH/src后开始计算的,使用/进行路径分隔
    • 4、GO语言中禁止循环导入语句 //包A找包B,包B找包C,包C找包A
  • 匿名导入包: import _ "要导入的包名" ,如果只希望导入包,而不使用包内的数据时,可以使用
    匿名导入包。匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中,其实是只执行
    包内的init()方法

  • inint() 在GO语言执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是: init()
    函数没有参数也没有返回值。init()函数在程序运行时自动被调用执行,不能在代码中主动调用
    它。

  • 包初始化执行的顺序如下:全局声明-->init() --> main()

  • 一个包中只能定义一个init()

// main.go 
package main
import (
	mm "pk" //默认从GOPATH/src 目录下找,注意pk是目录名 pk目录下的go文件声明"package pk"
	//mm时 pk包的别名
	"fmt"
)
//全局变量先声明,然后才是init()
const pai = 3.14
var x = 100
func init(){
	fmt.Println("我属于main包")
	fmt.Println(pai,x)
}

func main(){
	sum := mm.Add(10,20)
	fmt.Println(sum)
}
//我属于pk包
//我属于main包
//3.14 100
//30
#pk/add.go
package pk
import "fmt"
func init(){
fmt.Println("我属于pk包")
}
//包中的 标识符(变量名、函数名、结构体、接口等)如果首字母是小写的表示私有(只能在当前包中使用)。首
字母大写的标识符可以被外部的包调用
func Add(x,y int) int {
	return x+y
}
main(import A),A(import B),B(import c);执行顺序为:C.init()->B.init()->A.init()->main.init()

8.1.2、工程管理

  • 同级目录多文件

同一个project下 "func main()"只有一个,"package main"在同一级别目录下都可以定义。
同一级不同文件,func 不能直接执行,必须由main函数调用执行

场景1:执行多个文件

#...src/main.go 
package main

import (
	"fmt"
)

func main() {
	fmt.Println("hello world")
	test()
}

#...src/test.go
package main

import (
	"fmt"
)

func test() {
	fmt.Println("test func")
}

#终端执行
go build 或者go build test.go main.go ,然后运行exe文件
或者:go run main.go test.go 也可以,对单个(main.go或者test.go) build都会失败
  • 不同级目录多文件

同级别目录,package 名称要一致;不同目录 package 不一致
子包的函数名首字母要大写,才能被其他包调用到

# ...src/sub/sub.go
package sub

import (
	"fmt"
)

var Pai float32 = 3.14

func Sub(x, y int) int {
	fmt.Println("sub Func")
	x *= y
	return x
}


# ...src/sub/add.go
package sub

import (
	"fmt"
)

func Add(a, b int) int {
	fmt.Println("add Func")
	a += b
	return a
}


# ... src/main.go
package main

import (
	"fmt"
	"sub"
)

func main() {
	fmt.Println("hello world")
	test()

	a, b := 2, 12
	sum := sub.Add(a, b)
	x := sub.Sub(a, b)
	fmt.Println(sum, x) //14,24

	var f1 func(int, int) int
	f1 = sub.Add          //注意 sub.Add和sub.Add() 是两个含义,一个是函数本身,一个是函数调用
	fmt.Println(f1(a, b)) // 14
	f1 = sub.Sub
	fmt.Println(f1(a, b)) //24
	fmt.Println(sub.Pai)  //3.14

	tmp := func(f1 func(int, int) int, a int, b int) int {
		return f1(a, b)
	}
	a1 := tmp(sub.Add, a, b)
	a2 := tmp(sub.Add, a, b)
	fmt.Println(a1, a2)
}

注意:cd %GOPATH%\src; go install sub 才会生成pkg的归档文件,在%GOPATH%\src目录下才有效

8.1.3.、小结

  • 包的定义:package 关键字,包名通常是目录名,文件中存放的都是.go文件
  • 单号导入包、多行导入包,包的别名,匿名导入->sql包导入的时候会用到
  • go不支持循环导入
  • 包中标识符(var名,func名字,const名,接口名称,struct名...)的可见性,首字母大写
  • go导入路径是从GOPATH/src目录下,go module有特殊
  • init()导入的时候回自动执行,一个包只有一个init()没有参数和返回值并且不能被直接调用。多
    用于初始化操作
  • 多个包的init顺序
  • 接口是一种抽象的类型。接口就是要实现的一个方法的清单
  • 接口的定义:```type mover interface
  • 接口的实现,实现了接口的所有方法,实现了接口就可以当成这个接口类型的变量
  • 接口变量,实现了一个万能的变量,可以保存所有实现了这个接口的类型的值,通常是作为函数
    的参数出现
  • 空接口:接口中没有定义任何方法,也就是说任意类型都实现了空接口。任何类型都可以存到空接
    口定义的接口变量中 interface{}
  • 空接口的应用场景:1、作为函数参数fmt.Println() ,2、map[string]interface{} value想要
    存储任意类型;接口底层:1、动态类型 2、动态值
  • 类型断言: 判断动态类型,做类型断言的前提是一定要是一个接口类型的var, x.(T) 。使用
    switch来做断言
func test1(){
    var a interface{} //定义一个空接口变量
    a = 100
    //如何判断a的类型
    //类型断言,方法1 x.(T) 方法2 switch
    v,ok := a.(int8);
    if !ok {
        fmt.Println("猜错了,类型不是: int8",v)
        //return
    }
    switch a.(type) {
        case int8:
        	fmt.Println("类型是:int8")
        case int16:
        	fmt.Println("类型是:int16")
        case int32:
        	fmt.Println("类型是:int32")
        default:
        	fmt.Printf("猜错了,类型是:%T\n",a)
    }
}

8.2、文件

8.2.1、创建文件

go提供了Create()函数专门创建文件,首先会判断文件是否存在,如果不存在,就创建,如果存在就清空然后再打开。文件被创建后,会被默认打开。不需要再执行打开操作。

func file1() {
	filePtr, err := os.Create("D:/Program_language/PRD/PG1/src/test.txt") //返回一个文件类型的指针,路径分隔符为"/"
	if err != nil {
		fmt.Println(err)
	}
	defer filePtr.Close() //注意关闭
}

8.2.2、打开/读取文件

  • 文件:存储在计算机上的数据集合。文件分为文本文件和二进制文件

  • 打开和关闭文件,返回一个*file和一个err 。对得到的文件实例调用close()方法能够关闭文件。
    os.Open,os.Close()

  • 读取方法:file.Read(用于自定义一次读取多少字节),bufio.NewReader(按行读),ioutil.Readfile(一次
    读完)

  • os.Open只能只读打开,os.OpenFile可以指定打开时的模式

    //读取方法1:io.Read版本
    import (
        "os"
        "fmt"
        "io"
    )
    func main(){
        fileObj,err := os.Open("../06/main.go") //支持绝对路径和相对路径(相对于改文件的路径)
        if err != nil {
       		fmt.Println("open file error.",err)
        }
        defer fileObj.Close() //注意及时关闭
     	f, err := fileObj.WriteString("fuckBMC") //不能够写入,因为os.Open只能以read-only的方式打开
    	fmt.Println(f, err)                      //D:/Program_language/PRD/PG1/src/test.txt: Access is denied.
        //读文件
        var tmp = make([]byte,128) //创建一个128byte的
        for { //循环度,每次读取128 byte
            n, err := fileObj.Read(tmp[:]) //一次读取128个byte ,然后存放到tmp中
            if err == io.EOF || n == 0 {
            	fmt.Println("读取完毕")
            break //注意这里要跳出循环要用break
            }
            if err != nil {
                fmt.Println("read failed..", err)
                return
            }
            //fmt.Printf("读取了%v字节\n", n)
            fmt.Println(string(tmp[:n]))
            if n < 128 { //这里感觉有问题...保留意见
                return
            }
        }
    }
    
    //读取方法2:bufio
    //bufio是在file的基础上封装了一层API,支持更多的功能
    func readFromBufio(){
        fileObj,err := os.Open("../06/main.go")
        if err != nil {
            fmt.Println("open file error.",err)
            return
        }
        defer fileObj.Close()
        reader := bufio.NewReader(fileObj)
        for {
            line,err := reader.ReadString('\n') //字符'\n',按行读,可以指定其他字符作为换行符
            if err == io.EOF {
                fmt.Println("文件读完了")
                break
            }
            if err != nil {
                fmt.Println("read file error.",err)
                return
            }
            fmt.Print(line)
        }
    }
    
    
    //读取方法3:ioutil,集成了打开和关闭文件,不再需要os.Open/os.Close
    func readFromioutil() {
        ret,err := ioutil.ReadFile("../06/main.go")
        if err != nil {
            fmt.Println("read error,err ",err)
        }
    	fmt.Println(string(ret))
    }
    

8.2.3、文件写入

Write和WriteString以及WriteAt

func write1() {
	filePtr, err := os.Create("D:/Program_language/PRD/PG1/src/test.txt") //返回一个文件类型的指针,路径分隔符为"/"
	if err != nil {
		fmt.Println(err)
	}
	filePtr.WriteString("你好,世界! ")
	filePtr.WriteString("你好,北京!a ") //文件为一行,并没有自动换行。

	filePtr.WriteString("\r\n世界第一等") //在windows中\n不会换行,`\r\n` 结合才能实现换行。\r为回车

	defer filePtr.Close() //注意关闭
}

func write2() {
	filePtr, err := os.Create("D:/Program_language/PRD/PG1/src/test.txt") //返回一个文件类型的指针,路径分隔符为"/"
	if err != nil {
		fmt.Println(err)
	}

	defer filePtr.Close() //注意关闭

	slice := []byte{'h', 'e', 'l', 'l', 'o'} //写法1
	count, err := filePtr.Write(slice)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(count)
	}
	count, err = filePtr.Write([]byte("\r\n你好世界")) //写法2,这里每个汉字占用3个byte。用windows自己建立文件写入内容的话是一个中文2个byte
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(count) //14
	}

}

func write3() {
	filePtr, err := os.Create("D:/Program_language/PRD/PG1/src/test.txt") //返回一个文件类型的指针,路径分隔符为"/"
	if err != nil {
		fmt.Println(err)
	}

	defer filePtr.Close() //注意关闭,获取光标位置
	filePtr.Write([]byte("\r\n你好世界"))

	//1、查找光标位置
	// count, _ := filePtr.Seek(0, os.SEEK_END)
	count, _ := filePtr.Seek(0, io.SeekEnd)
	fmt.Println(count)
	filePtr.WriteAt([]byte("北京欢迎你!"), count) //在文档尾部插入
}
  • os.OpenFile 函数能够以指定模式打开文件,从而实现文件写入相关功能
  • 写入方式:
    • 1、Write和WriteString以及WriteAt
      • WriteString:默认从0开始写入,覆盖模式
    • 2、bufio.NewWriter
    • 3、ioutil.WriteFile
func OpenFile(name string, flag int, perm FileMode) (*File, error) { //文件名,打开模式,打开权限	testlog.Open(name)    return openFileNolog(name, flag, perm)}name:要打开的文件名flag:打开文件的模式,模式如下:(Liunx上才有用,win上没用)
  • 模式含义
    • os.O_WRONLY 只写
    • os.O_CREATE 创建文件
    • os.O_RDONLY 只读
    • os.O_RDWR 读写
    • os.O_TRUNC 清空
    • os.O_APPEND 追加
  • 权限含义:取值范围(0-7)
    • 0 无权限
    • 1 执行
    • 2 写入权限
    • 3 写权限和执行权限
    • 4 读权限
    • 5 读权限和执行权限
func write1(){
    fileObj,err := os.OpenFile("xx.txt",os.O_WRONLY|os.O_APPEND|os.O_CREATE,0644) //如果
    不存在就创建,或 关系
    if err !=nil {
    	fmt.Println(err)
	}
    fileObj.Write([]byte("我们都是好好孩子\n"))
    fileObj.WriteString("hello world")
    fileObj.Close()
}
//清空写入
func write2(){
    fileObj,err := os.OpenFile("xx.txt",os.O_WRONLY|os.O_CREATE|os.O_TRUNC,0644) //按位
    或后的值:只写|创建|清空 合并后的权限就是
    //如果不存在就创建,存在就清空,只写方式打开
    if err !=nil {
    	fmt.Println(err)
    }
    fileObj.Write([]byte("我们都是好好孩子\n"))
    fileObj.WriteString("hello world")
    fileObj.Close()
}
func write3(){
    fileObj,err := os.OpenFile("xx.txt",os.O_WRONLY|os.O_CREATE|os.O_TRUNC,0644)
    if err !=nil {
    	fmt.Println(err)
    }
    defer fileObj.Close()
    //创建一个写对象
    rd := bufio.NewWriter(fileObj)
    rd.WriteString("你好,中国")
    rd.Flush() //bufio是写入到缓存中,刷写缓存到磁盘
}
func write4(){
    str := "你好世界"
    err := ioutil.WriteFile("xx.txt",[]byte(str),0666) //覆盖写入
    if err != nil {
        fmt.Println(err)
        return
    }
}

  • 获取用户输入时如果需要输入空格
func show1(){
    var s string
    fmt.Print("请输入与你要输入的内容: ") //如果要输入的内容有空格,就...
    fmt.Scanln(&s)
    fmt.Printf("输入内容: %v\n",s)
}
func useBufio(){
    var s string
    fmt.Print("请输入与你要输入的内容: ")
    reader := bufio.NewReader(os.Stdin)
    s,_ = reader.ReadString('\n')
    fmt.Printf("输入内容: %v\n",s)
}

8.2.4、Copy文件

func Copyfile(src,dst string)(written int64,err error){
    sd,err := os.Open(src)
    if err !=nil {
    	fmt.Println("src file open failed.",src)
    }
    defer sd.Close()
    dd,err := os.Open(dst)
    if err !=nil {
    	fmt.Println("src file open failed.",dst)
    }
    defer dd.Close()
    return io.Copy(dd,sd)
}
func main(){
    _,err := Copyfile("xx.txt","dst.txt.go")
    if err != nil {
    	fmt.Println(err)
    }
}

8.2.5、Cat文件

package main
import (
   "bufio"
   "flag"
   "fmt"
   "io"
   "os"
)
// cat命令实现
func cat(r *bufio.Reader) {
   for {
      buf, err := r.ReadBytes('\n') //注意是字符
      if err == io.EOF {
         break
      }
      fmt.Fprintf(os.Stdout, "%s", buf)
   }
}
func main() {
   flag.Parse() // 解析命令行参数
   if flag.NArg() == 0 {
      // 如果没有参数默认从标准输入读取内容
      cat(bufio.NewReader(os.Stdin))
   }
   // 依次读取每个指定文件的内容并打印到终端
   for i := 0; i < flag.NArg(); i++ {
      f, err := os.Open(flag.Arg(i))
      if err != nil {
         fmt.Fprintf(os.Stdout, "reading from %s failed, err:%v\n", flag.Arg(i), err)
         continue
      }
      cat(bufio.NewReader(f))
   }
}

8.2.6、文件内容提取

需求:有如下文件a.txt,按要求提取pod名称,podip以及nodeip
[root@localhost ~]# cat $文件名
NAME          READY     STATUS    RESTARTS   AGE       IP           NODE
mysql-dr556   1/1       Running   1          11h       172.17.0.2   127.0.0.1
myweb-j9760   1/1       Running   1          11h       172.17.0.3   127.0.0.1
myweb-j9789   1/1       Running   1          11h       172.17.0.4   127.0.0.1

执行结果如下:
POD:mysql-dr556	IP:172.17.0.2	NODE_IP:127.0.0.1
POD:myweb-j9760	IP:172.17.0.3	NODE_IP:127.0.0.1
POD:myweb-j9789	IP:172.17.0.4	NODE_IP:127.0.0.1
文件读取完毕 !
/*
1)打开/关闭文件
2)按行读取文件
3)去除多余的空格、多余的行、多余的尾部换行符
4)执行切割
*/

//4、提取内容
func parseStr(a string) {
	//1、多个空格,合并为一个空格
	var tmp string
	signal := 0
	for _, v := range a {
		if string(v) == " " {
			if signal == 0 {
				tmp = tmp + string(v)
				signal++
			} else {
				continue
			}
		} else {
			tmp = tmp + string(v)
			signal = 0
		}
	}
	tmp2 := strings.Split(tmp, " ")
	fmt.Printf("POD:%v\tIP:%v\tNODE_IP:%v\n", tmp2[0], tmp2[5], tmp2[6])
}

func readFile() {
	//1、打开文件
	fileObj, err := os.Open("D:/Program_language/PRD/PG1/src/test.txt")
	if err != nil {
		fmt.Printf("打开文件错误: %v\n", err)
	}
	//2、注意及时关闭文件
	defer fileObj.Close()
	//3、按行读取文件
	reader := bufio.NewReader(fileObj)
	for {
		line, err := reader.ReadString('\n')
		//3.1、排除标题
		if strings.Contains(line, "kubectl") || strings.Contains(line, "NAME") {
			continue
		}
		if err == io.EOF {
			parseStr(line)
			fmt.Println("文件读取完毕 !")
			break
		}
		if err != nil {
			fmt.Println("read file error: ", err)
			return
		}
		line = strings.ReplaceAll(line, "\r\n", "") //替换尾部的换行符
		parseStr(line)
	}
}

func main() {
	readFile()
}

//提取方式2
func parseStr(a string) {
	tmp, _ := regexp.Compile(` +`) //匹配一个或者多个空格符
	tmp3 := tmp.ReplaceAllString(a, " ")
	tmp2 := strings.Split(tmp3, " ")
	fmt.Printf("POD:%v\tIP:%v\tNODE_IP:%v\n", tmp2[0], tmp2[5], tmp2[6])
}

8.2.7、断点续传

seek方法

type Seeker interface { //设置要读取的偏移量
    Seek(offset int64, whence int) (int64, error) //offset偏移多少字节,whence从哪里开始偏移{1.相对于文件开头,2.光标当前位置,3.文件末尾}
}
func main() {
	//文件内容: abcdefghijk
	fileName := "D:/Program_language/PRD/PG1/src/a.txt"
	file, err := os.OpenFile(fileName, os.O_RDWR, os.ModePerm)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	//1、读取两个字符
	var bs = make([]byte, 2)
	file.Read(bs)
	fmt.Println(string(bs)) //ab
	file.Read(bs)
	fmt.Println(string(bs)) //cd

	file.Seek(3, io.SeekStart) //io.SeekStart|io.SeekEnd|io.SeekCurrent 对应0|1|2
	file.Read(bs)
	fmt.Println(string(bs)) //de

}

端点续传

应用场景:

  • 传输的文件较大,如何缩短耗时
  • 传输中断,是否可以从中断处重新开始
  • 传输文件时如何暂停和恢复

核心在于记录传输了多少数据进行记录

package main

import (
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
	"strings"
)

func HandlErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	//1、定义文件
	srcFile := "D:/PERSON_BOOK/3.pdf"
	destFile := "new_" + srcFile[strings.LastIndex(srcFile, "/")+1:]
	tempFile := destFile + ".temp"
	fmt.Println(tempFile)

	sFile, err := os.Open(srcFile)
	HandlErr(err)
	dFile, err := os.OpenFile(destFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
	HandlErr(err)
	tFile, err := os.OpenFile(tempFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
	HandlErr(err)

	defer sFile.Close()
	defer dFile.Close()

	//获取文件大小,计算百分比
	df, _ := sFile.Stat()
	fmt.Println(df.Size())

	//2、读取temp文件中记录的位置
	tFile.Seek(0, io.SeekStart)   //复位temp 的光标位置
	buf := make([]byte, 100, 100) //用于保存读取位置信息
	n, err := tFile.Read(buf)
	if err == io.EOF && err != nil { //首次打开文件为 io.EOF
		fmt.Println("首次传输从0开始====>")
	} else {
		HandlErr(err)
	}

	countStr := string(buf[:n]) //读取位置 并转换为int
	count, err := strconv.ParseInt(countStr, 10, 64)
	// HandlErr(err)

	//3、根据count设置偏移量
	rNum := -1                               //读取了多少数据
	wNum := -1                               //写入了多少数据
	data := make([]byte, 512*1024, 512*1024) //用于保存读取出来的数据,每次读取512k

	for {
		sFile.Seek(count, io.SeekStart) //定位光标位置
		dFile.Seek(count, io.SeekStart) //从文件开头开始

		rNum, err = sFile.Read(data)
		if err == io.EOF || rNum == 0 {
			fmt.Println("文件读取完毕")
			tFile.Close()
			os.Remove(tempFile)
			break
		}

		wNum, err = dFile.Write(data[:rNum]) //最后可能不足512k

		tFile.Seek(0, io.SeekStart)
		result := strconv.Itoa(int(count))
		tFile.WriteString(result)

		count += int64(wNum)
		percent := (float64(count) / float64(df.Size())) * 100

		fmt.Printf("已经读取了 %vk 完成了:%v%% \n", count/1024, fmt.Sprintf("%.0f", percent))
		// fmt.Println("已经保存了数据: ", count)

		// if count > 77824 {
		// 	panic("错误")
		// }

	}

}

8.2.8、文件夹遍历


func main() {
	srcDir := `D:\PERSON_BOOK\OS`
	sDir := strings.Replace(srcDir, "\\", "/", -1)
	fmt.Println(sDir)
	listDir(sDir, 0)
}

func listDir(dir string, level int) {
	d, err := ioutil.ReadDir(dir)
	s := "|--"
	for i := 0; i < level; i++ {
		s = "| " + s
	}
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, v := range d {
		filename := dir + "/" + v.Name()
		fmt.Println(s + filename)
		if v.IsDir() {
			listDir(filename, level+1)
		}
	}
}

8.3、time包

8.3.1、time包

时间类型:
    time 类型: "2006-01-02 15:04:05"
    时间戳:time.Now().Unix()
    时间类型: time.Time //time.now() ,time.Nanosecond
时间间隔类型:
	time.Duration() //time.Day(),time.Month()
定时器:
	timmer := time.Tick(time.Second)
	for t := range timmer {
		fmt.Println(t)  //每一秒打印一次
	}

解析字符串格式的时间: 
	loc,err := time.LoadLocation("Asia/Shanghai")  //先获取时区
	time.ParseInLocation("2006-01-02","2020-01-01",loc)  //然后解析,这样不会存在时区问题
  • 时间戳是1970年1月1日08:00:00 GMT到当前的秒数 UnixStamp
  • time.Unix可以转换时间戳为时间格式
  • 时间间隔,在const中定义的
const (
	Nanosecond  Duration = 1
	Microsecond          = 1000 * Nanosecond
	Millisecond          = 1000 * Microsecond
	Second               = 1000 * Millisecond
	Minute               = 60 * Second
	Hour                 = 60 * Minute
)
  • Sub求2个时间点的差值
  • Equal判断2个时间是否相同。不通时区也可以正常比较
  • Before在时间点之前,返回bool值
  • After在时间点之后,返回bool值
  • time.Tick 定时器,定时器的本质是一个通道
  • 时间格式化:时间类型有一个特定的方法format进行格式化。需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:s,而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀:2006 1 2 3 4)
  • 如果想格式化为12小时,需要制定"PM" //时间格式化:把语言中时间对象,转换成字符串类型的时间

8.3.2、示例脚本

func time1(){
	now := time.Now()  //time.Now()获取到一个time类型的结构体
	fmt.Printf("%#v\n",now)  //time.Time{wall:0xbfa1b41fe1d4e3e4, ext:13034901, loc:(*time.Location)(0x56efe0)}
	fmt.Println(now)  //2020-04-27 15:01:12.7175896 +0800 CST m=+0.073194401
	fmt.Println(now.Year())  //Year方法
	fmt.Println(now.Month())
	fmt.Println(now.Day())
	fmt.Println(now.Date())  //2020 April 27
	fmt.Println(now.Hour())
	fmt.Println(now.Minute())
	fmt.Println(now.Second())

	fmt.Println(now.Unix())
	t := time.Unix(0,0)
	fmt.Println(t.Year(),t.Month(),t.Day(),t.Hour(),t.Minute()) //1970 January 1 8 0

	//时间间隔
	fmt.Println(time.Second)
	//now+24h
	fmt.Println(now.Add(24*time.Hour))  //明天的这个时间

	//定时器
	//timer := time.Tick(5*time.Second)  //每间隔5s执行一次
	//for t := range timer {
	//	fmt.Println(t)
	//}

	//时间格式化;按照指定格式输出的意思
	fmt.Println(now.Format("2006-01-02"))  //2020-04-27
	fmt.Println(now.Format("2006/01/02 15:04:05"))   //2020/04/27 15:23:16
	fmt.Println(now.Format("2006/01/02 18:14:05"))   //有问题,,2020/04/27 48:423:50
	fmt.Println(now.Format("2006/01/02 03:04:05 PM"))   //2020/04/27 03:26:20 PM

	//按照对应的格式,解析字符串类型
	obj,err := time.Parse("2006-01-02","1992-03-10")
	if err !=nil {
		fmt.Println(err)
	}
	fmt.Println(obj.Year(),obj.Month(),obj.Day())  //1992 March 10
}
func next2(){
	nextYear,err := time.Parse("2006-01-02","2020-01-01")
	if err != nil {
		fmt.Println(err)
	}
	//fmt.Println(nextYear)  //2020-01-01 00:00:00 +0000 UTC ,返回的是UTC时间
	//nextYear2,err := time.ParseInLocation("2006-01-02","2020-01-01",time.Local)
	//if err != nil {
	//	fmt.Println(err)
	//}
	//fmt.Println(nextYear2)  //2020-01-01 00:00:00 +0800 CST ,返回的是CST时间
	//d := nextYear.Sub(nextYear2)  //求nextYear 和 nextYear2 的差值
	//fmt.Println(d)  //8h0m0s

	nextYear3,err := time.Parse("2006-01-02","2020-01-02")
	if err != nil {
		fmt.Println(err)
	}

	d := nextYear.Sub(nextYear3)  //求nextYear - nextYear3 的差值(外边的减去里面的)
	fmt.Println(d)  //-24h0m0s

}
func next3(){  //parese注意区分时区
	now := time.Now()
	fmt.Println(now) //2020-04-27 16:18:02.3892771 +0800 CST m=+0.022058101,东八区的时间
	//获取明天的时间
	timeO,err := time.Parse("2006-01-02 15:04:05","2020-04-28 14:41:50")  //这样解析后的时间是0区时间,不是东八区
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(timeO)  //2020-04-28 14:41:50 +0000 UTC
	//按照东八区的时间和格式解析一个字符串格式的时间
	loc,err := time.LoadLocation("Asia/Shanghai") //根据字符串加载时区
	if err != nil {
		fmt.Println(err)
	}
	//按照指定时区解析时间
	timeOjb,err := time.ParseInLocation("2006-01-02 15:04:05","2020-04-28 14:41:50",loc)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(loc)  //Asia/Shanghai
	fmt.Println(timeOjb)  //2020-04-28 14:41:50 +0800 CST
	td := timeOjb.Sub(now)
	fmt.Println(td)  //22h19m32.546142s
}

8.3.3、时间戳转换

import (
	"fmt"
	"time"
)

func main() {
	//1、获取当前时间戳
	nowStamp := time.Now().Unix()
	convUton(nowStamp)
}

func convUton(unix int64) {
	t2 := time.Unix(unix, 0)  //time.Unix负责将int64 转换time.Time类型
	fmt.Println(t2.Format("2006年1月2日 15时04分05秒"))
}

8.4、flags

8.4.1、os.args

func main(){
   fmt.Printf("TYPE:%T VALUE:%#v\n",os.Args,os.Args)
   //执行:flags.exe -a -b -c
   //输出:TYPE:[]string VALUE:[]string{"flags.exe", "-a", "-b", "-c"}
   fmt.Println(os.Args[0],os.Args[1])  //flags.exe -ip=127.0.0.1  第1个元素就是" -ip=127.0.0.1"
}

参数值比较生硬,直接使用空格分隔

8.4.2、flags包

flag包支持的命令行参数类型有boolintint64uintuint64float float64stringduration

flag参数 有效值
字符串flag 合法字符串
整数flag 1234、0664、0x1234等类型,也可以是负数。
浮点数flag 合法浮点数
bool类型flag 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。
时间段flag 任何合法的时间段字符串。如”300ms”、”-1.5h”、”2h45m”。 合法的单位有”ns”、”us” /“µs”、”ms”、”s”、”m”、”h”。

8.4.3、flag的函数

  • flag.Type()

基本格式如下:

flag.Type(flag名, 默认值, 帮助信息)*Type 例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 18, "年龄")
married := flag.Bool("married", false, "婚否")
delay := flag.Duration("d", 0, "时间间隔")

需要注意的是,此时nameagemarrieddelay均为对应类型的指针。

  • flag.typevar()

基本格式如下: flag.TypeVar(Type指针, flag名, 默认值, 帮助信息) 例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "时间间隔")
  • flag.Parse()

通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。

支持的命令行参数格式有以下几种:

  • -flag xxx (使用空格,一个-符号)
  • --flag xxx (使用空格,两个-符号)
  • -flag=xxx (使用等号,一个-符号)
  • --flag=xxx (使用等号,两个-符号)

其中,布尔类型的参数必须使用等号的方式指定。

Flag解析在第一个非flag参数(单个”-“不是flag参数)之前停止,或者在终止符”–“之后停止。

flag.Args()  ////返回命令行参数后的其他参数,以[]string类型;或者出现非命令行参数之后的 所有参数(包括自己和非命令行参数后的所有参数)
flag.NArg()  //出现非命令行参数之后的参数个数(包含自己)。
flag.NFlag() //非命令行参数之前的所有参数个数
func f1(){
   //创建一个标志位参数
	var name string
	flag.StringVar(&name, "name", "小王", "请输入姓名")  //方法1

    age := flag.Int("age",9000,"年龄")  //方法2
   married := flag.Bool("married",false,"结婚否")
   cTime := flag.Duration("ct",time.Second,"结婚多久了")
   //使用flag
   flag.Parse()
   fmt.Println(name,*age,*married,*cTime)
   //执行:flags.exe -name 小刚|flags.exe -name=小刚都可以使用,flags.exe -h|--help 可以查看需要的参数信息,如果没有传递则使用默认参数

   fmt.Println(flag.NArg(),flag.NFlag())
   //执行:flags.exe -name 小高 -age 18  xiaohong  结果:1 2
}

九、反射Reflect

9.1、概念介绍

  • 反射是指在程序运行期间(主要是通过类型理解自身数据结构)并对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
  • Go程序在运行期使用reflect包访问程序的反射信息。因为在接口类型底层都会维护一个pair (value和type)记录实际的变量的值和类型
  • go语言是静态类型,编译时类型已经确定。运行时才知道的类型叫做动态类型
    • 静态类型: type Myint int;type A struct{name string}; var a *int 都是静态类型
    • 动态类型:var a interface{};a = 10;a="我是谁" 是动态类型
  • 反射的三大定律:
    • 反射可以从接口值得到反射对象通过typeof 和valueof获得
    • 反射可以从反射对象获得接口值
    • 可以通过反射去修改反射变量

为什么需要反射:

  • 有时需要编写一个函数,但是并不知道对方传递的参数类型,可能是没有商量好,也可能是传入的类型很多不能统一,这时候就需要反射了
  • 有时候需要根据输入的内容,函数或者函数的参数进行解析,在运行期间动态的选择函数进行调用。比如计算器程序。有时输入的式整数/float/复数/二进制数;就需要对应不通的函数去解析和处理
  • 反射的弊端:
    • 1)代码难以阅读
    • 2)编译器难以发现反射的错误,包含反射的代码可能在运行很久才能发现问题panic;造成严重的后果
    • 3)反射对于性能影响较大

go中反射:

  • 空接口可以存储任意类型的变量,那我们如何知道这个空接口保存的数据是什么呢? 反射就是在运行时动态的获取一个变量的类型信息和值信息。
  • 反射主要是对interface 类型相关的 ;一个interface{}的类型的变量包含了2个指针,一个指针指向值的类型(type),另外一个指向实际的值(value);value和type可以合成为pair。
  • 学习反射主要用于 获取接口内部变量的信息(value/type)
func main() {
	var x float32 = 3.14
	fmt.Println(reflect.TypeOf(x), reflect.ValueOf(x)) //float32 3.14
	/*
		func TypeOf(i interface{}) Type {
		func ValueOf(i interface{}) Value {
	*/

	//根据反射获取的值来获取类型和数值
	v := reflect.ValueOf(x)
	fmt.Println("kind is float32: ", v.Kind() == reflect.Float32) //kind is float32:  true
	fmt.Println(v.Type())                                         //float32
	fmt.Println(v.Float())                                        //3.140000104904175
}

9.2、Typeof

reflect包

  • 在Go语言的反射机制中,任何值都由是一个具体类型和具体类型的值两部分组成的.任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。
  • go语言的类型又分为:类型(Type)和种类(Kind),种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)
    • Type: int,bool,float32,... 以及使用type定义的类型 //原生类型;对应reflect.Type()
    • Kind: //归属的种类,比如type myInt int;var a myInt中a的kind为int,type为myInt ;对应reflect.Ptr为指针类型
  • 求字段个数:使用reflect.TypeOf(&cfg).Elem().NumField() 或者 reflect.TypeOf(&cfg).NumField() 取决于传递的是指针还是值
TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil
reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
type person struct {
	Name string `json:"name"`
	Age int `json:"age"`
}

func reflectType1(m interface{}){
	ty := reflect.TypeOf(m)
	v := reflect.ValueOf(m)
	fmt.Printf("type:%v value:%v \n",ty,v)
}

func reflectType(m interface{}){
	t := reflect.TypeOf(m)
	fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}

func test() {
	var a *float32 // 指针
	type myInt int
	var b myInt    // 自定义类型
	var c rune     // 类型别名
	reflectType(a) //type: kind:ptr
	reflectType(b) //type:myInt kind:int
	reflectType(c) //type:int32 kind:int32

	type person struct {
		name string
		age  int
	}
	type book struct{ title string }
	var d = person{
		name: "我爱中国",
		age:  18,
	}
	var e = book{title: "《跟小王子学Go语言》"}
	reflectType(d) //type:person kind:struct
	reflectType(e) //type:book kind:struct
}

func main() {
	str := `{"name":"小王","age":9000}` //字符串
	var p person
	var d int = 10
	json.Unmarshal([]byte(str),&p) //"func Unmarshal(data []byte, v interface{}) error" 方法签名,接收参数为空接口类型,什么类型都可以传递
	fmt.Println(p.Name,p.Age)
	fmt.Println(d)
	//如何让 str的name赋值给p.Nmae,str的age赋值给p.Age,才是关键
	a := 3.41
	reflectType1(a)  //type:float64 value:3.41
	test()
}

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

func test2() {
   stu1 := student{
      Name:  "小王子",
      Score: 90,
   }
   t := reflect.TypeOf(stu1)  //返回的是reflect.Type
   fmt.Println(t.Name(), t.Kind()) //student struct
   // 通过for循环遍历结构体的所有字段信息
   for i := 0; i < t.NumField(); i++ {
      field := t.Field(i)
      fmt.Printf("name:%s index:%d type:%v tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
   }
   // 通过字段scoreField名获取指定结构体字段信息
   if scoreField,ok := t.FieldByName("Score"); ok {
      fmt.Printf("name:%s index:%d type:%v tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
   }
/*
student struct
name:Name index:[0] type:string tag:name
name:Score index:[1] type:int tag:score
name:Score index:[1] type:int tag:score

分析:
reflect.TypeOf(stu1).Name  //student
               .Kind  //struct
               .Field(i)
                     .Name  //Name 只提及第一次循环
                     .Type  //type: string  student.Name类型为string
                     .Index // index:[0] student.Name索引为0
                     .Tag.Get("json") //tag:name student.Name的 json标签对应的值为 "name"
               .FieldByName("Score")  //student类型的子类型.*中找出 名称为Score的字段,返回一个reflect.Type对象
                     .Score  //Score student.
                     .Type  //type: int  student.Name类型为string
                     .Index // index:[1] student.Name索引为0
                     .Tag.Get("json") //tag:score student.Score的 json标签对应的值为 "score"
*/
}
  • 注意valueof中有.kind和.type ,kindof中的是.Name和.kind
  • Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空
  • reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换

9.3、Valueof

ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0
reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值
如果要获取具体的值要使用: 

func main() {  //使用reflect.Valueof($变量).Interface().($类型)
	a := "3.14"
	value := reflect.ValueOf(a)
	s := value.Interface().(string)
	fmt.Printf("Type: %T value:%v\n", s, s) //Type: string value:3.1
}

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

    方法 说明
    Interface() interface {} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
    Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回
    Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回
    Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
    Bool() bool 将值以 bool 类型返回
    Bytes() []bytes 将值以字节数组 []bytes 类型返回
    String() string 将值以字符串类型返回
func reflectType(m interface{}){
   t := reflect.TypeOf(m)
   fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())  //reflect.TypeOf的 kind()
}

func reflectValue(x interface{}){
   v := reflect.ValueOf(x)  //获取reflct.value对应结构体的类型
   k := v.Kind() //reflect.ValueOf 的kind 注意区分
   //值的类型,
   switch k {
   case reflect.Int64:
      fmt.Printf("type is Int64,value is %d\n",int64(v.Int()))
   case reflect.Float32:
      fmt.Printf("type is Float32,value is %f\n",float32(v.Float()))
   case reflect.Float64:
      fmt.Printf("type is Float64,value is %f\n",float64(v.Float()))
   }
}

func main(){
   var a float32 = 3.14
   var b int64 = 32
   reflectValue(a)  //type is Float32,value is 3.140000
   reflectValue(b)  //type is Int64,value is 32
   c := reflect.ValueOf(10)
   fmt.Printf("type c: %T\n",c)  //type c: reflect.Value
}

9.3.1、通过valueof获取结构体所有信息

package main

import (
	"fmt"
	"reflect"
)

type person struct {
	Name   string
	Age    int
	Gender string
}

//报错: panic: reflect.Value.Interface: cannot return value obtained from unexported field or method
//因为person中 的字段必须要首字母大写才能行
//Hell()和Printinfo()首字母也必须大写,不然reflect不到

func (p person) Hello(info string) {
	fmt.Println("hello ", info, " !")
}

func (p person) Printinfo() {
	fmt.Printf("Name: %s age: %s 性别:%s \n", p.Name, p.Age, p.Gender)
}

func main() {
	var stu1 = person{Name: "小明", Age: 22, Gender: "男"}
	Getmessage(stu1)
}

func Getmessage(p interface{}) {
	//1、不知道传递的是什么内容,查看Kind和Type,以及value信息
	getType := reflect.TypeOf(p)
	getValue := reflect.ValueOf(p)
	fmt.Printf("Type: %v Kind: %v Value: %v\n", getType.Name(), getType.Kind(), getValue) //Type: person Kind: struct Value: {小明 22 男}

	//2、获取各个字段信息{字段名称,字段值,字段类型}
    for i := 0; i < getType.NumField(); i++ { //注意NumFiled()方法,必须是getType否则会异常
		field := getType.Field(i)              //获取结构体子成员的信息
		value := getValue.Field(i).Interface() //获取值,得需要getValue()
		//因为这个结构体中有三个字段,两种类型age为int,name和gender为string,不统一。所以用interface()
		fmt.Printf("字段名: %v 字段类型: %v 值:%v \n", field.Name, field.Type, value)
	}
	/*
	   字段名: Name 字段类型: string 值:小明
	   字段名: Age 字段类型: int 值:22
	   字段名: Gender 字段类型: string 值:男
	*/

	//	3、获取方法
	for i := 0; i < getType.NumMethod(); i++ {
		method := getType.Method(i)
		fmt.Printf("方法名称: %v 方法类型: %v \n", method.Name, method.Type)
	}
	/*
		方法名称: Hello 方法类型: func(main.person, string)
		方法名称: Printinfo 方法类型: func(main.person)
	*/

}

9.3.2、通过反射设置变量的值

  • 想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()方法来获取指针对应的值。
  • reflect.Value是通过reflect.ValueOf(x)获得的,只有当X是指针的时候,才可以通过reflect.Value修改实际的变量X的值
  • 用到的方法fun (v value) Elem() value Elem返回接口v包含的值或指针指向的值,如果v的类型不是interface或ptr会panic,如果v是零,则返回零值;如果变量是一个指针/map/slice/channel/array,那么可使用reflect.Typeof(v).Elem()来确定包含的类型
func setValue1(x interface{}) {
	v := reflect.ValueOf(x)
	if v.Kind() == reflect.Int64 {
		v.SetInt(200)
	}
}

func SetValue2(x interface{}) {
	value := reflect.ValueOf(x)

	defer func() { //对判断执行的异常进行捕获
		err := recover()
		if err != nil {
			fmt.Println(x, "不可修改,错误为: ", err)
		}
	}()

	if value.Elem().CanSet() { //如果不能修改就直接panic了
		fmt.Println("可以修改")
		if value.Elem().Kind() == reflect.Int64 {
			value.Elem().SetInt(200)
		}
	}
}

func main() {
	var b int64 = 100
	//setValue1(b)  //panic: reflect: reflect.Value.SetInt using unaddressable value
	SetValue2(b)   //100 不可修改,错误为:  reflect: call of reflect.Value.Elem on int64 Value
	SetValue2(&b)  //可以修改
	fmt.Println(b) //200
}

结构体设置变量的值:

type Student struct {
	Name   string
	age    int
	School string
}

func main() {
	s1 := Student{"令狐冲", 28, "华山剑派"}

	//1、改变数值
	newValue := reflect.ValueOf(&s1)
	if newValue.Kind() == reflect.Ptr {
		value := newValue.Elem()
		fmt.Println(value.CanSet()) //是否可被修改

		f1 := value.FieldByName("School") //修改的是reflect.Valueof(&s1).Elem().FieldByName("School")
		f1.SetString("日月神教")
		fmt.Println(s1)
	}
}

9.3.3、isNil和isValid

  • isNil() 函数签名:func (v Value) IsNil() bool ;IsNil()报告v持有的值是否为nil。v持有的值的分类必须是:通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。
  • isValid() 函数签名:func (v Value) IsValid() bool ;IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。
func test2(){
	// *int类型空指针
	var a *int
	fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())  //true
	// nil值
	fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())  //false
	// 实例化一个匿名结构体
	b := struct{}{}
	// 尝试从结构体中查找"abc"字段
	fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())  //false
	// 尝试从结构体中查找"abc"方法
	fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())  //false
	// map
	c := map[string]int{}
	// 尝试从map中查找一个不存在的键
	fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid()) //false
}

9.3.4、reflect之函数/方法调用

1.想要通过反射来调用方法,必须要先通过reflect.Valueof()获取到reflect.value对象.得到之后才能进行下一步
2.reflect.Value.MethodByName这个MethodName必须指定精确的方法名字,如果错误将直接panic。该函数返回 一个reflect.Value方法的名字
3.[]reflect.Value,这个最终要调用方法的参数。可以没有,没有填nil
4.reflect.Value的call这个方法,为最终调用方法。参数务必保持一致。如果reflect.Value.Kind不是一个方法,则panic
5.本来可以用对象方法直接调用的。但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后.call

package main

import (
	"fmt"
	"reflect"
)

type person struct {
	Name   string
	Age    int
	Gender string
}

func (p person) Hello(info string) {
	fmt.Println("hello ", info, " !")
}

func (p person) Printinfo() {
	fmt.Printf("Name: %s age: %d 性别:%s \n", p.Name, p.Age, p.Gender)
}

func (p person) Test(x, y int, s string) {
	fmt.Println("test Func: ", x, y, s)
}

func main() {
	/* 目标:通过反射来进行方法的调用
	思路:
		step1:接口变量-> 对象反射对象 value
		step2:获取对应的方法对象-> MethodByName()
		step3:对方法对象进行调用 Call()
	*/
	var stu1 = person{Name: "小明", Age: 22, Gender: "男"}
	value := reflect.ValueOf(stu1)
	fmt.Printf("Kind: %s Type: %s\n", value.Kind(), value.Type()) //Kind: struct Type: main.person

	methodValue1 := value.MethodByName("Printinfo")
	fmt.Printf("Kind: %s Type: %s\n", methodValue1.Kind(), methodValue1.Type()) //Kind: func Type: func()

	//step3:方法调用
	methodValue1.Call(nil) //如果没有参数 ,传递nil或者空切片
	arg1 := make([]reflect.Value, 0)
	methodValue1.Call(arg1)

	methodValue2 := value.MethodByName("Hello")
	fmt.Printf("Kind: %s Type: %s\n", methodValue2.Kind(), methodValue2.Type()) //Kind: func Type: func(string)
	arg2 := []reflect.Value{reflect.ValueOf("古力娜扎")}
	methodValue2.Call(arg2)

	//多个参数
	methodValue3 := value.MethodByName("Test")
	fmt.Printf("Kind: %s Type: %s\n", methodValue3.Kind(), methodValue3.Type()) //Kind: func Type: func(int, int, string)
	arg3 := []reflect.Value{reflect.ValueOf(100), reflect.ValueOf(200), reflect.ValueOf("Test")}
	methodValue3.Call(arg3)
}

9.4、结构体反射

  • 从string中取出值,赋值给结构体

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

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

    方法 说明
    Field(i int) StructField 根据索引,返回索引对应的结构体字段的信息
    NumField() int 返回结构体成员字段数量
    FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息
    FieldByIndex(index []int) StructField 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息
    FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段
    NumMethod() int 返回该类型的方法集中方法的数目
    Method(int) Method 返回该类型方法集中的第i个方法
    MethodByName(string)(Method, bool) 根据方法名返回该类型方法集中的方法

StructField :类型用来描述结构体中的一个字段的信息

type StructField struct {
    Name    string  //字段名称
    PkgPath string  //字段路径
    Type      Type      // 字段的类型  定义结构体后面放的类型"Name string `json:"name"`"
    Tag       StructTag // 字段的标签
    Offset    uintptr   // 字段在结构体中的字节偏移量
    Index     []int     // 用于Type.FieldByIndex时的索引切片
    Anonymous bool      // 是否匿名字段
}
func test2() {
   stu1 := student{
      Name:  "小王子",
      Score: 90,
   }
   v := reflect.ValueOf(stu1)  //返回的是reflect.Type
   t := reflect.TypeOf(stu1)
   fmt.Println(t.Kind(),t.Name()) //struct student
   fmt.Println(v.Kind(),v.Type()) //struct main.student
   // 通过for循环遍历结构体的所有字段信息
   for i := 0; i < v.NumField(); i++ {
      tv := t.Field(i)  //返回值是StructField,v也是 不能直接打印的,
      vv := v.Field(i)   //返回值是value
      ttag := tv.Tag.Get("json")
      //vtag := vv.Tag.Get("json")  //注意,valueof没有这种用法,因为tag是打在 struct 类型上的
      fmt.Printf("typeof_field:%s valueof_field:%v typeof_tag:%v\n",tv.Type,vv,ttag)
   }
   scoreField := v.FieldByName("Score");
   fmt.Println(scoreField.Kind(),scoreField.Type())  //int int
/*
typeof_field:string valueof_field:小王子 typeof_tag:name
typeof_field:int valueof_field:90 typeof_tag:score

分析:
reflect.ValueOf(stu1).Type  //main.student
               .Kind  //struct
               .NumField //2 字段个数
               .Field(i)  // 90
                     .Kind  //int 只提及第2次循环
                     .Type  //int: string  student.Score类型为int
               .FieldByName("Score")
                     .Kind  //int 只提及第2次循环
                     .Type  //int: string  student.Score类型为int
*/
}
type student struct {
	Name string `json:"name" gang:"fuckName_v"`
	Score int `json:"score" gang:"fuckScore_v"`
}

func main(){
	stu1 := student{
		Name:"小刚",
		Score:85,
	}
	t := reflect.TypeOf(stu1)
	fmt.Println(t.Kind(),t.Name()) //struct student

	for i :=0; i< t.NumField() ; i ++ {  //NumField返回字段的总数
		filed := t.Field(i)  //返回的是StructField 结构体对象,该对象才有如下的方法和属性
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n",filed.Name,filed.Index,filed.Type,filed.Tag.Get("gang"))
		//name:Name index:[0] type:string json tag:fuckName_v
		//name:Score index:[1] type:int json tag:fuckScore_v
	}
	if id,ok := t.FieldByName("Score");ok {  //结构体中单个字段(kv)的 信息
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n",id.Name,id.Index,id.Type,id.Tag.Get("gang"))
		//name:Score index:[1] type:int json tag:fuckScore_v
	}
}

9.4.1、解析配置文件

package main

import (
	"fmt"
	"reflect"
)

type RedisConfig struct {
	Host     string `ini:"host"` //后面的名称host要和配置文件中的对应
	Port     int    `ini:"port"`
	Database int    `ini:"database"`
	Password string `ini:"password"`
}

func main() {
	var cfg = RedisConfig{
		Host:     "127.0.0.1",
		Port:     6379,
		Database: 0,
		Password: "admin123",
	}
	//1、注意,如果传递的是地址,求字段个数的表达式为: reflect.TypeOf(&cfg).Elem().NumField()
	// m := reflect.TypeOf(&cfg) //要传递地址
	// fmt.Println(m, m.Kind())  //*main.RedisConfig ptr
	// for i := 0; i < m.Elem().NumField(); i++ { // m.NumField() 为4,这个结构体共4个元素
	// 	fmt.Printf("Index:%d value:%v NumField:%d\n", i, m.Elem().Field(i), m.Elem().NumField())
	// }
	/*  注意:m.NumField() 和 m.Elem().NumField() 的用法基本相同
	Index:0 value:{Host  string ini:"host" 0 [0] false} NumField:4
	Index:1 value:{Port  int ini:"port" 16 [1] false} NumField:4
	Index:2 value:{Database  int ini:database 24 [2] false} NumField:4
	Index:3 value:{Password  string ini:password 32 [3] false} NumField:4
	*/

	//2、如果传递的是值,求字段个数的表达式为: reflect.TypeOf(&cfg).NumField()
	// m := reflect.TypeOf(cfg)
	// for i := 0; i < m.NumField(); i++ { //elem的操作一定要用到 指针
	// 	fmt.Printf("Index:%d Type:%v 值:%v 值2:%v \n", i, m, m.Field(i), m.Field(i).Name)
	// }
	/*
	   Index:0 Type:main.RedisConfig 值:{Host  string ini:"host" 0 [0] false} 值2:Host
	   Index:1 Type:main.RedisConfig 值:{Port  int ini:"port" 16 [1] false} 值2:Port
	   Index:2 Type:main.RedisConfig 值:{Database  int ini:database 24 [2] false} 值2:Database
	   Index:3 Type:main.RedisConfig 值:{Password  string ini:password 32 [3] false} 值2:Password
	*/

	//3、提取内容gender
	getType := reflect.TypeOf(&cfg)
	getValue := reflect.ValueOf(&cfg)
	for i := 0; i < getType.Elem().NumField(); i++ { //elem的操作一定要用到 指针
		newType := getType.Elem().Field(i)
		newValue := getValue.Elem().Field(i)
		if newType.Tag.Get("ini") == "host" {
			fmt.Printf("TAG:%v 字段名:%v 字段值:%v\n", newType.Tag, newType.Name, newValue.Interface())
		}

		fmt.Printf("TAG:%v 字段名:%v 字段值:%v\n", newType.Tag, newType.Name, newValue.Interface())

	}
	/*
		TAG:ini:"host" 字段名:Host 字段值:127.0.0.1
		TAG:ini:"host" 字段名:Host 字段值:127.0.0.1
		TAG:ini:"port" 字段名:Port 字段值:6379
		TAG:ini:"database" 字段名:Database 字段值:0
		TAG:ini:"password" 字段名:Password 字段值:admin123
	*/
}

type MysqlConfig struct {
   Address string `ini:"address"`  //和my.cnf中的一样
   Port int `ini:"port"`
   Username string `ini:"user"`
   Password string `ini:"password"`
}

type RedisConfig struct {
   Host string `ini:"host"`  //后面的名称host要和配置文件中的对应
   Port int `ini:"port"`
   Database int `ini:"database"`
   Password string `ini:"password"`
}

type Config struct {
   MysqlConfig `ini:"mysql"`
   RedisConfig `ini:"redis"`
}

func main(){
   var cfg Config
   m := reflect.TypeOf(&cfg)  //要传递地址
   for i :=0 ;i < m.Elem().NumField();i++{  //elem的操作一定要用到 指针
      tmp := m.Elem().Field(i)
      if tmp.Tag.Get("ini") == "host" {  //匹配不到,m.Elem().Field(i) 只能匹配到 Config的子类型,不能匹配到 Config.MysqlConfig.*
         fmt.Printf("类型:%s\n",tmp.Name)
      }
      if tmp.Tag.Get("ini") == "mysql" {  //如果ini="mysql"匹配,则输出对应的结构体的元素的key
         fmt.Printf("类型:%s\n",tmp.Name)  //类型:MysqlConfig
      }
      fmt.Printf("tag:%v name:%v\n",tmp.Tag.Get("ini"),tmp.Name)
   }
/*
类型:MysqlConfig
tag:mysql name:MysqlConfig
tag:redis name:RedisConfig
 */
}

9.4.2、解析xml文件

//文件名config.xml 
<?xml version="1.0" encoding="UTF-8"?>
<config>
    <smtpServer>blackhorsesmtp.163.com</smtpServer>
    <smtpPort>25</smtpPort>
    <sender>user@163.com</sender>
    <senderPasswd>123456</senderPasswd>
    <receivers flag="true">
        <user>test@live.com</user>
        <user>test1@qq.com</user>
    </receivers>
</config>
package main

import (
   "encoding/xml"
   "fmt"
   "io/ioutil"
   "os"
)

type SConfig struct {
   XMLName  xml.Name `xml:"config"` // 指定最外层的标签为config,注意类型为xml.Name 
    //注意XMLName这个变量名是固定的,否则会获取不到值
   SmtpServer string `xml:"smtpServer"` // 读取smtpServer配置项,并将结果保存到SmtpServer变量中
   SmtpPort int `xml:"smtpPort"`
   Sender string `xml:"sender"`
   Test01 int `xml:"fuckBMC"`  //这个字段并不存在,因此会赋对应的0值
   SenderPasswd string `xml:"senderPasswd"`
   Receivers SReceivers `xml:"receivers"` // 读取receivers标签下的内容,以结构方式获取
}

type SReceivers struct {
   Flag string `xml:"flag,attr"` // 读取flag属性
   User []string `xml:"user"` // 读取user数组,user的值不止一个,这里的User首字母必须大写,否则不会打印出来
}

func main() {
   file, err := os.Open("config.xml") // 打开文件
   if err != nil {
      fmt.Printf("error: %v", err)
      return
   }
   defer file.Close()
   data, err := ioutil.ReadAll(file)
   if err != nil {
      fmt.Printf("error: %v", err)
      return
   }
   v := SConfig{}
   err = xml.Unmarshal(data, &v) //xml 反序列化
   if err != nil {
      fmt.Printf("error: %v", err)
      return
   }

   fmt.Println(v)
   fmt.Println("SmtpServer : ",v.SmtpServer)
   fmt.Println("SmtpPort : ",v.SmtpPort)
   fmt.Println("Sender : ",v.Sender)
   fmt.Println("SenderPasswd : ",v.SenderPasswd)
   fmt.Println("Receivers.Flag : ",v.Receivers.Flag)
   for i,element := range v.Receivers.User {
      fmt.Println(i,element)
   }

   fmt.Println("test")
   s,err := xml.Marshal(v)
   if err != nil {
      fmt.Println(err)
   }
   fmt.Println(string(s))
}
/*
{{ config} blackhorsesmtp.163.com 25 user@163.com 0 123456 {true [test@live.com test1@qq.com]}}
SmtpServer :  blackhorsesmtp.163.com
SmtpPort :  25
Sender :  user@163.com
SenderPasswd :  123456
Receivers.Flag :  true
0 test@live.com
1 test1@qq.com
test
<config><smtpServer>blackhorsesmtp.163.com</smtpServer><smtpPort>25</smtpPort><sender>user@163.com</sender><fuckBMC>0</fuckBMC><senderPasswd>123456</senderPasswd><receivers flag="tr
ue"><user>test@live.com</user><user>test1@qq.com</user></receivers></config>
*/

9.4.3、解析ini文件

//配置文件
; mysql config 分号也可以做配置
[mysql]
address=127.0.0.1
port=3306
username=root
password=root123


#redis配置
[redis]
#test
#= test1
host=127.0.0.11
port=6379
database=0
password=redis123
//解析配置文件
package main

import (
   "errors"
   "fmt"
   "io/ioutil"
   "reflect"
   "strconv"
   "strings"
)

// ini配置文件解析器

// MysqlConfig MySQL配置结构体
type MysqlConfig struct {
   Address  string `ini:"address"`
   Port     int    `ini:"port"`
   Username string `ini:"username"`
   Password string `ini:"password"`
}

// RedisConfig ...
type RedisConfig struct {
   Host     string `ini:"host"`
   Port     int    `ini:"port"`
   Password string `ini:"password"`
   Database int    `ini:"database"`
   Test     bool   `ini:"test"`
}

// Config ...
type Config struct {
   MysqlConfig `ini:"mysql"`
   RedisConfig `ini:"redis"`
}

// 尽量去掌握
func loadIni(fileName string, data interface{}) (err error) {
   // 0. 参数的校验
   // 0.1 传进来的data参数必须是指针类型(因为需要在函数中对其赋值)
   t := reflect.TypeOf(data)
    //t.Kind()=> ptr  data本是个指针
    //t.Elem().Kind()=>struct   data的值(&cfg)的kind为Config(Struct)
    //t.Elem().Field(0).Type=>main.MysqlConfig  data的值(&cfg)的第一元素的类型(MysqlConfig)
    //t.Elem().Field(1).Type=>main.RedisConfig	data的值(&cfg)的第一元素的类型(RedisConfig)
    
   fmt.Println(t, t.Kind())
   if t.Kind() != reflect.Ptr {
      err = errors.New("data param should be a pointer") // 新创建一个错误
      return
   }
   // 0.2 传进来的data参数必须是结构体类型指针(因为配置文件中各种键值对需要赋值给结构体的字段)
   if t.Elem().Kind() != reflect.Struct {
      err = errors.New("data param should be a struct pointer") // 新创建一个错误
      return
   }
   // 1. 读文件得到字节类型数据
   b, err := ioutil.ReadFile(fileName)
   if err != nil {
      return
   }
   // string(b) // 将字节类型的文件内容转换成字符串
   lineSlice := strings.Split(string(b), "\r\n")
   // fmt.Printf("%#v\n", lineSlice)
   // 2. 一行一行得读数据
   var structName string
   for idx, line := range lineSlice {
      // 去掉字符串首尾的空格
      line = strings.TrimSpace(line)
      // 如果是空行就跳过
      if len(line) == 0 {
         continue
      }
      // 2.1 如果是注释就跳过
      if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
         continue
      }
      // 2.2 如果是[开头的就表示是节(section)
      if strings.HasPrefix(line, "[") {
         if line[0] != '[' || line[len(line)-1] != ']' {
            err = fmt.Errorf("line:%d syntax error", idx+1)
            return
         }
         // 把这一行首尾的[]去掉,取到中间的内容把首尾的空格去掉拿到内容
         sectionName := strings.TrimSpace(line[1 : len(line)-1])
         if len(sectionName) == 0 {
            err = fmt.Errorf("line:%d syntax error", idx+1)
            return
         }
         // 根据字符串sectionName去data里面根据反射找到对应的结构体
         for i := 0; i < t.Elem().NumField(); i++ {
            field := t.Elem().Field(i)
            if sectionName == field.Tag.Get("ini") {
               // 说明找到了对应的嵌套结构体,把字段名记下来
               structName = field.Name
               fmt.Printf("找到%s对应的嵌套结构体%s\n", sectionName, structName)
            }
         }
      } else {
         // 2.3 如果不是[开头就是=分割的键值对
         // 1. 以等号分割这一行,等号左边是key,等号右边是value
         if strings.Index(line, "=") == -1 || strings.HasPrefix(line, "=") {
            err = fmt.Errorf("line:%d syntax error", idx+1)
            return
         }
         index := strings.Index(line, "=")
         key := strings.TrimSpace(line[:index])
         value := strings.TrimSpace(line[index+1:])
         // 2. 根据strucrName 去 data 里面把对应的嵌套结构体给取出来
         v := reflect.ValueOf(data)
         sValue := v.Elem().FieldByName(structName) // 拿到嵌套结构体的值信息
         sType := sValue.Type()                     // 拿到嵌套结构体的类型信息

         if sType.Kind() != reflect.Struct {
            err = fmt.Errorf("data中的%s字段应该是一个结构体", structName)
            return
         }
         var fieldName string
         var fileType reflect.StructField
         // 3. 遍历嵌套结构体的每一个字段,判断tag是不是等于key
         for i := 0; i < sValue.NumField(); i++ {
            filed := sType.Field(i) // tag信息是存储在类型信息中的
            if filed.Tag.Get("ini") == key {
               // 找到对应的字段
               fieldName = filed.Name
               fileType = filed
               break
            }
         }
         // 4. 如果key = tag,给这个字段赋值
         // 4.1 根据fieldName 去取出这个字段
         if len(fieldName) == 0 {
            // 在结构体中找不到对应的字符
            continue
         }
         fileObj := sValue.FieldByName(fieldName)
         // 4.2 对其赋值
         fmt.Println(fieldName, fileType.Type.Kind())
         switch fileType.Type.Kind() {
         case reflect.String:
            fileObj.SetString(value)
         case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
            var valueInt int64
            valueInt, err = strconv.ParseInt(value, 10, 64)
            if err != nil {
               err = fmt.Errorf("line:%d value type error", idx+1)
               return
            }
            fileObj.SetInt(valueInt)
         case reflect.Bool:
            var valueBool bool
            valueBool, err = strconv.ParseBool(value)
            if err != nil {
               err = fmt.Errorf("line:%d value type error", idx+1)
               return
            }
            fileObj.SetBool(valueBool)
         case reflect.Float32, reflect.Float64:
            var valueFloat float64
            valueFloat, err = strconv.ParseFloat(value, 64)
            if err != nil {
               err = fmt.Errorf("line:%d value type error", idx+1)
               return
            }
            fileObj.SetFloat(valueFloat)
         }
      }
   }
   return
}

func main() {
   var cfg Config
   err := loadIni("./my.cnf", &cfg)
   if err != nil {
      fmt.Printf("load ini failed, err:%v\n", err)
      return
   }
   fmt.Printf("%#v\n", cfg)
}
**举例:解析对应的ini文件到go的结构体**
package main

import (
	"encoding/json"
	"fmt"
)

type person struct {
	Name string //注意大写,不然ref1 引用不到 value,如果不大写会导致 解析后的结果为0 
	Age  int
}

func ref1() {
	str1 := `{"name":"令狐冲","age":18,"爱好":"练剑","Name":"令狐冲二"}`
	str2 := `{"name":"岳不群","age":50}`
	str3 := `{"name":"东方不败","age":"18"}`

	var p1, p2, p3 person

	json.Unmarshal([]byte(str1), &p1) //因为这里要修改的是p的真实值,因此要传递 地址
	json.Unmarshal([]byte(str2), &p2) //json.Unmarsha1内部就是基于reflect实现的
	json.Unmarshal([]byte(str3), &p3)

	fmt.Println(p1)
	fmt.Println(p2)
	fmt.Println(p3)
	/*
	   {令狐冲二 18} //如果name和Name同时出现Name优先级高
	   {岳不群 50}
	   {东方不败 0}  //age的类型不匹配,因此结果为0
	*/

}

func main() {
	ref1()
}

十、go并发编程

10.1、并发与并行

1、线程、进程、协程

线程:轻量级的线程;协程:轻量级的线程 ;并发的程序不一定快是因为并发的程序进程间需要通信。

  • 进程Process:正在执行的程序,是CPU资源分配和调度的独立单位,进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以用它来控制和管理进程,它是系统感知其存在的唯一标志;*** 进程的局限是创建、撤销和切换的开销较大 ***
  • 线程Thread: 轻量级进程,是一个基本的CPU执行单元,也是程序执行过程中的最小单元,有线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。线程的优点是减少了程序并发执行时的开销,提高了操作系统的并发性能。缺点是线程没有自己的系统资源。只拥有在运行时必不可少的资源,但同一个进程内的线程可以共享进程所拥有的系统资源。如果把进程比作一个车间。那么线程就好比是车间里的工人。不过对于某些独占性资源存在锁机制,处理不当会导致"死锁"
  • 协程Coroutine: 用户态的轻量级线程,调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。子程序调用总是一个入门,一次返回。一旦退出即完成了子程序的执行。协程最大的优点”轻量级“,可以轻松创建成百上千个而不导致系统资源衰竭,而线程和进程通常最多也不能超过1w的。

2、并发与并行

  • 并发(Concurency):统一时间内执行多个任务(你在用微信和你的两个女朋友聊天[1=n])

  • 并行(parallelism):同一时刻执行多个任务(你和你的朋友都在用微信和女朋友聊天[n=n])

go语言并发通过goroutine实现。goroutine类似于线程,属于用户态的线程。我们可以根据需要创建成千上万个goroutine工作。goroutine是由go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成

go语言还提供channel,在多个goroutine之间进行通信。goroutine和channel是Go语言秉承的CSP(communicating Sequential Process )并发模式的重要实现基础

  • goroutine

在java/c++中实现并发编程,通常需要自己维护一个线程池,并且要自己去包装一个有一个的任务。同时还需要自己去调度线程执行任务并维护上下文切换。这样耗费程序员心力。那么能不能有一种机制,程序员定义很多个任务,让系统去帮忙我们把这些任务分配到cpu上实现并发运行呢?

goroutine就类似这样一种机制,但goroutine是由Go的运行时runtime调度和管理。因为他在语言层面已经内置了调度和上下文切换的机制。

3、主goroutine的操作

首先:设定每一个goroutine所能申请的最大的栈空间大小(32bit os为250MB,64 bit os为1GB);如果有某个goroutine的栈空间尺寸大于这个限制,那么运行时系统会引发一个栈溢出(Stack overflow)的panic,并停止主goroutine,主goroutine的主要工作内容如下:

  1. 创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常技术
  2. 启动专用于后台清理内存垃圾的goroutine。并设置gc可用的标识
  3. 执行main包中的init函数
  4. 执行main函数;执行后检查主goroutine是否引发了panic,并进行必要的处理。最后主goroutine会结束自己以及当前进程的运行

10.2、runtime

  • runtime.NumGoroutine() goroutine个数
  • runtime.GOMAXPROCS //设定goroutine的max值,建议和CPU个数一样。1-256之间
  • GOSched 让当前线程让出cpu,它不会挂起当前线程,未来会继续运行。这个函数的作用是让当前goroutine让出CPU,当一个goroutine发生阻塞,Go会自动把与该goroutine处于同一系统线程的其他goroutine转义到另一个系统线程上去,以使这些goroutine不阻塞
  • Goexit 终止当前goroutine
package main

import (
	"fmt"
	"runtime"
)

func main() {

	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println("sub goroutine..", i)
		}
	}()

	for i := 0; i <= 4; i++ {
		runtime.Gosched() //加上之后,main goroutine会等待sub goroutine执行完毕后再执行
		fmt.Println("main goroutine...", i)
	}

}

10.3、使用goroutine

在调用函数的时候前面加上go 关键字,就可以为一个函数创建一个goroutine

一个goroutine必定会对应一个函数,可以创建多个goroutine去执行相同的函数

  • 启动单个goroutine
func hello(){
   fmt.Println("hello")
}

//程序启动之后会创建一个主 goroutine去执行,对应main() 函数
func main(){
   go hello() //启动了一个goroutine
   fmt.Println("main") //
}
// 只输出main  ,在goroutine返回前,main函数就已经结束,main函数那么main函数启动的goroutine就都会结束
func hello(i int){
   fmt.Println("hello",i)
}

//程序启动之后会创建一个主 goroutine去执行
func main(){
   for i :=0 ;i< 10;i ++ {
      go hello(i) //启动了10个goroutine,go启动了多少次就是启动了多少
   }
   fmt.Println("main")
   time.Sleep(time.Second) //为了防止主goroutine运行完毕后子goroutine仍然还在执行,可以使用channel或者通道
}
// 只输出main  ,在goroutine返回前,main函数就已经结束,main函数那么main函数启动的goroutine就都会结束
/*
hello 0
main
hello 9
hello 4
hello 5
hello 6
hello 7
hello 8
hello 1
hello 2
hello 3
 */
func main(){
   for i :=0 ;i< 10;i ++ {
      go func(){  //该匿名函数内部的i,不是别人传递给自己的,是自己引用外部的变量(闭包)
         fmt.Println(i) //当goroutine访问外部i的时候,for已经循环了n次
      }() //解决方法,把i传递进去 (i)就可以了
      //启动goroutine需要一定的资源消耗,可能goroutine还没启动完,外部的已经执行完毕
   }
   fmt.Println("main")
   time.Sleep(time.Second)
}
/*
3
main
3
10
10
10
10
10
10
10
10
*/

10.4、goroutine调度模型

  • 可增长的栈

OS线程,一般都有固定的栈内存(通常2M),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈是不固定的,他可以按需增大或缩小。goroutine的栈大小限制可以得到1GB,所以在Go语言中一次创建10W个goroutine是可以的

10.4.1、GMP

goroutine的调度

GMP调度是go语言运行时runtime层面的实现。是go语言自己实现的一套调度系统。区别于操作系统调。M/G/P定义在runtime.h中,Sched定义在proc.h中

  • Sched: 的结构就是调度器,维护有存储M和G的队列以及调度器的一些状态信息
  • G 就是goroutine,里面存放了goroutine外,还有与所在P的绑定信息。
  • M(machine)是go runtime对操作系统内核线程的虚拟,M与内核线程一般是一一映射的关系,一个goroutine最终要放到M上运行。M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等非常多的信息。
  • P(processor) 处理器,主要用途是用来执行goroutine,它维护了一个gorouteine队列,即runqueue。管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用cpu时间较长的goroutine暂停,运行后续的goroutine等)当自己的队列消费完了,就去全局队列里取。如果全局队列也消费完了,就从其他的P队列里抢任务

P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

两级线程模型:M:N 把m个线程(用户级线程)分配给n个os的线程去执行,用户线程和内核线程为多对多,一个进程可以创建内核线程。并且线程可以与不同的内核线程进行动态关联(一个内核线程被阻塞entity kernel space的时候,其所关联的线程动态关联到另外一个内核线程建立连接动态调度)。kernel负责调度内核线程到cpu上

10.4.2、调度器工作模式

在单核处理器下,所有goroutine运行在同一个M系统线程中,每一个M线程对应一个Processor,任何时刻,一个Processor中只有一个goroutine,其他goroutine在runqueue中等待。一个goroutine运行完自己的时间片后。让出上下文,回到runqueue中。多核处理器的场景下,每个M系统线程对应一个processor。

在上图中:如果两个M在不同cpu上执行则为并发,如果两个M在同一个cpu上执行则为并行。

在阻塞的情况下,goroutine如何处理

当正在运行的goroutine(G0)阻塞的时候,比如IO操作。会在创建一个系统线程M1,并且会把P(原来的P关联M1,P中等待的G就会关联到M1上执行。G0占用的M0仍然在阻塞中。M1的创建和系统相关,如果线程池中有空闲则直接使用。如果没有则新建。

runqueue执行完成

当其中一个processor中runqueue为空,没有goroutine可以调度时。它会从其他P中偷取一般的goroutine执行。

go语言早期是没有P的概念的。go中调度其直接把G放到适合的M上执行。这样带来的问题:不同的G和不同的M上并发运行时可能都需要向系统申请资源,比如堆栈内存。但是资源是全局的,就会由于资源竞争造成性能的损耗。为了解决这个问题,引入了P(GO1.1),P管理G对象,M必须先绑定P才能执行G。这样带来的好处是:可以在P中预先申请一些资源。G在需要的时候先向P进行申请。如果没有或者不够用再向全局进行申请。并且由于P解耦了G和M的关系。加入在M上运行了一个阻塞的G,那么其余和M相关联的G也可以动态迁移到其他M上继续运行。

package main

import (
	"fmt"
	"time"
)

func numbers() {
	for i := 1; i <= 5; i++ {
		time.Sleep(250 * time.Millisecond)
		fmt.Printf("%d ", i)
	}
}

func alphabetas() {
	for i := 'a'; i <= 'e'; i++ {
		time.Sleep(400 * time.Millisecond)
		fmt.Printf("%c ", i)
	}
}

func main() {
	go numbers()
	go alphabetas()
	time.Sleep(3000 * time.Millisecond)
	fmt.Println("| main terminalted!")
}

执行结果永远是: 1 a 2 3 b 4 c 5 d e | main terminalted!
这里面共有三个goroutine,main、number和alphabetas三个

十一、sync

11.1、waitGroup

sync.WaitGroup实现goroutine的同步

goroutine什么时候结束:goroutine对应的函数结束的时候

等待goroutine结束: 方式1、time.sleep,这种显然不太好估算值 ; 方式2、sync.WaitGroup 方式3、进程间通信channel(go推荐)

main函数执行完了,由main函数创建的那些goroutine都结束了

sync.WaitGroup的WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为nDone() 每次把计数器-1wait() 会阻塞代码的运行,直到计数器地值减为0。

var wg sync.WaitGroup

func hello(i int){
   defer wg.Done()  //goroutine结束就登记-1
   fmt.Println("hello goroutine",i)
}

func main(){
   for i :=0 ;i< 10;i ++ {
      wg.Add(1) //启动一个goroutine就登记+1
      go hello(i)
   }
    wg.Wait() //等待所有goroutine结束(计数器减为0),然后退出
}

/*  乱序的原因是goroutine是go 
hello goroutine 9
hello goroutine 4
hello goroutine 0
hello goroutine 1
hello goroutine 2
hello goroutine 3
hello goroutine 6
hello goroutine 5
hello goroutine 7
hello goroutine 8
*/

11.2、临界区资源

如果多个goroutine访问同一个资源的时候,其中一个goroutine修改了资源。对于其他goroutine来说,这个数值就可能是不对的。

举例:100张票,4个售票口同时售票;竟然卖出了负值。

package main

import (
	"fmt"
	"math/rand"
	"time"
)

var ticks = 10

func main() {
	/*
		启动4个goroutine模拟四个售票窗口。
	*/
	go saleTicket("窗口1")
	go saleTicket("窗口2")
	go saleTicket("窗口3")
	go saleTicket("窗口4")
	time.Sleep(time.Second * 4)
}

func saleTicket(name string) {
	rand.Seed(time.Now().UnixNano())
	for {
		if ticks > 0 {
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			fmt.Println(name, "售出: ", ticks)
			ticks--

		} else {
			fmt.Println("没有票了")
			break
		}
	}
}

/* 执行输出
...
没有票了
窗口1 售出:  0
没有票了
窗口3 售出:  -1
没有票了
窗口2 售出:  -2
没有票了
*/

go中提倡:不要以共享内存的方式进行通信,而要以通信的方式共享内存

11.2.1、互斥锁

go语言建议使用channel但是也提供了传统的互斥锁。限制同时只有一个goroutine可以访问共享资源:sync.Mutex

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var ticks = 10
var lock sync.Mutex  //用于限制临界区资源访问
var wg sync.WaitGroup  //用于goroutine在同步状态

func main() {
	/*
		启动4个goroutine模拟四个售票窗口。
	*/
	wg.Add(4) //共4个goroutine
	go saleTicket("窗口1")
	go saleTicket("窗口2")
	go saleTicket("窗口3")
	go saleTicket("窗口4")
	defer wg.Wait()
}

func saleTicket(name string) {
    //	defer wg.Done()  //wg.Done()可以放在两个位置。
	rand.Seed(time.Now().UnixNano())
	for {
		//在这里加锁
		lock.Lock()
		if ticks > 0 {
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			fmt.Println(name, "售出: ", ticks)
			ticks--

		} else {
			lock.Unlock() //这里也要解锁,防止在为0的情况下,lock.Lock()后直接break没有释放锁
			fmt.Println("没有票了")
			wg.Done()
			break
		}
		lock.Unlock()
	}
}

11.2.2、读写互斥锁

很多实际场景下读多写少,当需要读取一个资源而不需要修改的时候是不需要加锁的。sync.RWMutex

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是和获取写锁就会等待;当一个goroutine获取到写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待

注意:在使用互斥锁的时候注意一定要解锁,并且建议使用defer解锁

package main

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

var (
   x = 0
   wg sync.WaitGroup
   lock sync.Mutex
   rwlock sync.RWMutex
)

func write(){
   //lock.Lock()
   rwlock.Lock()
   time.Sleep(5*time.Millisecond)
   x= x + 1
   //lock.Unlock()
   rwlock.Unlock()
   wg.Done()
}

func read(){
   //lock.Lock()
   rwlock.RLock()
   time.Sleep(time.Millisecond)
   fmt.Println(x)
   //lock.Unlock()
   rwlock.RUnlock()
   wg.Done()
}

func main(){
   start := time.Now()
   for i:= 0;i< 10;i++ {  //写入
      go write()
      wg.Add(1)
   }
   time.Sleep(time.Second)  //读取太快,会读取不到write之后的值
   for i:= 0;i< 1000;i++ {  //读取
      go read()
      wg.Add(1)
   }
   wg.Wait()
   fmt.Println(time.Now().Sub(start))
}

11.3、sync.once

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。(多个goroutine也只会执行一次)

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once

sync.Once只有一个Do方法,其签名如下:

func (o *Once) Do(f func()) {}
package main

import (
	"fmt"
	"sync"
)

func main() {
	var once sync.Once
	onceBody := func() {
		fmt.Println("Only once")
	}
	done := make(chan bool)
	for i := 0; i < 10; i++ {
		go func() {
			once.Do(onceBody)
			done <- true
		}()
	}
	for i := 0; i < 10; i++ {
		<-done
	}
}
执行输出:Only once //不管多少个goroutine只执行一次就可以了

11.4、sync.map

go语言内置的map不是并发安全的

var m = make(map[string]int)
var lck sync.Mutex

func get(s string)int{
   return m[s]
}

func set(key string,value int){
   m[key] = value
}

func main(){
   lk := sync.WaitGroup{}
   for i:=0;i<22;i++ {
      lk.Add(1)
      go func(n int){
         key := strconv.Itoa(n)
         lck.Lock()  //加锁
         set(key,n)
         lck.Unlock()  //加锁
         fmt.Printf("key:%v,value:%v\n",key,get(key))
         lk.Done()
      }(i)
   }
   lk.Wait()
}
/*
多运行几次报错
fatal error: concurrent map writes  并发映射写入
解决方法1:加锁
 */

像这种场景下就需要map加锁来保证并发的安全性了,go语言的syn包提供了一个开箱即用的并发安全版的map sync.map 。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时 sync.map 内置了诸如 store(设置值),load(获取值),loadOrStore,Delete,Range(遍历) 等操作方法。

var m = sync.Map{}

func main(){
   lg := sync.WaitGroup{}
   for i := 0;i<20;i++ {
      lg.Add(1)
      go func(n int) {
         key := strconv.Itoa(n)
         m.Store(key,n)  
         value,_ := m.Load(key) //根据map访问,这个map不需要make初始化
         fmt.Printf("k=%v,v=%v\n",key,value)
         lg.Done()
      }(i)
   }
   lg.Wait()
}

11.5、atomic

方法 解释
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
读取操作
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
写入操作
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
修改操作
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
交换操作
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
比较并交换操作
package main

import (
   "sync"
   "fmt"
   "sync/atomic"
)

var m int32 = 0
var wg sync.WaitGroup
var lock sync.Mutex

func add(){
   m++
   wg.Done()
}

func add2(){
   lock.Lock()
   m++
   defer lock.Unlock()
   wg.Done()
}

func add3(){
   atomic.AddInt32(&m,1)
   wg.Done()
}

func main(){
   //方式1:使用不加锁的方式,每次执行的结果会不一样,有可能为 99/100/101/....
   //for i :=0;i<10000;i++    {
   // wg.Add(1)
   // go add()
   //}
   //wg.Wait()
   //fmt.Println(m)

   //方式2:在1的基础上加锁,这次结果正常
   //for i :=0;i<10000;i++    {
   // wg.Add(1)
   // go add2()
   //}
   //wg.Wait()
   //fmt.Println(m)

   //方式3:使用atomic包进行操作
   for i :=0;i<10000;i++  {
      wg.Add(1)
      go add3()
   }
   wg.Wait()
   fmt.Println(m)
}

十二、channel

进程间通信方式:1)Pipe;2)message queue;3)Shared memory;4)sempaphore;5)Socket;6)signal;...

通道可以理解为管道,从一端流入,一端流出。

12.1、channel概述

单纯的将函数并发执行时没有意义的。函数与函数间需要交换数据并发执行才有意义

  • channel是引用类型

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes)Do not communicate by sharing memory; instead, share memory by communicating. “不要以共享内存的方式来通信,相反,要通过通信来共享内存。”

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。go从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据。所以go的做法就是通过通道来共享内存,是的内存数据在不同的goroutine中传递。而不是以共享内存来通信。

func chn1(){
   var b chan int //需要制定channel中元素的类型,channel需要初始化后才能使用
   fmt.Println(b)  //<nil>
   b = make(chan int)  //通道必须使用make初始化才能使用 make(chan $元素类型,[缓冲大小])
   b = make(chan int,16)  //待缓冲区的channel  make(chan $元素类型,[缓冲大小])
   fmt.Println(b)  //0xc000044060
}

12.2、channel的使用

通道的操作:1、发送ch1 <- 1。2、接收 <- ch1 。3、关闭 close

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

<-ch       // 从ch中接收值,忽略结果
package main

import (
	"fmt"
	"time"
)

var chn1, chn2 chan int

func f1() {
	for i := 0; i < 10; i++ {
		chn1 <- i
	}
	close(chn1)
}

func f2() {
	for {
		i, ok := <-chn1
		if !ok {
			break
		}
		chn2 <- i * 2
	}
	close(chn2)
}

func main() {
	var b chan int     //需要制定channel中元素的类型,channel需要初始化后才能使用
	fmt.Println(b)     //<nil>
	b = make(chan int) //通道必须使用make初始化才能使用 ,不带缓冲区

	//1、注意点1:发送数据,但是没有其他
	// b <- 10  //deadlock,因为没有人接受数据

	//2、解决方法1: range 方法,接收goroutine在后台运行,后面跟上发送goroutine。注意顺序不能反,反了就deadlock
	// go func() {
	// 	for tmp := range b {
	// 		fmt.Println("s1: ", tmp)
	// 	}
	// }()
	// b <- 10

	// 3、解决方法2:v,ok 方法。注意必须单独启一个goroutine去消费或者,单独启动一个goroutine去生产。
	// tmp := <-b //放在这里会deadlock,因为写在这里,main goroutine无法往下执行去创建子goroutine,因此deadlock
	go func() {
		b <- 10
	}()
	tmp := <-b
	fmt.Println("S3: ", tmp)

	//4、独立两个goroutine去生产和消费;这里生产者和消费者前后关系不重要,但是close必须用defer,否则会deadlock
	// go func() {
	// 	b <- 10
	// }()

	// go func() {
	// 	for tmp := range b {
	// 		fmt.Println("s1: ", tmp)
	// 	}
	// }()

	defer close(b)
	time.Sleep(time.Second)

}

案例1:

package main

import (
	"fmt"
	"sync"
)

func main() {
	ch1 := make(chan int, 10)
	//		for i := 0; i < 11; i++ { //最多只能存放10个数据,大于则"all goroutines are asleep - deadlock!"
	for i := 0; i < 10; i++ {
		ch1 <- i
	}
	//输出channel内容
	for i := 0; i < 10; i++ {
		tmp := <-ch1
		fmt.Print(" ", tmp) // 0 1 2 3 4 5 6 7 8 9
	}
	close(ch1) //注意一定要关闭通道
	chanel2()
}

func chanel2() {
	var wg sync.WaitGroup
	ch2 := make(chan int, 2)

	wg.Add(1)

	go func() {
		ch2 <- 10
		ch2 <- 20
		wg.Done()
	}()
	wg.Wait()
	fmt.Println()
	fmt.Println(<-ch2)
	fmt.Println(<-ch2)
	// fmt.Println(<-ch2)  //fatal error: all goroutines are asleep - deadlock!

	defer close(ch2)
}

或者:

func chanel2() {
	var wg sync.WaitGroup
	ch2 := make(chan int, 2)

	wg.Add(1)

	go func() {
		ch2 <- 10
		ch2 <- 20
		wg.Done()
	}()
	wg.Wait()
	close(ch2)  //close关闭放在前面
	for {
        v, ok := <-ch2  //关闭的通道,才能用这种方式判断,不然仍然会认为是ok的(即使数据已经全部取完)
		if !ok {
			break
		} else {
			fmt.Println(v)

		}
	}
}

案例2:

//1、启动一个goroutine,生成100个发送到chn1
//2、启动一个goroutine,从ch1中取值,计算其平方值放到chn2中
//3、在main函数中从chn2中取值并打印
//注意:wg.WaitGroup()是等待goroutine结束,close(chn1)是关闭channel

package main

import (
	"fmt"
	"time"
)

var chn1, chn2 chan int

func f1() {
	for i := 0; i < 10; i++ {
		chn1 <- i
	}
	close(chn1) //一定要注意关闭 
}

func f2() {
	for {
		i, ok := <-chn1
		if !ok {
			break
		}
		chn2 <- i * 2
	}
	close(chn2) //一定要注意关闭 
}

func main() {
	chn1 = make(chan int, 10)
	chn2 = make(chan int, 8)
	go f1()
	go f2()

	//读取方法1:range  main函数
	// for m := range chn2 {
	// 	fmt.Println(m)
	// }

	//读取方法2: v,ok main函数
	for {
		v, ok := <-chn2
		if !ok { //i,ok = <- chn1这种方式和for range一样。不过for range内部自己实现判断
			break
		}
		fmt.Println("value: ", v)
	}

	//读取方法3:子goroutine
	go func() {
		for {
			v, ok := <-chn2
			if !ok { //i,ok = <- chn1这种方式和for range一样。不过for range内部自己实现判断
				break
			}
			fmt.Println("value——sub: ", v)
		}
	}()
	time.Sleep(time.Second)
}
//格式2:完整的写法
var c1,c2 chan  int
var wg sync.WaitGroup

func f1(chn1 chan int){
   defer wg.Done()
   for i :=0;i<10 ;i++ {
      chn1 <- i
   }
   close(chn1)  //关闭之后仍然可以读,但是不能写
}

func f2(chn1,chn2 chan int){
   defer wg.Done()
   for i := range chn1 {
      chn2 <- i*i
   }
   close(chn2)
}

func main(){
   c1 = make(chan int,10)
   c2 = make(chan int,10)

   wg.Add(2)
   go f1(c1)  //不管使用使用 go f1()或者 go f2() 都要注意关闭channel
   go f2(c1,c2)
   wg.Wait()
   for m := range c2 {
      fmt.Println(m)
   }
}

12.3、通道类型

12.3.1、无缓冲通道

  • chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;

  • <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。

  • 注意:nil 是指 var声明了,但是没有make初始化

  • channel为nli状态下:发送(/接收deadlock),关闭(panic: close of nil channel);

  • channel为make(chan int)状态下,close(CHAN)不会panic。CH<- 10会panic,<-CH 不会panic

  • 如果channel 已经关闭,则可以使用v,ok := <- chan 或者 range chan的方式,如果channel没有关闭则直接读取会panic

  • channel是阻塞的,无论是发送数据还是接收数据,在同一时间都是阻塞的,只能由一个goroutine操作channel; 带有缓冲区的通道会有区别

channel nil 非空(有容量) 空的(没有容量) 满了 没满
接收 阻塞 容量为N,可接收N个值
值取出完毕后未close则deadlock
阻塞 接收 接收
发送 阻塞 容量为N,则可以存储N个值,超过N个则deadlock 发送值为空 阻塞 发送
关闭close(CHAN) panic 容量为N,则可以接收N个值,值取出完毕后读0值 close后返回零值 close且读取读完后读取0;close前读取读完后 deadlock 同左

通过channel来实现同步:

package main

import (
	"fmt"
)

func main() {
	var chn1 chan bool
	chn1 = make(chan bool)
	go func() {
		for i := 1; i <= 10; i++ {
			fmt.Println("goroutine...", i)
		}
		chn1 <- true
	}()
	data := <-chn1 //这里会一直阻塞到有人向其发送内容
	fmt.Println("data info is: ", data)
	fmt.Println("main over...")
	fmt.Println(x)
}

注意:不能直接这样使用 x := <-chn1; chn1 <- true 因为channel用于多个goroutine之间通信,不能直接main goroutine自己和自己通信。

注意:判断通道是否关闭·close(b);v, ok := <-b //打印0 false

12.3.2、带缓冲的通道

不带缓冲的通道,一次发送对应一次接收操作。对于一个goroutine来讲,它的一次发送,在另一个goroutine接收之前都是阻塞的。同样的对于接收来讲,在另一个goroutine发送之前,它也是阻塞的。

带缓冲的通道,只有在缓冲区满的时候才会阻塞。接收的时候也是在缓冲区为空的时候才会阻塞。

12.4、timer

标准库中的Timer让用户可以定义自己的超时逻辑。尤其是在应对select处理多个channel的超时、单channel读写的超时时等情形下尤为重要

Timer时一次性的时间触发事件,这点和Ticker不通。Ticker时按一定时间间隔持续发生时间事件

Timer的创建方式:

t := time.NewTimer(d)
t := time.AfterFunc(d,f)
c := time.After(d)  //一个channel 

Timer的三个要素:定时时间(d),触发动作(f),时间channel,也就是t.c
time.After,在等待持续时间之后,然后在返回的通道上发送当前时间。它相当于NewTimer(d).C
package main

import (
	"fmt"
	"time"
)

func main() {
	//1、初识
	// timer := time.NewTimer(3 * time.Second)
	// fmt.Println(time.Now()) //打印当前时间
	// t := timer.C
	// fmt.Println(<-t) //等待三秒后,打印时间

	//2、timer停止
	// timer2 := time.NewTimer(5 * time.Second)
	// go func() {
	// 	<-timer2.C
	// 	fmt.Println("Timer2 执行完毕")
	// }()

	// time.Sleep(3 * time.Second) //在子函数执行完毕之前执行完毕
	// flag := timer2.Stop()
	// if flag {
	// 	fmt.Println("timer2 被停止了...")
	// }

	//3、time.After
	ch := time.After(3 * time.Second)
	fmt.Printf("TYPE: %T\n", ch)
	fmt.Println(time.Now()) //当前时间:
	fmt.Println(<-ch)       //等待三秒后,输出的时间
}

12.5、Goroutine pool

编写代码实现一个计算机随机数的每个位置的数字之和的程序。要求使用goroutine和channel构建生产者和消费者模式。可以指定启动的goroutine的数量-work pool模式

在工作中我们经常使用workpool模式,控制goroutine的数量,防止goroutine泄露和暴涨

package main

import (
   "fmt"
   "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
   for j := range jobs {
      fmt.Printf("worker:%d start job:%d\n", id, j)
      time.Sleep(time.Second)
      fmt.Printf("worker:%d end job:%d\n", id, j)
      results <- j * 2
   }
}

func main() {
   jobs := make(chan int, 100)
   results := make(chan int, 100)  //2个channel
   for w := 1; w <= 3; w++ {  // 开启3个goroutine,但是具有5个任务
      go worker(w, jobs, results)
   }

   for j := 1; j <= 5; j++ {  // 5个任务,3个goroutine谁先干完,谁先干剩余的job
      jobs <- j
   }

   close(jobs) //这个去掉 也不会报错

   for a := 1; a <= 5; a++ {  // 输出结果
      x := <-results
      fmt.Println("result:",x)
   }
}
func worker(id int, jobs chan int, results chan int) {
   for i := range jobs {
      fmt.Printf("goroutine:%d job:%d start..\n",id,i)
      time.Sleep(time.Second)
      fmt.Printf("goroutine:%d job:%d end..\n",id,i)
      results <- i *2 //接收的值是 1,2,3,4,5
   }
}

//3个goroutine,2个channel,5个信息要在2个channel中处理。
func main(){
   job := make(chan int,100)
   result := make(chan int,100)

   //1、创建3个goroutine
   for i:=1 ;i <=3 ;i ++ {
      go worker(i,job,result)
   }

   //2、向job传输数据
   for i :=1;i<=5;i++ {
      job <- i
   }

   //3、输出结果

//方式1,这种就不会报错
   for a:=1;a<=5 ;a++ {  //只会遍历5次
      <- results
   }
    
//方式2,会报错 fatal error: all goroutines are asleep - deadlock!,
    //这个报错是发生了阻塞,只有关闭了result,ok的值才会变为false
// for  {
//    v,ok :=  <- result
//    if !ok {
//       break
//    }
//    fmt.Println(v)
// }
    
//方式3:也会报错fatal error: all goroutines are asleep - deadlock!,原因是
// count := 0
//	for  {
//    	count++
//    	fmt.Println("Cycle: ",count)  //输出Cycle: 6之后报错all goroutines are asleep - deadlock!
//		v,ok := <- results
//		if !ok {
//			break
//		}
//		fmt.Println(v)
//	}
}

小结:
当results这个channel为空,并且么有clse(results)时,v,ok := <- results 会报错:fatal error: all goroutines are asleep - deadlock!

要解决的问题:
	1、如果使用方式2和方式3的方式遍历 ,//看内容回顾章节
	2、输出结果为什么会用 worker的printf语句
	3、没有用sync.WaitGrooup

看报错:创建了不止一个goroutine

12.6、select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会:

for{
    // 尝试从ch1接收值
    data, ok := <-ch1
    // 尝试从ch2接收值
    data, ok := <-ch2
    …
}

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

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

select{
    case <-ch1:
        ...
    case data := <-ch2:
        ...
    case ch3<-data:
        ...
    default:
        默认操作
}
func main() {
	ch := make(chan int, 1) //如果容量不是 1,结果就不可预测了
	for i := 0; i < 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
		}
	}
}
/*
0
2
4
6
8
*/

使用select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。其他不会执行
  • 对于没有caseselect{}会一直等待,可用于阻塞main函数。
  • 每个case 都必须为channel;并且所有channel的表达式都会被求值
  • 如果没有case可以运行,看是否有default,如果有就执行default。如果没有就进入阻塞,直到有case可以运行
package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(time.Second * 3)
		ch1 <- 100
	}()
	go func() {
		time.Sleep(time.Second * 3)
		ch2 <- 200
	}()

	select {
	case num1 := <-ch1:
		fmt.Println("ch1获取数据: ", num1)
	case num2, ok := <-ch2:
		if ok {
			fmt.Println("ch2获取数据: ", num2)
		}
	}
	close(ch1)

	close(ch2)
}

//执行结果,可能为 ch1获取数据 也可能时ch2获取数据
package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(time.Second * 3)
		ch1 <- 100
	}()
	go func() {
		time.Sleep(time.Second * 3)
		ch2 <- 200
	}()

	select {
	case num1 := <-ch1:
		fmt.Println("ch1获取数据: ", num1)
	case num2, ok := <-ch2:
		if ok {
			fmt.Println("ch2获取数据: ", num2)
		}
	default:
		fmt.Println("default语句")
	}
	close(ch1)

    close(ch2)
}
//这里只会执行default语句

等待超时设置:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	select {
	case <-ch1:
		fmt.Println("ch1执行 ")
	case <-ch2:
		fmt.Println("ch2执行: ")
	case <-time.After(3 * time.Second):
		fmt.Println("case3 可以执行,timeout...")
	}
	close(ch1)

	close(ch2)
}

12.6、示例

使用goroutinechannel实现一个计算int64随机数各位数和的程序。

  1. 开启一个goroutine循环生成int64类型的随机数,发送到jobChan
  2. 开启24个goroutinejobChan中取出随机数计算各位数的和,将结果发送到resultChan
  3. goroutineresultChan取出结果并打印到终端输出
package main

import (
   "math/rand"
   "time"
   "sync"
   "fmt"
)

//1. 开启一个`goroutine`循环生成int64类型的随机数,发送到`jobChan`
//2. 开启24个`goroutine`从`jobChan`中取出随机数计算各位数的和,将结果发送到`resultChan`
//3. 主`goroutine`从`resultChan`取出结果并打印到终端输出

type Job struct {
   value int64
}

type Result struct {
   job *Job
   sum int64
}

var jobChan = make(chan *Job,100)
var resultChan = make(chan *Result,100)
var wg sync.WaitGroup

//生成随机数,写入到jobChan
func producer(z1 chan<-  *Job){
   defer wg.Done()
   for {
      r1 := rand.Int63()
      newJob := &Job{ //结构体初始化后才能使用
         value:r1,
      }
      z1 <- newJob
      time.Sleep(time.Millisecond * 500)
   }

}

//获取jobChan的内容,处理后写入到 resultChan
func consumer(z1 <- chan *Job,resultChan chan <- *Result ){
   defer wg.Done()
   for {
      job := <- z1
      sum := int64(0)
      n := job.value
      for n > 0 {
         sum += n % 10
         n = n /10
      }
      newResult := &Result{
         job: job,
         sum:sum,
      }
      resultChan <- newResult
   }

}

func main(){
   wg.Add(1)
   go producer(jobChan)
   wg.Add(24)
   for i :=0 ;i < 24;i ++ {
      go consumer(jobChan,resultChan)
   }
   for result := range resultChan {
      fmt.Printf("value:%d sum:%d\n",result.job.value,result.sum)
   }
   wg.Wait()
}

12.7、channel注意事项

注意发送数据的位置

var ch1 = make(chan bool)
var ch2 = make(chan bool)

func main() {
	fmt.Println("main开始")
	go c1()
	go c2()
	<-ch1
	<-ch2
  	fmt.Println("main结束")
	close(ch1)
	close(ch2)
    //time.Sleep(time.seconds)
}

func c1() {
	ch1 <- true
	fmt.Println("c1执行")
}

func c2() {
	ch2 <- true  //这里去掉会导致deadlock,因为main函数一直在等待 <-ch2
	fmt.Println("c2执行")
}
这里可能会出现三种情况:因为在c2执行完毕的情况下
main开始  //main gourinte阻塞在ch1,在ch2执行(fmt.Println"c2执行")的时候main函数已经结束了,如果在main()中加上time.sleep(time.second)就可以看到两个都结束了
c1执行
main结束


main开始  //main gourinte 退出之前ch2执行了
c1执行
main结束
c2执行

main开始  //main函数在print ”main结束“之前ch1和出都执行完毕了
c1执行
c2执行
main结束

改进:

var ch1 = make(chan bool)
var ch2 = make(chan bool)

func main() {
	fmt.Println("main开始")
	go c1()
	go c2()
	<-ch1  //让main函数一直处于阻塞状态
	<-ch2
	fmt.Println("main结束")
	close(ch1)
	close(ch2)
}

func c1() {
	fmt.Println("c1执行")
	ch1 <- true  //在执行完毕后再发送消息到通道
}

func c2() {
	fmt.Println("c2执行")
	ch2 <- true

}
posted @ 2021-08-26 14:35  MT_IT  阅读(184)  评论(0)    收藏  举报