当前位置:   article > 正文

Golang_go打包成exe还需要依赖吗

go打包成exe还需要依赖吗

一、Go语言基础

运行.go 程序

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

第一种:先通过go build xxx编译成 .exe 文件再运行这个exe文件(不依赖环境);
第二种:直接使用 go run xxx命令

安装go的sdk

golang社区:https://golang.google.cn/dl/
在社区中能下载不同版本的golang
在这里插入图片描述

配置系统环境变量

1、配置GOROOT系统环境变量
在这里插入图片描述
2、配置go语言SDK环境变量
在这里插入图片描述

执行流程

在这里插入图片描述

执行流程的方式区别

1、在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件
变大了很多。生成可执行文件后,这个文件可以在没有go开发环境下运行;

  • 使用第一种方式编译还可以进行重命名,go build -o 重命名.exe 源文件.exe

2、使用go run xxx执行代码时,如果要在另外一个机器上这么运行,则需要go
开发环境,否则无法执行。

语法注意事项

  • 源文件以"go"为扩展名。
  • 程序的执行入口是main()函数。
  • 严格区分大小写。
  • 大括号都是成对出现的。
  • 定义的变量或者import的包如果没有使用到,代码不能编译通过。
  • 方法由一条条语句构成,每个语句后不需要分号(Go语言会在每行后自动加分号),这也体现出Golang的简洁性。
  • Go编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否则报错。

Go语言的标准库

在这里插入图片描述

go语言具有中文社区(https://studygolang.com/),在其中可以看到Go语言提供的标准库以及相关用法。

二、数据类型

在这里插入图片描述

基本数据类型声明变量

package main

import "fmt"

func main() {
	// 声明后赋值
	var a int
	a = 10
	fmt.Println(a)
	// 声明并赋值
	var b int = 20
	fmt.Println(b)
	// 语法糖声明赋值
	c := 30
	fmt.Println(c)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

1、整数类型

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

使用整数类型声明变量
在这里插入图片描述

  • 指定类型的变量的值不能大于类型的最大值例如int8不能大于127否则会报错
  • 如果给一个未声明类型的变量赋值,则这个变量会具有隐式类型
  • 利用fmt库的 %T 能获取当前变量的类型
  • 使用unsafe库的Sizeof方法可以看当前变量占用字节数

2、浮点类型

浮点数类型主要分为了float32和float64,如果不给定类型则默认为float64类型,浮点数可能会存在精度缺失的问题。
在这里插入图片描述

3、字符类型

在Golang中如果要存储单个字符,一般使用byte保存,使用的是UTF-8字符编码:

  • 字母,数字,标点等字符,底层是按照ASCII进行存储;
  • 汉字使用的是unicode编码;
  • 字符类型,本质上就是一个整数,也可以直接参与运算,输出字符的时候,会将对应的码值做一个输出;
  • 使用fmt标准库的 ‘%c’ 能格式化输出,将字符原样输出。
package main
import "fmt"

func main() {
	var a byte = 'a'
	fmt.Println(a) //97
	var b byte = '('
	fmt.Println(b + 20) //40
	var c int = '中'
	fmt.Println(c) //20013
	var d byte = 'A'
	fmt.Printf("c5对应的具体的字符为:%c", d) //A
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

4、布尔类型

占用一个字节。

5、字符串类型

如果字符串类型中字符含有特殊字符则可以使用反引号``包裹。

复杂数据类型

1、指针类型

声明变量实际上我们的变量是保存在一个内存地址中的

func main(){
   var a int = 10
   //&符号+变量 就可以获取这个变量内存的地址
   fmt.Println(&a) // 0xc0000a2058
   // 定义指针类型,想要定义指针类型使用 *数据类型的方法
   var b *int = &a
   fmt.Println(b) // 0xc0000a2058
   fmt.Println("%v",*b) // 10 ===> 使用*内存地址可以得到源地址
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

数据类型转换

1、默认值

在这里插入图片描述

2、数据类型转换

Go在不同类型的变量之间赋值时需要显式转换,并且只有显式转换(强制转换)

2.1、基本数据类型转string
方式1:fmt.Sprintf(“%参数”,表达式)
这里第一个参数传递的是需要转换的参数类型例如:%d(整数)、%f(浮点数)、%t(布尔值)、%c(字符类型)

var a int = 10
var a1 string = fmt.Sprintf("%d",a) // '10'
var b float32 = 10.01
var b1 string = fmt.Sprintf("%f",b) // '10.01'
var c bool = false
var c1 string = fmt.Sprintf("%t",c) // 'false'
var d byte = 'd'
var d1 string = fmt.Sprintf("%c",d) // 'd'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

方式2:使用strconv包的函数
strconv包提供了FormatInt、FormatFloat、FormatBool等格式化方法,但使用方式较为复杂,限制较多一般不使用。

 var a int = 18
 var a1 string = strconv.FormatInt(int64(a),10) 
 //参数:第一个参数必须转为int64类型 ,第二个参数指定字面值的进制形式
  • 1
  • 2
  • 3

2.2、string转基本数据类型
string转基本数据类型使用到了strconv包的函数

  • strconv.ParseInt (目标字符串, 进制, 整数类型)
  • strconv.ParseFloat (目标字符串, 浮点数类型)
  • strconv.ParseBool (目标字符串)

以上函数会返回两个值,value数据,和err出现的错误

 var a string = "19"
 var a1 int64
 a1,_ = strconv.ParseInt(a,10,64)
 fmt.Printf("类型是:%T,a1=%v \n",a1,a1)
  • 1
  • 2
  • 3
  • 4

自定义包的使用

  • 不能与标准库冲突
  • 需要配置对于项目的GOPATH
  • 引入自定义包时,直接从GOPATH\src后引入即可
  • 工具包需要被其他包调用的属性名需要大写
工具包
package test
var TestA int = 10

主包
package main
import (
	"fmt"
	"module02/test"
)
func main() {
	fmt.Println(test.TestA)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

三、运算符

在这里插入图片描述

特殊的运算符

1、++与–

**算术运算符规则和JS基本一致,只有++,–有区别,**区别如下:
go语言里,++,–操作非常简单,只能单独使用,不能参与到运算中去,++,--只能在变量的后面,不能写在变量的前面 --a ++a 错误写法。

func main(){
	var a int = 10
	a++
	fmt.Println(a) //11
	a--
	fmt.Println(a) //10
	var b int = a + a-- // syntax error: unexpected -- at end of statement
	fmt.Println(b)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2、&与*

&返回变量的存储地址
*取指针变量对应的数值

package main
import "fmt"
func main(){
    var a int = 10
    fmt.Println(&a) //0xc0000100b0
    var b *int = &a
    fmt.Println(b)
    fmt.Println(*ptr) //10
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

获取用户终端输入

有两种方式:

  • fmt.scanln()
    在这里插入图片描述
//方式1:Scanln
var age int
fmt.Println("请录入学生的年龄:")
//传入age的地址的目的:在Scanln函数中,对地址中的值进行改变的时候,实际外面的age被影响了
fmt.Scanln(&age)//录入数据的时候,类型一定要匹配,因为底层会自动判定类型的
var name string
fmt.Println("请录入学生的姓名:")
fmt.Scanln(&name)
var score float32
fmt.Println("请录入学生的成绩:")
fmt.Scanln(&score)
var isVIP bool
fmt.Println("请录入学生是否为VIP:")
fmt.Scanln(&isVIP)
//将上述数据在控制台打印输出:
fmt.Printf("学生的年龄为:%v,姓名为:%v,成绩为:%v,是否为VIP:%v",age,name,score,isVIP)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • fmt.scanf()
    在这里插入图片描述
//方式2:Scanf
fmt.Println("请录入学生的年龄,姓名,成绩,是否是VIP,使用空格进行分隔")
fmt.Scanf("%d %s %f %t",&age,&name,&score,&isVIP)
//将上述数据在控制台打印输出:
fmt.Printf("学生的年龄为:%v,姓名为:%v,成绩为:%v,是否为VIP:%v",age,name,score,isVIP)
  • 1
  • 2
  • 3
  • 4
  • 5

四、流程控制

1、条件分支

分为了if语句和switch语句

	if a := 20; a > 30 {
		fmt.Println("A")
	} else {
		fmt.Println("B")
	}

	var b int = 20
	switch b {
	case 10, 20:
		fmt.Println("10")
	case 30:
		fmt.Println("20")
		fallthrough   //switch穿透
	default:
		fmt.Println("其他")
	}
	
	var c int = 30
	switch {
	case c > 20:
		fmt.Println("10")
	case c > 30:
		fmt.Println("20")
		fallthrough //switch穿透
	default:
		fmt.Println("其他")
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 针对于if结构需要注意的是if后的小括号可以不写,但是大括号必须写
  • 针对于switch语句的switch后可以不添加变量,可以直接将变量判断写在case后
  • switch语句每个case不用跟break
  • switch中每个case的代码块可以使用fallthrough进行switch穿透,直接运行到下一个case中

2、循环结构

普通的for循环或for range循环。for range类似于js中的foreach方法。

	for e := 0; e < 5; e++ {
		fmt.Println(e)
	}

	var str = "hello World!"
	for index, value := range str {
		fmt.Printf("%d,%c\n", index, value)
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

关键字 break continue return goto

使用方式和js一致,但添加了两个新的用法

  • goto可以跳到标记标签
  • label: 使用此形式可以定义一个标签
    label:
	for e := 0; e < 5; e++ {
		for f := 0; f < 5; f++ {
			if f == 3 {
				continue label
			}
			fmt.Println(f)
		}
	}
	// continue的作用是结束离它近的那个循环,继续离它近的那个循环,但这里结束的是里层循环,继续的是外层循环。
	
	// goto
	fmt.Println(1)
	goto label
	fmt.Println(2)
	fmt.Println(3)
	fmt.Println(4)
    label:
	fmt.Println(5)
    // 最终打印1 5,会跳过2 3 4
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

五、函数与包

1、函数

package utils

import "fmt"

func AddNum (a int, b int) int  {
	sum := a + b
	return sum
}

func AddNumOrigin (a int, b int) (int,int,int)  {
	sum := a + b
	return sum, a, b
}

func MulPrint (args...int)  {
	for i := 0; i < len(args); i++ {
		fmt.Println(args[i])
	}
}

________________________________________________________

package main

import (
	"fmt"
	"module05/utils"
)

func main()  {
	var res = utils.AddNum(12,23)
	var res2,or1,or2 = utils.AddNumOrigin(12,23)
	fmt.Println(res)
	fmt.Println(res2, or1, or2)
    utils.MulPrint(1,2,3,4)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 可以传递任意个参数,参数使用args…数据类型进行接收。
  • 可以传递一个值的内存地址,这样在函数内部也能修改到函数外部传递进入的值。
package main

import "fmt"

//定义一个函数:
func test(num int) {
	fmt.Println(num)
}

func test02(num1 int, num2 float32, testFunc func(int)) {
	testFunc(1)
	fmt.Println("-----test02")
}
func main() {
	a := test                                        
	fmt.Printf("a的类型是:%T,test函数的类型是:%T \n", a, test)
	//a的类型是:func(int),test函数的类型是:func(int)
	a(10)
	test02(10, 3.19, test)
	test02(10, 3.19, test)
	test02(10, 3.19, a)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • go语言也能将函数作为参数传递给另一个函数
  • 支持对函数返回值命名
func test03(num1 int, num2 int) (int, int) {
	sub := num1 - num2
	sum := num1 + num2
	return sum, sub
}

func test04(num1 int, num2 int) (sum int,sub int) {
	sum = num1 - num2
	sub = num1 + num2
	return
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

2、包

什么是包?
在程序层面,所有使用相同 package 包名 的源文件组成的代码模块,在同一个包下不能有重复的声明。
在这里插入图片描述
使用包的原因:

  • 我们不可能把所有的函数放在同一个源文件中,可以分类的把函数放在不同的源文件中
  • 解决同名问题,在同一个文件中是不可以定义相同名字的函数的,此时可以用包来区分

包引用时也能起别名

package main

import (
	"fmt"
	alias "module05/utils"
)

func main()  {
	var res = alias.AddNum(12,23)
	var res2,or1,or2 = alias.AddNumOrigin(12,23)
	fmt.Println(res)
	fmt.Println(res2, or1, or2)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

3、关于init函数、main函数和其他函数的执行时机

先执行引入包中的函数、再执行当前包的函数
而这里针对于函数的优先级为:变量定义 > init函数 > main函数
在这里插入图片描述

4、匿名函数与闭包

Go支持匿名函数,如果希望函数只是使用一次,可以考虑使用匿名函数,在声明时就调用
在这里插入图片描述
针对于闭包的话,按照下面的例子解释更加清晰:

  • 不使用闭包的时候:我想保留的值,不可以反复使用
  • 闭包应用场景:闭包可以保留上次引用的某个值,我们可以反复使用上次的值
package main
import "fmt"

func getSum() func (int) int {
        var sum int = 0
        return func (num int) int{
                sum = sum + num 
                return sum
        }
}


func getSum01(sum int,num int) int{
        sum = sum + num
        return sum
}

func main(){
        f := getSum()
        fmt.Println(f(1))//1 
        fmt.Println(f(2))//3
        fmt.Println(f(3))//6
        fmt.Println(f(4))//10

        fmt.Println(getSum01(0,1))//1
        fmt.Println(getSum01(1,2))//3
        fmt.Println(getSum01(3,3))//6
        fmt.Println(getSum01(6,4))//10
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

5、系统函数

字符串函数

len() :统计字符串的长度

str := "golang你好"
fmt.Println(len(str)) // 12 (因为在golang中,汉字是三个字节)
  • 1
  • 2

for-range

str := "golang"
	for i, v := range str {
		fmt.Printf("%d,%c", i, v)
	}
  • 1
  • 2
  • 3
  • 4

res := []rune(str)

	str := "golang"
	res := []rune(str)
	fmt.Println(res) //[103 111 108 97 110 103]
	for i := 0; i < len(res); i++ {
		fmt.Printf("%c\n", res[i])
	}
	/* 
	g
	o
	l
	a
	n
	g
	*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

字符串转整数

n, err := strconv.Atoi("10") 
  • 1

整数转字符串

str := strconv.Itoa(10)
  • 1

查找子串是否在指定的字符串中

strings.Contains("golanggolang", "go")
  • 1

统计一个字符串有几个指定的子串

strings.Count("golanggolang","a")
  • 1

不区分大小写的字符串比较

strings.EqualFold("go" , "Go")
  • 1

返回子串在字符串第一次出现的索引值,如果没有返回-1

strings.lndex("golanggolang" , "a") 
  • 1

日期函数

时间和日期的函数,需要导入time包,所以你获取当前时间,就要调用**time.Now()**函数。

  • Now()返回值是一个结构体,类型是:time.Time
package main
import (
        "fmt"
        "time"
)
func main(){
        now := time.Now()
        fmt.Printf("%v ~~~ 对应的类型为:%T\n",now,now)
        //2021-02-08 17:47:21.7600788 +0800 CST m=+0.005983901 ~~~ 对应的类型为:time.Time
        fmt.Printf("年:%v \n",now.Year())
        fmt.Printf("月:%v \n",now.Month()) //月:February
        fmt.Printf("月:%v \n",int(now.Month())) //月:2
        fmt.Printf("日:%v \n",now.Day())
        fmt.Printf("时:%v \n",now.Hour())
        fmt.Printf("分:%v \n",now.Minute())
        fmt.Printf("秒:%v \n",now.Second())
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

格式化日期

	 //Printf将字符串直接输出:
	 fmt.Printf("当前年月日: %d-%d-%d 时分秒:%d:%d:%d  \n",now.Year(),now.Month(),
	 now.Day(),now.Hour(),now.Minute(),now.Second())
	 //Sprintf可以得到这个字符串,以便后续使用:
	 dateStr := fmt.Sprintf("当前年月日: %d-%d-%d 时分秒:%d:%d:%d  \n",now.Year(),now.Month(),
	 now.Day(),now.Hour(),now.Minute(),now.Second())
	 fmt.Println(dateStr)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

按照指定格式输出,这里每个数字都是固定的不能改变,但是可以随意组合。

	  //这个参数字符串的各个数字必须是固定的,必须这样写
	  datestr2 := now.Format("2006/01/02 15/04/05")
	  fmt.Println(datestr2)
	  //选择任意的组合都是可以的,根据需求自己选择就可以(自己任意组合)。
	  datestr3 := now.Format("2006 15:04")
	  fmt.Println(datestr3)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

内置函数

Golang设计者为了编程⽅便,提供了⼀些函数,这些函数不⽤
导包可以直接使⽤,我们称为Go的内置函数/内建函数。
常⽤函数:
(1)len函数:
统计字符串的⻓度,按字节进⾏统计
在这里插入图片描述

(2)new函数:
分配内存,主要⽤来分配值类型(int系列, float系列, bool,
string、数组和结构体struct),返回的是Type的指针
在这里插入图片描述

(3)make函数:
分配内存,主要⽤来分配引⽤类型(指针、slice切⽚、map、管
道chan、interface 等),返回的是Type本身

defer关键字

defer是Go里面的一个关键字,用在方法或函数前面,作为方法或函数的延迟调用;
作用是进行优雅释放资源
官方对defer的解释中写到每次defer语句执行的时候,会把函数压栈,同时函数参数会被拷贝下来。这两点很重要:

  • 一是说明当一个函数中有多个defer的时候,执行顺序是先进后出

  • 二是说明延迟函数的参数在defer语句出现时就已经确定下来了

func CopyFile(dstFile, srcFile string) (wr int64, err error) { 
    src, err := os.Open(srcFile)     
    if err != nil {         
        return     
    }      
    dst, err := os.Create(dstFile)     
    if err != nil {         
        return    
    }      
    wr, err = io.Copy(dst, src)     
    dst.Close()     
    src.Close()     
    return 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
func main() {
	deferRun()
	deferRun2()
 }
 func deferRun() {
	var num = 1
	defer test(&num)
	num = 2
	return
 }

 func test (b *int) {
	fmt.Println(*b)
 }

 func deferRun2() {
	var arr = [4]int{1, 2, 3, 4}
	defer printArr(&arr)
	arr[0] = 100
	return
 }

 func printArr(arr *[4]int) {
	for i := range arr {
	   fmt.Println(arr[i])
	}
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

defer+recover机制处理错误

程序中出现错误/恐慌以后,程序被中断,⽆法继续执⾏。这里可以引入机制:defer+recover机制处理错误;
在这里插入图片描述

func main() {
	test()
	fmt.Println("除法成功")
	fmt.Println("正常执行")
}

func test() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("错误信息", err)
		}
	}()
	num1 := 10
	num2 := 0 // 0不能做除数
	result := num1 / num2
	fmt.Println(result)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
package main

import (
	"fmt"
	"errors"
)

func main() {
	err := test()
	if err != nil {
		fmt.Println("自定义错误",err)
		panic(err) //还有可能的情况就是,遇到错误后直接结束。
	}
	fmt.Println("除法成功")
	fmt.Println("正常执行")
 }

 func test()(err error){
	num1 := 10
	num2 := 0
	if num2 == 0 {
		return errors.New("除数不能为0")
	}else {
		result := num1 / num2
		fmt.Println(result)
		return nil
	}
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

六、数组

数组定义格式

var 数组名 [数组⼤⼩]数据类型

func main() {
	var scores [5]int
	scores[0] = 95
	scores[1] = 91
	scores[2] = 39
	scores[3] = 60
	scores[4] = 21
	fmt.Println(scores)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

数组的初始化方式

func main() {
	//第⼀种:
	var arr1 [3]int = [3]int{3, 6, 9}
	fmt.Println(arr1)
	//第⼆种:
	var arr2 = [3]int{1, 4, 7}
	fmt.Println(arr2)
	//第三种:
	var arr3 = [...]int{4, 5, 6, 7}
	fmt.Println(arr3)
	//第四种:
	var arr4 = [...]int{2: 66, 0: 33, 1: 99, 3: 88}
	fmt.Println(arr4)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

二维数组

func main() {
 	// 二维数组的定义
	var arr1 [3][2]int
	// fmt.Printf("%T",arr1)
	arr1[0][0] = 10
	arr1[1][0] = 11
	arr1[2][0] = 12
	fmt.Println(arr1)

	// 二维数组初始化
	var arr2 = [2][2]int{{1,2},{3,4}}
	fmt.Println(arr2)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

数组的遍历

  • for循环遍历
  • for range遍历
for key, val := range 数组 {
  ...
}
  • 1
  • 2
  • 3

七、切片(slice)

切片(slice)是golang中⼀种特有的数据类型;
切片是⼀种建⽴在数组类型之上的抽象,它构建在数组之上并且提供更强⼤的能⼒和便捷;
切片(slice)是对数组⼀个连续片段的引用,所以切片是⼀
个引用类型。这个片段可以是整个数组,或者是由起始和终止索
引标识的⼀些项的子集。需要注意的是,终止索引标识的项不包
括在切片内。

切片定义格式

方式一

var 切⽚名 []类型 = 数组的⼀个⽚段引⽤

func main() {
	var arr = [5]int{1,2,3,4,5}
	slice1 := arr[1:3]
	fmt.Println(slice1) //[2,3]
	// 切片的长度
	fmt.Println(len(slice1)) //2
	// 获取切片的容量,这个是可以动态变化的
	fmt.Println(cap(slice1)) //4
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

方式二

var 切⽚名 type = make([], len,[cap])

slice2 := make([]int,4,20) //类型,长度,类型
	fmt.Println(slice2)
	// 切片的长度
	fmt.Println(len(slice2)) 
	// 获取切片的容量,这个是可以动态变化的
	fmt.Println(cap(slice2)) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

方式三

slice3 := []int{1,2,3} //类型,长度,类型
	fmt.Println(slice3)
	// 切片的长度
	fmt.Println(len(slice3)) 
	// 获取切片的容量,这个是可以动态变化的
	fmt.Println(cap(slice3)) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

切片的遍历

  • for循环常规方式遍历
  • for-range 结构遍历切片
package main
import "fmt"
func main(){
        //定义切片:
        slice := make([]int,4,20)
        slice[0] = 66
        slice[1] = 88
        slice[2] = 99
        slice[3] = 100
        //方式1:普通for循环
        for i := 0;i < len(slice);i++ {
                fmt.Printf("slice[%v] = %v \t" ,i,slice[i])
        }
        fmt.Println("\n------------------------------")
        //方式2:for-range循环:
        for i,v := range slice {
                fmt.Printf("下标:%v ,元素:%v\n" ,i,v)
        }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

注意事项

1、简写方式:

  • var slice = arr[0:end] 等价于 var slice = arr[:end]
  • var slice = arr[start:len(arr)] 等价于 var slice = arr[start:]
  • var slice = arr[0:len(arr)] 等价于 var slice = arr[:]

2、切片不能越界
3、切片后还能继续切片
4、切片增加元素

  • 通过append函数将切片追加给切片:
      //定义数组:
      var intarr [6]int = [6]int{1,4,7,3,6,9}
      //定义切片:
      var slice []int = intarr[1:4]
      slice2 := append(slice,88,50) 
      fmt.Println(slice2) //[4 7 3 88 50]
      slice2 = append(slice2,slice...) 
      fmt.Println(slice2)//[4 7 3 88 50 4 7 3]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

5、切片的拷贝

func main() {
	//定义切片:
	var a []int = []int{1, 4, 7, 3, 6, 9}
	//再定义一个切片:
	var b []int = make([]int, 10)
	//拷贝:
	copy(b, a) 
	//将a中对应数组中元素内容复制到b中对应的数组中
	//元素多了会丢失多余的,元素少了会补0 
	fmt.Println(b) //[1 4 7 3 6 9 0 0 0 0]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

八、map(映射)

映射(map), Go语言中内置的一种类型,它将键值对相关联,我们可以通过键 key来获取对应的值 value;

基本语法

// 声明map,但暂未分配空间
var map变量名 map[key type]value type
PS:key、value的类型:bool、数字、string、指针、channel 、还可以是只包含前面几个类型的接口、结构体、数组
PS:key通常为intstring类型,value通常为数字(整数、浮点数)、stringmap、结构体
PS:key:slice、map、function不可以

map的特点:
(1map集合在使用前一定要make2map的key-value是无序的
(3)key是不可以重复的,如果遇到重复,后一个value会替换前一个value
(4)value可以重复的

package main
import "fmt"
func main(){
        //定义map变量:
        var a map[int]string
        a = make(map[int]string,10) //map可以存放10个键值对
        //将键值对存入map中:
        a[20095452] = "张三"
        a[20095387] = "李四"
        a[20097291] = "王五"
        a[20095337] = "朱六"
        a[20096699] = "张三"
        //输出集合
        fmt.Println(a) // map[20095385:朱六 20095387:李四 20095452:张三 20096699:张三20097291:王五]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

map的创建

package main
import "fmt"

func main() {
	//方式1:
	//定义map变量:
	var a map[int]string
	//只声明map内存是没有分配空间
	//必须通过make函数进行初始化,才会分配空间:
	a = make(map[int]string, 10) //map可以存放10个键值对
	//将键值对存入map中:
	a[20095452] = "张三"
	a[20095387] = "李四"
	//输出集合
	fmt.Println(a)
	//方式2:使用语法糖
	b := make(map[int]string)
	b[20095452] = "张三"
	b[20095387] = "李四"
	fmt.Println(b)
	//方式3:设置初始值
	c := map[int]string{
		20095452: "张三",
		20098765: "李四",
	}
	c[20095387] = "王五"
	fmt.Println(c)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

map的操作

  • 增加和更新操作:
    map[“key”]= value ——》 如果key还没有,就是增加,如果key存在就是修改。
  • 删除操作:
    delete(map,“key”) , delete是一个内置函数,如果key存在,就删除该key-value,如果k的y不存在,不操作,但是也不会报错
  • 清空操作:
    (1)如果我们要删除map的所有key ,没有一个专门的方法一次删除,可以遍历一下key,逐个删除
    (2)或者map = make(…),make一个新的,让原来的成为垃圾,被gc回收
  • 查找操作:
    value ,bool = map[key]
    value为返回的value,bool为是否返回 ,要么true 要么false

九、面向对象

(1)Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。
(2)Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解Gelang是基于struct来实现OOP特性的。
(3)Golang面向对象编程非常简洁,去掉了传统OOP语言的方法重载、构造函数和析构函数、隐藏的this指针等等
(4)Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承:Golang没有extends 关键字,继承是通过匿名字段来实现。

结构体

创建结构体实例方式

方式一:直接创建

package main
import "fmt"

type Teacher struct {
	Name   string
	Age    int
	School string
}

func main() {
	var t1 Teacher
	fmt.Println(t1)
	t1.Name = "terry"
	t1.Age = 23
	t1.School = "东软"
	fmt.Println(t1)
	fmt.Println(t1.Age + 10)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

方式二:创建时携带初始值

	var t1 Teacher = Teacher{"terry", 23, "东软"}
  • 1

方式三:返回的是结构体指针

    var t1 *Teacher = &Teacher{"terry", 23, "东软"}
	(*t1).Name = "张三"
	fmt.Println(*t1)
  • 1
  • 2
  • 3

方式四:返回的是结构体指针

	var t1 *Teacher = &Teacher{"terry", 23, "东软"}
	(*t1).Name = "张三"
	fmt.Println(*t1)
  • 1
  • 2
  • 3

结构体之间的转换

  • 结构体之间转换必须要有完全相同的字段(名字、个数和类型)
  • 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
package main
import "fmt"
type Student struct {
        Age int
}
type Stu Student
func main(){
        var s1 Student = Student{19}
        var s2 Stu = Stu{19}
        s1 = Student(s2)
        fmt.Println(s1)
        fmt.Println(s2)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

方法的引入

  • 方法是作用在指定的数据类型上、和指定的数据类型绑定,因此自定义类型,都可以有方法,而不仅仅是struct
  • 方法的声明和调用格式
//声明:
type A struct {
    Num int
}
func (a A) test() {
    fmt.Println(a.Num)
}
//调用:
var a A
a.test()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意事项

  • 在方法的调用中,我们可以在外部传参是传递值或值对应的地址,来控制方法内是否能修改到外部的值;
  • Golang中的方法作用在指定的数据类型上的,和指定的数据类型绑定,因此自定义类型,都可以有方法,而不仅仅是struct,比如int , float32等都可以有方法;
package main
import "fmt"
type integer int
func (i integer) print(){
        i = 30
        fmt.Println("i = ",i)
}
func (i *integer) change(){
        *i = 30
        fmt.Println("i = ",*i)
}
func main(){
        var i integer = 20
        i.print()
        i.change()
        fmt.Println(i)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。

方法和函数的区别

  • 绑定指定类型:
    方法:需要绑定指定数据类型
    函数:不需要绑定数据类型
  • 调用方式不一样:
    函数的调用方式: 函数名(实参列表)
    方法的调用方式:变量.方法名(实参列表)

创建结构体实例时指定字段值

【1】方式1:按照顺序赋值操作
缺点:必须按照顺序有局限性
【2】方式2:按照指定类型
【3】方式3:想要返回结构体的指针类型

type Student struct {
	Name string
	Age  int
}

func main() {
	// 方式一
	var s1 Student = Student{"小李", 19}
	fmt.Println(s1)
	// 方式二
	var s2 Student = Student{Name: "Terry", Age: 18}
	fmt.Println(s2)
	// 方式三
	var s3 *Student = &Student{Name: "Terry", Age: 18}
	fmt.Println(*s3)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

跨包创建结构体实例

  • 直接暴露结构体,只需要在其他包将结构体首字母大小即可
  • 当结构体首字母小写时可以使用暴露函数的方法来返回结构体
    在这里插入图片描述

封装

在这里插入图片描述
在这里插入图片描述

继承

  • 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体。
  • 通过嵌入匿名结构体来进行继承
  • 继承的优点,提高代码的复用性、扩展性
package main
import (
        "fmt"
)
//定义动物结构体:
type Animal struct{
        Age int
        Weight float32
}
//给Animal绑定方法:喊叫:
func (an *Animal) Shout(){
        fmt.Println("我可以大声喊叫")
}
给Animal绑定方法:自我展示:
func (an *Animal) ShowInfo(){
        fmt.Printf("动物的年龄是:%v,动物的体重是:%v",an.Age,an.Weight)
}
//定义结构体:Cat
type Cat struct{
        //为了复用性,体现继承思维,嵌入匿名结构体:——》将Animal中的字段和方法都达到复用
        Animal
}
//对Cat绑定特有的方法:
func (c *Cat) scratch(){
        fmt.Println("我是小猫,我可以挠人")
}
func main(){
        //创建Cat结构体示例:
        cat := &Cat{}
        cat.Age = 3
        cat.Weight = 10.6
        cat.Shout()
        cat.ShowInfo()
        cat.scratch()
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

注意事项

  • 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
package main
import (
        "fmt"
)
type A struct{
        a int
        b string
}
type B struct{
        c int
        d string
        a int
}
type C struct{
        A
        B
}
func main(){
        //构建C结构体实例:
        c := C{A{10,"aaa"},B{20,"ccc",50}}
        fmt.Println(c.b)
        fmt.Println(c.d)
        fmt.Println(c.A.a)
        fmt.Println(c.B.a)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 结构体的匿名字段可以是基本数据类型、指针、结构体类型

接口

  • 接口中可以定义一组方法,但不需要实现,不需要方法体。并且接口中不能包含任何变量。到某个自定义类型要使用的时候(实现接口的时候),再根据具体情况把这些方法具体实现出来。
  • Golang中实现接口是基于方法的,不是基于接口的,实现接口要实现所有的方法才是实现;
package main

import "fmt"

type SayHello interface {
	sayHello()
}

type Chinese struct {
}

func (person Chinese) sayHello() {
	fmt.Println("你好")
}

type American struct {
}

func (person American) sayHello() {
	fmt.Println("hi")
}

func greet(s SayHello) {
	s.sayHello()
}
func main() {
	c := Chinese{}
	a := American{}
	greet(a)
	greet(c)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

多态

可以按照统一的接口来调用不同的实现
例如上述案例,greet方法的参数s就是个多态参数;

断言

判断是否是该类型的变量: value, ok := element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型

func greet(s SayHello) {
	s.sayHello()
	//断言:
	ch, flag := s.(Chinese) //看s是否能转成Chinese类型并且赋给ch变量
	if flag == true {
		ch.niuYangGe()
	} else {
		fmt.Println("美国人不会扭秧歌")
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Type Switch
Type Switch 是 Go 语言中一种特殊的 switch 语句,它比较的是类型而不是具体的值。它判断某个接口变量的类型;

func greet(s SayHello) {
	s.sayHello()
	switch s.(type) { //type属于go中的一个关键字,固定写法
	case Chinese:
		ch := s.(Chinese)
		ch.niuYangGe()
	case American:
		am := s.(American)
		am.disco()
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

十、文件操作

打开关闭文件

os包下的File结构体封装了对文件的操作
File结构体—打开文件和关闭文件:
(1)打开文件,用于读取,传入一个字符串(文件的路径),返回的是文件的指针,和是否打开成功
在这里插入图片描述
在这里插入图片描述
OpenFile可配置文件打开模式,(可以利用"|"符号进行组合)
在这里插入图片描述

(2)关闭文件:(方法)
在这里插入图片描述

package main

import (
	"fmt"
	"os"
)

func main() {
	//打开文件:
	file, err := os.Open("C:/Users/admin/Desktop/test.txt")
	if err != nil { //出错
		fmt.Println("文件打开出错,对应错误为:", err)
	}
	fmt.Printf("文件=%v", file) //文件指针
	//关闭文件:
	err2 := file.Close()
	if err2 != nil {
		fmt.Println("关闭失败")
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

读取文件(一次读完)

在这里插入图片描述

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {
	//备注:在下面的程序中不需要进行 Open\Close操作,
	//因为文件的打开和关闭操作被封装在ReadFile函数内部了
	//读取文件:
	content, err := ioutil.ReadFile("C:/Users/admin/Desktop/test.txt") //返回内容为:[]byte,err
	if err != nil { //读取有误
		fmt.Println("读取出错,错误为:", err)
	}
	//如果读取成功,将内容显示在终端即可:
	fmt.Printf("%v", content) //因为它是一个字节数组,所以需要转为string
	fmt.Printf("%v", string(content))
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

读取文件(带缓冲区)

package main

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

func main() {
	//打开文件:
	file, err := os.Open("C:/Users/admin/Desktop/test.txt")
	if err != nil { //打开失败
		fmt.Println("文件打开失败,err=", err)
	}
	//当函数退出时,让file关闭,防止内存泄露:
	defer file.Close()
	//创建一个流:
	reader := bufio.NewReader(file)
	//读取操作:
	for {
		str, err := reader.ReadString('\n') //读取到一个换行就结束 如果读取完毕err会返回EOF
		if err == io.EOF {                  //io.EOF 表示已经读取到文件的结尾
			break
		}
		//如果没有读取到文件结尾的话,就正常输出文件内容即可:
		fmt.Println(str)
	}
	//结束:
	fmt.Println("文件读取成功,并且全部读取完毕")
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

写入文件

package main

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

func main() {
	//写入文件操作:
	//打开文件:
	file, err := os.OpenFile("C:/Users/admin/Desktop/test.txt", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
	if err != nil { //文件打开失败
		fmt.Println("打开文件失败", err)
		return
	}
	//及时将文件关闭:
	defer file.Close()
	//写入文件操作:---》IO流---》缓冲输出流(带缓冲区)
	writer := bufio.NewWriter(file)
	for i := 0; i < 10; i++ {
		writer.WriteString("你好 马士兵\n")
	}
	//流带缓冲区,刷新数据--->真正写入文件中:
	writer.Flush()
	s := os.FileMode(0666).String()
	fmt.Println(s)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

这里0666代表的是权限:
四位数代表意思: 特殊权限用户,用户,组用户,其它用户
每位值代表意思:读是4,写是2,执行是1
0666 就是用户,组用户,其它用户都只有读写权限;

文件复制

package main
import(
        "fmt"
        "io/ioutil"
)
func main(){
        //定义源文件:
        file1Path := "C:/Users/admin/Desktop/test.txt"
        //定义目标文件:
        file2Path := "C:/Users/admin/Desktop/test2.txt"
        //对文件进行读取:
        content,err := ioutil.ReadFile(file1Path)
        if err != nil {
                fmt.Println("读取有问题!")
                return
        }
        //写出文件:
        err = ioutil.WriteFile(file2Path,content,0666)
        if err != nil {
                fmt.Println("写出失败!")
        }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

十一、协程

程序

是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。

进程(process)

是程序的一次执行过程。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。 (进程是动态的)是一个动的过程 ,进程的生命周期 : 有它自身的产生、存在和消亡的过程 。

线程(thread)

进程可进一步细化为线程, 是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。

协程(goroutine)

概念

又称为微线程,纤程,协程是一种用户态的轻量级线程
作用:在执行A函数的时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),注意这一切换过程并不是函数调用(没有调用语句),过程很像多线程,然而协程中只有一个线程在执行(协程的本质是个单个线程)

对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就将寄存器上下文和栈保存到某个其他地方,然后切换到另外一个任务去计算。在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态(注意:线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级)

package main

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

func test() {
	for i := 1; i <= 10; i++ {
		fmt.Println("hello golang + " + strconv.Itoa(i))
		//阻塞一秒:
		time.Sleep(time.Second)
	}
}
func main() { //主线程
	go test() //开启一个协程
	for i := 1; i <= 10; i++ {
		fmt.Println("hello msb + " + strconv.Itoa(i))
		//阻塞一秒:
		time.Sleep(time.Second)
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在这里插入图片描述
主死从随

  • 如果主线程退出了,则协程即使还没有执行完毕,也会退出
  • 当然协程也可以在主线程没有退出前,就自己结束了,比如完成了自己的任务

使用WaitGroup控制协程退出

WaitGroup的作用:
WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。—》解决主线程在子协程结束后自动结束;
在这里插入图片描述

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup //只定义无需赋值
func main() {
	//启动五个协程
	for i := 1; i <= 5; i++ {
		wg.Add(1) //协程开始的时候加1操作
		go func(n int) {
			fmt.Println(n)
			wg.Done() //协程执行完成减1
		}(i)
	}
	//主线程一直在阻塞,什么时候wg减为0了,就停止
	wg.Wait()
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 如果知道有几个协程那么可以先add协程数量
  • Done操作可以配合defer关键字来使用,使其在协程执行最后再减1

互斥锁

当多个协程同时操作统一数据时,会出现更新错误的情况

package main
import(
        "fmt"
        "sync"
)
//定义一个变量:
var totalNum int
var wg sync.WaitGroup //只定义无需赋值
func add(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                totalNum = totalNum + 1
        }
}
func sub(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                totalNum = totalNum - 1
        }
}
func main(){
        wg.Add(2)
        //启动协程
        go add()
        go sub()
        wg.Wait()
        fmt.Println(totalNum)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

按理说结果应该是0,但是结果却天差地别,是因为同时更新会导致数据有误。
加入互斥锁,确保:一个协程在执行逻辑的时候另外的协程不执行

package main
import(
        "fmt"
        "sync"
)
//定义一个变量:
var totalNum int
var wg sync.WaitGroup //只定义无需赋值
//加入互斥锁:
var lock sync.Mutex
func add(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                //加锁
                lock.Lock()
                totalNum = totalNum + 1
                //解锁:
                lock.Unlock()
        }
}
func sub(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                //加锁
                lock.Lock()
                totalNum = totalNum - 1
                //解锁:
                lock.Unlock()
        }
}
func main(){
        wg.Add(2)
        //启动协程
        go add()
        go sub()
        wg.Wait()
        fmt.Println(totalNum)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

读写锁

golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁);

  • 互斥锁

其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别
----性能、效率相对来说比较低

  • 读写锁

RWMutex是一个读写锁,其经常用于读次数远远多于写次数的场景.

—在读的时候,数据之间不产生影响,写和读之间才会产生影响

package main

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

var wg sync.WaitGroup //只定义无需赋值
// 加入读写锁:
var lock sync.RWMutex

func read() {
	defer wg.Done()
	lock.RLock() //如果只是读数据,那么这个锁不产生影响,但是读写同时发生的时候,就会有影响
	fmt.Println("开始读取数据")
	time.Sleep(time.Second)
	fmt.Println("读取数据成功")
	lock.RUnlock()
}
func write() {
	defer wg.Done()
	lock.Lock()
	fmt.Println("开始修改数据")
	time.Sleep(time.Second)
	fmt.Println("修改数据成功")
	lock.Unlock()
}
func main() {
	wg.Add(6)
	//启动协程 ---> 场合:读多写少
	for i := 0; i < 5; i++ {
		go read()
	}
	go write()
	wg.Wait()
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

管道(channel)

  • 管道本质就是一个数据结构-队列
  • 数据是先进先出
  • 自身线程安全,多协程访问时,不需要加锁,channel本身就是线程安全的
  • 管道有类型的,一个string的管道只能存放string类型数据
    在这里插入图片描述

管道的定义

var  变量名  chan  数据类型
PS1:chan管道关键字
PS2:数据类型指的是管道的类型,里面放入数据的类型,管道是有类型的,int类型的管道只能写入整数int
PS3:管道是引用类型,必须初始化才能写入数据,即make后才能使用
  • 1
  • 2
  • 3
  • 4
package main

import (
	"fmt"
)

func main() {
	//定义管道 、 声明管道 ---> 定义一个int类型的管道
	var intChan chan int
	//通过make初始化:管道可以存放3个int类型的数据
	intChan = make(chan int, 3)
	//证明管道是引用类型:
	fmt.Printf("intChan的值:%v\n", intChan) // 0xc000112080
	//向管道存放数据:
	intChan <- 10
	num := 20
	intChan <- num
	intChan <- 40
	//注意:不能存放大于容量的数据:
	//intChan<- 80
	//在管道中读取数据:
	num1 := <-intChan
	num2 := <-intChan
	num3 := <-intChan
	fmt.Println(num1)
	fmt.Println(num2)
	fmt.Println(num3)
	//注意:在没有使用协程的情况下,如果管道的数据已经全部取出,那么再取就会报错:
	// num4 := <-intChan
	// fmt.Println(num4)
	//输出管道的长度:
	fmt.Printf("管道的实际长度:%v,管道的容量是:%v", len(intChan), cap(intChan))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

管道的关闭

使用内置函数close可以关闭管道,当管道关闭后,就不能再向管道写数据了,但是仍然可以从该管道读取数据。

close(intChan)
  • 1

管道的遍历

管道支持for-range的方式进行遍历,请注意两个细节

  • 在遍历时,如果管道没有关闭,则会出现deadLock的错误
  • 在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
package main
import(
        "fmt"
)
func main(){
        //定义管道 、 声明管道
        var intChan chan int
        //通过make初始化:管道可以存放3个int类型的数据
        intChan = make(chan int,100)
        for i := 0;i < 100;i++ {
                intChan<- i
        }
        //在遍历前,如果没有关闭管道,就会出现deadlock的错误
        //所以我们在遍历前要进行管道的关闭
        close(intChan)
        //遍历:for-range
        for v := range intChan {
                fmt.Println("value = ",v)
        }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

协程和管道协同工作

在这里插入图片描述

package main

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

var wg sync.WaitGroup //只定义无需赋值
// 写:
func writeData(intChan chan int) {
	defer wg.Done()
	for i := 1; i <= 10; i++ {
		intChan <- i
		fmt.Println("写入的数据为:", i)
		time.Sleep(time.Second)
	}
	//没关闭前读线程已经开始工作
	//如果没有关闭,编译器会一直等待下一个写入的内容,导致死锁
	close(intChan)
}

// 读:
func readData(intChan chan int) {
	defer wg.Done()
	//遍历:
	for v := range intChan {
		fmt.Println("读取的数据为:", v)
		time.Sleep(time.Second)
	}
}
func main() { //主线程
	//写协程和读协程共同操作同一个管道-》定义管道:
	intChan := make(chan int, 50)
	wg.Add(2)
	//开启读和写的协程:
	go writeData(intChan)
	go readData(intChan)
	//主线程一直在阻塞,什么时候wg减为0了,就停止
	wg.Wait()
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

声明只读或只写管道

//默认情况下,管道是双向的--》可读可写:
var intChan1 chan int
//声明为只写:
var intChan2 chan <- int  // 管道具备<- 只写性质
//声明为只读:
var intChan3 <- chan int  // 管道具备<- 只读性质 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/790193
推荐阅读
相关标签
  

闽ICP备14008679号