您的位置:首页 > 娱乐 > 八卦 > 潍坊网站建设推荐_ui网页设计是什么_电商培训机构哪家强_潍坊网站关键词推广

潍坊网站建设推荐_ui网页设计是什么_电商培训机构哪家强_潍坊网站关键词推广

2025/4/10 23:32:31 来源:https://blog.csdn.net/theVicTory/article/details/145483276  浏览:    关键词:潍坊网站建设推荐_ui网页设计是什么_电商培训机构哪家强_潍坊网站关键词推广
潍坊网站建设推荐_ui网页设计是什么_电商培训机构哪家强_潍坊网站关键词推广

随着计算机硬件的发展,编程语言无法充分利用CPU多核特性,并且缺乏一个简洁高效的语言,因此2007 年,谷歌工程师 Rob Pike, Ken Thompson 和 Robert Griesemer 设计了Go语言 ,它有以下特点:

  1. 静态类型+编译型语言,运行速度快
  2. 脚本化编程语言,易于编写
  3. 从 C 语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型,调用参数传值,指针等,也保留了和 C 语言一样的编译执行方式及弱化的指针
  4. 引入包的概念,用于组织程序结构,Go 语言的一个文件都要归属于一个包,而不能单独存在
  5. 垃圾回收机制,内存自动回收,不需开发人员管理
  6. 从语言层面支持并发
  7. 吸收了管道通信机制,形成 Go 语言特有的管道 channel 通过管道 channel , 可以实现不同的 goroute之间的相互通信。
  8. 函数可以返回多个值
  9. 新的创新:比如切片 slice、延时执行 defer

Quick Start

如下所示为一个hello.go文件

// 每个文件都必须归属于一个包
package main// 引入一个包 fmt
import "fmt"// 主程序入口
func main(){// 调用 fmt 包的函数 Println 输出fmt.Println("hello world!")
}

通过命令go build hello.go可以对源代码进行编译,生成可执行文件然后允许;也可以直接go run hello.go,会自动编译运行代码

Go 语言严格区分大小写;
编译器是按行进行编译的,一行只能由一条语句,且结尾不需要用分号;
go 语言定义的变量或者 import 的包如果没有使用到,代码不能编译通过;
go函数体的{需要放在行尾,下面写法会报错

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

1 基本数据类型

变量

变量声明

有三种方式:

	//第一种:指定变量类型,Go中声明后若不赋值,编译器会赋默认值例如int 的默认值是0 var i int fmt.Println("i=", i)//第二种:根据值自行判定变量类型(类型推导)var num  = 10.11fmt.Println("num=", num)//第三种:省略varname := "tom"fmt.Println("name=", name)//一次性声明多个变量的方式2 var n1, name , n3 = 100, "tom", 888fmt.Println("n1=",n1, "name=", name, "n3=", n3)//一次性声明多个变量的方式3, 同样可以使用类型推导n1, name , n3 := 100, "tom~", 888fmt.Println("n1=",n1, "name=", name, "n3=", n3)// 一次性声明全局变量
var (n3 = 300n4 = 900name2 = "mary"
)

变量类型

和其它语言一样,可以理解为基本和复杂数据类型

基本数据类型:

  • 整数类型(Integer Types):Go语言提供不同大小的整数类型,如int、int8(1字节)、int16、int32和int64。无符号整数类型有uint、uint8、uint16、uint32和uint64。整数类型的大小取决于计算机的架构,例如32位或64位。
  • 浮点数类型(Floating-Point Types):Go语言提供float32和float64两种浮点数类型,分别对应单精度和双精度浮点数。
  • 复数类型(Complex Types):Go语言提供complex64和complex128两种复数类型,分别对应由两个浮点数构成的复数。
  • 布尔类型(Boolean Type):布尔类型用于表示真(true)和假(false)的值,用于条件判断和逻辑运算。
  • 字符类型(Rune Type):用byte来保存单个字母字符
  • 字符串类型(String Type):字符串类型表示一系列字符。字符串是不可变的,可以使用双引号"或反引号`来定义。

复杂数据类型:

  • 数组类型(Array Types):数组是具有固定大小的同类型元素集合。声明数组时需要指定元素类型和大小。
  • 切片类型(Slice Types):切片是对数组的一层封装,是动态长度的可变序列。切片不保存元素,只是引用底层数组的一部分。
  • 映射类型(Map Types):映射是键值对的无序集合,用于存储和检索数据。键和值可以是任意类型,但键必须是可比较的。
  • 结构体类型(Struct Types):结构体是一种用户定义的复合数据类型,可以包含不同类型的字段,每个字段有一个名字和类型。
  • 接口类型(Interface Types):接口是一种抽象类型,用于定义一组方法。类型实现了接口的方法集合即为实现了该接口。
  • 函数类型(Function Types):函数类型表示函数的签名,包括参数和返回值类型。函数可以作为参数传递和返回。
  • 通道类型(Channel Types):通道是用于在协程之间进行通信和同步的一种机制。通道有发送和接收操作。
  • 指针类型(Pointer Types):指针类型表示变量的内存地址。通过指针可以直接访问和修改变量的值。
var n1 = 100 
// 查看某个变量的数据类型
fmt.Printf("n1 的 类型 %T \n", n1)// 查看某个变量的占用字节大小和数据类型
var n2 int64 = 10
fmt.Printf("n2 的 类型 %T  n2占用的字节数是 %d ", n2, unsafe.Sizeof(n2))//Golang 的浮点型默认声明为float64 类型
var num5 = 1.1
fmt.Printf("num5的数据类型是 %T \n", num5)

字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。对于传统的字符串是由字符组成的,而 Go 的字符串不同,它是由字节组成的.
Go字符采用UTF-8编码,如果字符在 ASCII 表的,比如[0-1, a-z,A-Z…]直接可以保存到 byte;如果字符对应码值大于 255,可以考虑使用 int 类型保存

var c1 byte = 'a'
//当我们直接输出byte值,就是输出了的对应的字符的码值
fmt.Println("c1=", c1)
//如果我们希望输出对应字符,需要使用格式化输出
fmt.Printf("c1=%c\n", c2)

GO中的String字符串一旦定义不支持修改,

// 可以使用双引号、反引号包裹字符串
str3 := ` package mainimport ("fmt""unsafe")`// 可以使用 + 拼接字符串,注意换行时 + 在行尾
str4 := "hello " + "world" + "hello " + "world" + "hello " + "world" + "hello " + "world" + "hello " + "world" + "hello " + "world"

Go 在不同类型的变量之间赋值时需要显式转换,转换是把i值赋值给n,i本身数据类型并不会变

	var i int32 = 100var n float32 = float32(i)fmt.Printf("i type is %T\n", i) // int32var n1 int32 = 12var n2 int64var n3 int8// go不会进行自动类型转换,下面n2是int64,n1是int32,会编译报错n2 = n1 + 20  

可以使用fmt.Sprintf函数或者strconv包来实现基本类型转字符串

import ("fmt"_ "unsafe""strconv"
)
func main() {var num1 int = 99var num2 float64 = 23.456var b bool = truevar myChar byte = 'h'var str string //空的str// fmt.Sprintf方法str = fmt.Sprintf("%d", num1)str = fmt.Sprintf("%f", num2)str = fmt.Sprintf("%t", b)str = fmt.Sprintf("%c", myChar)//第二种方式 strconv 函数 var num3 int = 99var num4 float64 = 23.456var b2 bool = truestr = strconv.FormatInt(int64(num3), 10)str = strconv.Itoa(num3)// 'f' 格式 10:表示小数位保留10位 64 :表示这个小数是float64str = strconv.FormatFloat(num4, 'f', 10, 64)str = strconv.FormatBool(b2)
}

使用strconv包实现str向其他数据类型的转换

	var str string = "true"var b bool// 函数会返回两个值 (value bool, err error)略b , _ = strconv.ParseBool(str)var str2 string = "1234590"var n1 int64var n2 intn1, _ = strconv.ParseInt(str2, 10, 64)n2 = int(n1)var str3 string = "123.456"var f1 float64f1, _ = strconv.ParseFloat(str3, 64)// 如果转换失败,会返回该类型变量的默认值var str4 string = "hello"var n3 int64 = 11n3, _ = strconv.ParseInt(str4, 10, 64)fmt.Printf("n3 type %T n3=%v\n", n3, n3)

Go中指针类型的用法和C语言一样:

  1. 基本数据类型,变量存的就是值,也叫值类型
  2. 指针类型:指针变量存的是一个地址,这个地址指向的空间存的才是值,var ptr *int = &num
  3. &获取变量的地址,比如: var num int, 获取 num 的地址:&num
  4. *获取指针类型所指向的值,var ptr int, 使用ptr 获取 ptr 指向的值
	var i int = 10fmt.Println("i的地址=", &i)var ptr *int = &i fmt.Printf("ptr 本身=%v\n", ptr)fmt.Printf("ptr 的地址=%v", &ptr) fmt.Printf("ptr 指向的值=%v", *ptr)

值类型:变量直接存储值,内存通常在栈中分配,包括基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,指针、slice 切片、map、管道 chan、interface 等都是引用类型

Go语言中通常使用fmt包中的scan函数获取标准输入

var name stringvar age bytevar sal float32var isPass boolfmt.Println("请输入姓名 ")// 1 使用Scanln获取一行输入fmt.Scanln(&name)// 2:fmt.Scanf,可以按指定的格式输入fmt.Println("请输入你的姓名,年龄,薪水, 是否通过考试, 使用空格隔开")fmt.Scanf("%s %d %f %t", &name, &age, &sal, &isPass)fmt.Printf("名字是 %v \n年龄是 %v \n 薪水是 %v \n 是否通过考试 %v \n", name, age, sal, isPass)

数组与切片

数组是多个相同类型数据的组合,一旦声明/定义了,其长度是固定的,不能动态变化。如下所示为数组的定义和使用

// 几种初始化数组的方式
var numArr01 [3]int = [3]int{1, 2, 3}	// var 数组名 [数组大小]数据类型
var numArr02 = [3]int{5, 6, 7}	// 省略变量类型
var numArr03 = [...]int{8, 9, 10}	// 自动推导元素个数
var score = [...]int{1: 800, 0: 900, 2:999}	// 定义各个元素
strArr05 := [...]string{1: "tom", 0: "jack", 2:"mary"}	// 类型推导// 遍历各个元素
for i := 0; i < len(score); i++ {fmt.Printf("请输入第%d个元素的值\n", i+1)fmt.Scanln(&score[i])
}
score[0] = 200
//变量数组打印
for i := 0; i < len(score); i++ {fmt.Printf("score[%d]=%v\n", i, score[i])
}for i, v := range score {	// 也可以使用for range的方式遍历数组fmt.Printf("i=%v v=%v\n", i , v)
}// 定义/声明二维数组
var arr [4][6]int
// 赋初值
arr[1][2] = 1
arr[2][3] = 3
arr3  := [2][3]int{{1,2,3}, {4,5,6}}

数组在内存中的分布是一整块地址空间,地址可以通过数组名来获取 &score[i],第一个元素的地址,就是数组的首地址,各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8个字节

数组创建后会为每个元素赋默认值

Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝,数组间不会相互影响,如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)

func test02(arr *[3]int) {fmt.Printf("arr指针的地址=%p", &arr)(*arr)[0] = 88 //!!
} 

长度是数组类型的一部分,不同长度的数组可以认为是不同类型,因此在参数传递时需要注意。

切片Slice是数组的一个引用,因此切片是引用类型。切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度都一样。不同的是切片的长度是可以变化的,因此切片是一个可以动态变化数组

切片有两种定义方式

  1. 第一种是通过截取数组的一部分成为切片,由于切片是引用,其地址指向截取数组的开头;这时切片和数组其实指向同一块内存地址,对切片进行修改后,数组内容也会生效。
  2. 通过`var 切片名 []type = make([]type, len, [cap])`手动创建一个新的切片,type 就是数据类型, len 切片长度, cap 可选,指定切片容量。也可以省略make直接定义一个slice,其实现原理相同
    

可以把切片看作一个结构体,包含指向首地址的指针、长度len和cap容量。容量是分配的地址空间指总共可容纳元素个数,比如一个slice虽然只有三个元素,但其空间容量为5;在slice长度动态增加超过容量时,会按照一定的规则扩容。slice扩容时会分配一片新的地址空间,然后把原有的值拷贝过去。

var arr [5]int = [...]int{10, 20, 30, 40, 50}
slice := arr[1:4]	// 如果省略为arr[:4]代表从头开始,arr[1:]代表直到结尾//使用常规的for循环遍历切片
for i := 0; i < len(slice); i++ {fmt.Printf("slice[%v]=%v ", i, slice[i])
}//使用for--range 方式遍历切片
for i, v := range slice {fmt.Printf("i=%v v=%v \n", i, v)
}slice2 := slice[1:2] //  slice [ 20, 30, 40]    [30]
slice2[0] = 100  // 因为arr , slice 和slice2 指向的数据空间是同一个,因此slice2[0]=100,其它的都变化//用append内置函数,可以对切片进行动态追加
slice3 = append(slice, 400, 500, 600)//通过append将切片slice3追加给slice3
slice3 = append(slice3, slice3...) // 100, 200, 300,400, 500, 600 100, 200, 300,400, 500, 600//切片使用copy内置函数完成拷贝,举例说明
var slice4 []int = []int{1, 2, 3, 4, 5}
var slice5 = make([]int, 10)
copy(slice5, slice4)

由于slice时引用,因此参数传递时是进行引用传递

func test(slice []int) {slice[0] = 100  //这里修改slice[0],会改变实参
}func main() {var slice = []int {1, 2, 3, 4}	// 省略make直接定义,有长度声明为数组,没有为slicefmt.Println("slice=", slice) // [1,2,3,4]test(slice)fmt.Println("slice=", slice) // [100, 2, 3, 4]
}

string 底层是一个 byte 数组,因此 string 也可以进行切片处理。由于string是不可变的,因此不能通过 str[0] = ‘z’ 方式来修改字符串,可以先将其转为slice,然后进行修改

	str := "hello@你好"slice := str[6:] 	// 使用切片获取到子串arr1 := []byte(str) arr1[0] = 'z'str = string(arr1)// 如果需要处理中文,转为rune按字符处理切片arr1 := []rune(str) arr1[0] = '北'str = string(arr1)

Map

Go中集合map的 key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组。需要注意的是声明不会分配内存,初始化需要 make 分配内存后才能赋值和使用

var a map[string]string
a = make(map[string]string, 10)	// 给map分配数据空间
a["no1"] = "value1"cities := make(map[string]string)	// 直接申请空间heroes := map[string]string{	// 定义并初始化"hero1" : "宋江",
}
heroes["hero2"] = "林冲"

map的增删改查操作

	cities := make(map[string]string)cities["no1"] = "北京"	// 新增cities["no1"] = "上海~" 	// 修改fmt.Println(cities)delete(cities, "no1")		// 删除元素cities = make(map[string]string)		// 如果希望一次性删除所有,make 一个新的,让原来的成为垃圾,被 gc 回收fmt.Println(cities)val, ok := cities["no2"]	// 查找if ok {fmt.Printf("有no1 key 值为%v\n", val)} else {fmt.Printf("没有no1 key\n")}

map可以进行嵌套定义

	studentMap := make(map[string]map[string]string)studentMap["stu01"] =  make(map[string]string, 3)		// map的元素还是一个mapstudentMap["stu01"]["name"] = "tom"studentMap["stu01"]["sex"] = "男"studentMap["stu02"] =  make(map[string]string, 3) studentMap["stu02"]["name"] = "mary"studentMap["stu02"]["sex"] = "女"for k1, v1 := range studentMap {		// 使用for range进行遍历fmt.Println("k1=", k1)for k2, v2 := range v1 {fmt.Printf("\t k2=%v v2=%v\n", k2, v2)}fmt.Println()}

切片的数据类型如果是 map,即map 切片,这样使用则 map 个数就可以动态变化了

	var monsters []map[string]string		//1. 声明一个map切片monsters = make([]map[string]string, 2)//2. 增加第一个妖怪的信息if monsters[0] == nil {monsters[0] = make(map[string]string, 2)monsters[0]["name"] = "老张"monsters[0]["age"] = "50"}newMonster := map[string]string{"name" : "小李","age" : "20",}monsters = append(monsters, newMonster)		// append函数动态的增加

注意

  • map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map
  • map 的容量达到后,再想 map 增加元素,会自动扩容

2 流程控制

标识符

Golang 对各种变量、方法、函数等命名时使用的字符序列称为标识符。其命名规则如下:

  • 由 26 个英文字母大小写,0-9 ,_ 组成
  • 数字不可以开头
  • Golang 中严格区分大小写。
  • 标识符不能包含空格
  • 下划线"_"本身在 Go 中是一个特殊的标识符,称为空标识符。可以代表任何其它的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用
  • 不能以系统保留关键字作为标识符
  • 变量名、函数名、常量名采用驼峰法命名
  • 如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果小写,则只能在本包中使用。Go中没有可见性关键字,首字母大写是公开的,首字母小写是私有的

运算符

算术运算符:包含:+, - , * , / , %, ++, – ,需要注意的是Go中的自增、自减运算符没有返回值,因此不能将结果进行赋值或者进行运算

var i int = 1;
var j = i++	 // 错误,不能赋值if i++ > 10 {	// 错误,不能参与比较fmt.Println("ok")
}

关系运算符:==、!=、>、<、>=、<=,用于比较数值,结果为bool值

逻辑运算符:用于连接逻辑表达式进行逻辑运算,与&&、或||、非!

位运算符:对二进制数据进行的运算(如加、减、乘、除)被称为位运算,即对二进制数的每一位进行操作的运算

运算符号规则
&两个位都为1时,结果才为1
两个位都为0时,结果才为0
异或^两个位相同为0,相异为1
取反~0变1,1变0
左移<<各二进位全部左移若干位,高位丢弃,低位补0
右移>>各二进位全部右移若干位,高位补0或符号位补齐

赋值运算符就是将某个运算后的值,赋给指定的变量,包括=、+=、-=、*=、/=、%=,还有位运算相关的左移赋值<<=、右移赋值>>=、按位与赋值&=、 按位异或赋值^=、按位或赋值|=

分支语句

顺序分支,从上向下执行
单、双、多分支,通过if进行分支判断,需要注意的是:

  • golang支持在if中,直接定义一个变量
  • 单分支不能省略大括号
  • else不要换行
if age := 20; age > 18 {fmt.Println("已成年!")
} else {fmt.Println("未成年!")
}//多分支判断
var score int
fmt.Println("请输入成绩:")
fmt.Scanln(&score)if score == 100 {fmt.Println("奖励一辆BMW")
} else if score > 80 && score <= 99 {fmt.Println("奖励一台iphone7plus")
} else {fmt.Println("什么都不奖励")
}

Switch的使用,注意

  • go中不需要break
  • switch\case后面可以跟常量、变量、表达式
  • case后面可以同时匹配多个条件
  • 通过关键字fallthrough实现穿透,执行下一个条件
	var key byte fmt.Println("请输入一个字符 a,b,c,d,e,f,g")fmt.Scanf("%c", &key)switch test(key)+1 { case 'a':fmt.Println("周一, 穿新衣")fallthrough	// 执行到这里会向下穿透一层,执行下面的一个casecase 'b':fmt.Println("周二,当小二")case 'c','d':fmt.Println("周三,爬雪山")//...default:fmt.Println("输入有误...")}

循环

golang中通过for来实现循环,有如下几种灵活的形式

	for i := 1; i <= 10; i++ {fmt.Println("你好,尚硅谷", i)}//for循环的第二种写法j := 1 //循环变量初始化for j <= 10 { //循环条件fmt.Println("你好,尚硅谷~", j)j++ //循环变量迭代}//for循环的第三种写法, 通常会配合break跳出循环k := 1for {  // 这里也等价 for ; ; { if k <= 10 {fmt.Println("ok~~", k)} else {break}k++}// 通过for range遍历字符串和数组str = "abc~ok"for index, val := range str {	// 遍历获取各个位置的字符fmt.Printf("index=%d, val=%c \n", index, val)}

Go中的break默认跳出最近的一层循环;break和continue可以使用一个label指定跳出多层循环

lable2: 
for i := 0; i < 4; i++ {
for j := 0; j < 10; j++ {if j == 2 {break lable2 // 跳转到最外层}fmt.Println("j=", j) 
}here:
for i:=0; i<2; i++ {for j:=1; j<4; j++ {if j==2 {continue here}fmt.Println("i=",i,"j=",j)
}

Golang中也支持使用goto跳转到代码的指定位置,但是这样会造成程序混乱可读性差,一般不支持使用。

3 函数

3.1 基本定义和使用

Go中函数的基本格式为

func 函数名 (形参列表)(返回值列表){执行语句return 返回值
}

如下所示定义一个四则运算的函数,注意函数首字母需要大写才能被其他包使用

package utils 
import ("fmt"
)func Cal(n1 float64, n2 float64, operator byte) float64 {var res float64switch operator {case '+':res = n1 + n2case '-':res = n1 - n2case '*':res = n1 * n2case '/':res = n1 / n2default:fmt.Println("操作符号错误...")}return res
}

Go中也是用包对项目文件进行管理,通过package定义包名,每个文件都必须属于一个包下面;通过包还可以对函数和变量的作用域进行控制。如下所示通过引入上面包中定义的函数并进行调用,路径从 环境变量定义的$GOPATH 的 src 下开始;引入的时候可以为包取别名

package main
import ("fmt"util "go_code/chapter06/fundemo01/utils"
)func main() {var n1 float64 = 1.2var n2 float64 = 2.3var operator byte = '+'result := util.Cal(n1, n2 , operator) fmt.Println("result~=", result)
}

如果要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main 。如果是写一个库 ,包名可以自定义,在编译main文件时相关的库函数会被编译为以.a结尾的库文件

Go语言中的基本数据类型一般分配在内存中的栈区,引用数据类型分配在堆区。在调用一个函数时,会给该函数分配一个新的堆栈,不同的堆栈之间彼此不可见,因此函数变量彼此隔离。当一个函数调用完毕(执行完毕)后,程序会销毁这个函数对应的栈空间。因此在函数调用传递形参时需要注意传递的值仅在当前函数中可见。

基本数据类型和数组默认都是值传递的,即进行值拷贝,在函数内修改,不会影响到原来的值。如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用

Go 函数不支持函数重载

在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量。函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用

func getSum(n1 int, n2 int) int {return n1 + n2
}func myFun(funvar func(int, int) int, num1 int, num2 int ) int {	// 入参是一个函数类型return funvar(num1, num2)
}func main() {res2 := myFun(getSum, 50, 60)	// 将函数getSum作为参数传递fmt.Println("res2=", res2)
}

为了简化数据类型定义,Go 支持自定义数据类型

// 给int取了别名后, go认为myInt和int是两个类型type myInt int 

go支持对返回参数进行命名

func getSumAndSub(n1 int, n2 int) (sum int, sub int){sub = n1 - n2sum = n1 + n2// 返回值的顺序按照函数定义,与这里写的前后无关return sub,sum
}

Go 支持可变参数,可变参数需要放在形参列表最后

func sum(n1 int, args... int) int {sum := n1 // 遍历可变参数列表args for i := 0; i < len(args); i++ {sum += args[i]  //args[0] 表示取出args切片的第一个元素值,其它依次类推}return sum
}

3.2 函数机制

init函数

每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用;注意,import包中的函数优先于本文件执行,即执行顺序为:外部包、全局变量、init、main

var age = test()
// 全局变量是先被初始化的,我们这里先写函数
func test() int {fmt.Println("test()") //1return 90
}// init函数,通常可以在init函数中完成初始化工作
func init() {fmt.Println("init()...") //2
}func main() {fmt.Println("main()...age=", age) //3
}
匿名函数

Go 支持匿名函数,匿名函数就是没有名字的函数,如下所示有三种使用场景

package main
import ("fmt"
)var (// 定义全局匿名函数Fun1 = func (n1 int, n2 int) int {return n1 * n2}
)func main() {// 定义匿名函数时就直接调用,只调用一次res1 := func (n1 int, n2 int) int {return n1 + n2}(10, 20)fmt.Println("res1=", res1)// 将匿名函数赋给变量,通过a完成调用a := func (n1 int, n2 int) int {return n1 - n2}res2 := a(10, 30)fmt.Println("res2=", res2)// 全局匿名函数的使用res4 := Fun1(4, 9)fmt.Println("res4=", res4)
}
闭包

闭包就是一个函数和与其相关的引用环境组合的一个整体

func AddUpper(n int) func (int) int {return func (x int) int {n = n + xreturn n}
}func main() {f := AddUpper(10)fmt.Println(f(1))// 10 + 1 = 11 fmt.Println(f(2))// 11 + 2 = 13
}

如上面代码所示,AddUpper返回一个匿名函数,其中用到了变量n,这样匿名函数和它用到的变量n就形成了一个闭包。这里的变量n是闭包的一个变量,只会初始化一次n=10,第一次调用结果n为11,第二次调用的时候n在11的基础上进行累加。

可以看到,使用闭包可以灵活定制并返回一个基数n为10的函数f

defer

Go中的延时执行机制,当执行到defer时,暂时不执行,会将defer的语句压入到独立的栈中;当函数执行完毕后,再按照先入后出的方式出栈执行。通常用于在函数执行完毕后,进行释放资源。在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈

func sum(n1 int, n2 int) int {defer fmt.Println("ok1 n1=", n1) //defer 3. ok1 n1 = 10defer fmt.Println("ok2 n2=", n2) //defer 2. ok2 n2= 20n1++ // n1 = 11n2++ // n2 = 21res := n1 + n2 // res = 32fmt.Println("ok3 res=", res) // 1. ok3 res= 32return res}func main() {res := sum(10, 20)fmt.Println("res=", res)  // 4. res= 32
}
内置函数
  1. len:用来求长度,比如 string、array、slice、map、channel
  2. new:用来分配内存,主要用来分配值类型,比如 int、float32,struct…返回的是指针
  3. make:用来分配内存,主要用来分配引用类型,比如 channel、map、slice。

3.3 字符串

Go中常见的字符串处理相关函数

	str2 := "hello北京"// 字符串的长度,golang的编码统一为utf-8 ,字母和数字占一个字节,汉字占用3个字节,用rune(str)处理r := []rune(str2)for i := 0; i < len(r); i++ {fmt.Printf("字符=%c\n", r[i])}//字符串转整数n, err := strconv.Atoi("123")if err != nil {fmt.Println("转换错误", err)}else {fmt.Println("转成的结果是", n)}//4)整数转字符串  str = strconv.Itoa(12345)//5)字符串 转 []bytevar bytes = []byte("hello go")str = string([]byte{97, 98, 99}) fmt.Printf("str=%v\n", str)//10进制转 2, 8, 16进制,返回对应的字符串str = strconv.FormatInt(123, 16)fmt.Printf("123对应的16进制是=%v\n", str)//查找子串是否在指定的字符串中b := strings.Contains("seafood", "mary")//统计一个字符串有几个指定的子串 4num := strings.Count("ceheese", "e")// 不区分大小写的字符串比较(==是区分字母大小写的)b = strings.EqualFold("abc", "Abc")fmt.Println("结果","abc" == "Abc") // false 区分字母大小写// 返回子串在字符串第一次出现的index值,如果没有返回-1 : index := strings.Index("NLT_abcabcabc", "abc") // 4// 返回子串在字符串最后一次出现的indexindex = strings.LastIndex("go golang", "go") //3//将指定的子串替换成 另外一个子串,n可以指定你希望替换几个,如果n=-1表示全部替换str2 = "go go hello"str = strings.Replace(str2, "go", "北京", -1)// 按照指定的某个字符分割字符串 strArr := strings.Split("hello,wrold,ok", ",")for i := 0; i < len(strArr); i++ {fmt.Printf("str[%v]=%v\n", i, strArr[i])} //15)将字符串的字母进行大小写的转换: str = "goLang Hello"str = strings.ToLower(str) str = strings.ToUpper(str) //将字符串左右两边的空格去掉str = strings.TrimSpace(" tn a lone gopher ntrn   ")// 将字符串左右两边指定的字符去掉 str = strings.Trim("! he!llo! ", " !")// 判断字符串是否以指定的字符串开头:b = strings.HasPrefix("ftp://192.168.10.1", "hsp") //true

3.4 时间相关

如下所示为Go中时间有关函数的使用

//1. 获取当前时间now := time.Now()fmt.Printf("now=%v now type=%T\n", now, now)//获取到年月日,时分秒fmt.Printf("年=%v\n", now.Year())fmt.Printf("月=%v\n", now.Month())fmt.Printf("月=%v\n", int(now.Month()))fmt.Printf("日=%v\n", now.Day())fmt.Printf("时=%v\n", now.Hour())fmt.Printf("分=%v\n", now.Minute())fmt.Printf("秒=%v\n", now.Second())//格式化日期时间dateStr := fmt.Sprintf("当前年月日 %d-%d-%d %d:%d:%d \n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())fmt.Printf("dateStr=%v\n", dateStr)//格式化日期时间,Go设计者规定了用2006-01-02 15:04:05这个常量来指代时间格式fmt.Printf(now.Format("2006-01-02 15:04:05"))fmt.Println()fmt.Printf(now.Format("2006-01-02"))fmt.Println()// 休眠100毫秒time.Sleep(time.Millisecond * 100)// 获取秒和纳秒的时间戳fmt.Printf("unix时间戳=%v unixnano时间戳=%v\n", now.Unix(), now.UnixNano())

3.5 异常处理

Go 语言不支持传统的 try…catch…finally 这种处理,而是通过抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常

func test() {defer func() {err := recover()  // recover()内置函数,可以捕获到异常if err != nil {fmt.Println("err=", err)}}()	// 定义并调用匿名函数num1 := 10num2 := 0res := num1 / num2	// 除0异常fmt.Println("res=", res)
}

也可以使用errors.New抛出自定义异常;使用panic 内置函数可以接收 error 类型的变量,输出错误信息,并退出程序

func readConf(name string) (errInfo error) {if name == "config.ini" {return nil} else {return errors.New("读取文件错误..") // 抛出一个自定义错误}
}func test02() {errInfo := readConf("config2.ini")if errInfo != nil {panic(errInfo)	// 输出这个错误,并终止程序}
}

3.6 单元测试

Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试

import ("fmt""testing" //引入go 的testing框架包
)func TestAddUpper(t *testing.T) {	// 测试addUpper是否正确res := addUpper(10)	//调用if res != 55 {t.Fatalf("AddUpper(10) 执行错误,期望值=%v 实际值=%v\n", 55, res)}t.Logf("AddUpper(10) 执行正确...")
}
  1. 测试用例文件名必须以 xxx_test.go 结尾。 比如 cal_test.go
  2. 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper
  3. TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T
  4. 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub
  5. 运行测试用例指令go test 只有运行错误时会输出日志, go test -v运行正确或是错误,都输出日志
  6. 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序
  7. t.Logf 方法可以输出相应的日志
  8. 测试单个文件,一定要带上被测试的原文件go test -v cal_test.go cal.go
  9. 测试单个方法go test -v -test.run TestAddUpper

4 面向对象

结构体

Golang 没有类(class),而是通过结构体(struct)来实现 OOP 特性的,它去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函
数、隐藏的 this 指针等。

它仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承:Golang 没有 extends 关键字,继承是通过匿名字段来实现。

在创建一个结构体变量后,如果没有给字段赋值,都对应一个默认值,指针、slice、和 map 都是 nil ,即还没有分配空间,使用前记得手动make空间

如下所示为结构体四种声明和使用方式

type Person struct{Name stringAge int
}func main() {var person Person	// 1 直接声明p2 := Person{"mary", 20}	// 2 声明并初始化p2.Name = "tom"var p3 *Person= new(Person)	// 3 使用指针//p3是一个指针,标准赋值方式(*p3).Name = "smith",Go底层进行了处理,可以简写成 p3.Name = "smith"(*p3).Name = "smith" p3.Name = "john"var person *Person = &Person{}	//方式4-{}(*person).Name = "scott"person.Name = "scott~~"
}// 返回结构体变量var stu1 = Stu{"小明", 19} // stu1---> 结构体数据空间var stu3 = Stu{	// 把字段名和字段值写在一起, 不依赖字段的定义顺序.Name :"jack",Age : 20,}// 返回结构体的指针类型var stu5 *Stu = &Stu{"小王", 29}  // stu5--> 地址 ---》 结构体数据[xxxx,xxx]var stu7 = &Stu{Name : "小李",Age :49,}fmt.Println(stu1, stu3, *stu5, *stu7) //

结构体是值类型,变量直接是值拷贝,修改不会传递;但是如果使用指针指向结构体地址,则修改的是同一个内容

	var p1 Personp1.Age = 10p1.Name = "小明"var p2 *Person = &p1 // 定义指针指向p1代表的结构体p2.Name = "tom~"	// 会同步修改p1的结构体fmt.Printf("p2.Name=%v p1.Name=%v \n", (*p2).Name, p1.Name) // 注意.运算顺序比价高,(*p2)要括起来
  • 不同类型结构体之间进行强制类型转换时需要有完全相同的字段。
  • 使用type关键字可以为变量赋别名,Go认为其是一种新的类型,但是可以通过类型转换
  • struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常用于序列化和反序列化。
type Monster struct{Name string `json:"name"` // `json:"name"` 就是 struct的 tagAge int `json:"age"`		// 为了使外部包可见,变量名首字母大写
}
func main() {//1. 创建一个Monster变量monster := Monster{"小牛", 500}//2. 将monster变量序列化为 json格式字串jsonStr, err := json.Marshal(monster)if err != nil {fmt.Println("json 处理错误 ", err)}fmt.Println("jsonStr", string(jsonStr))	// 序列化输出为name,而不是Name
}

方法

Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型都可以有方法,不仅 struct

// 定义结构体和变量
type Person struct{Name string
}/*
func (recevier type) methodName(参数列表) (返回值列表){方法体return 返回值
}
*/
func (p Person) speak() { // 给Persion类型添加一个方法fmt.Println(p.Name, "是一个goodman~")	// 形参p是值传递
}func main() {var p Personp.Name = "tom"p.test() // test 方法只能通过 Person 类型的变量来调用fmt.Println("main() p.Name=", p.Name) //输出 tom
}

变量调用方法时,该变量本身也会作为一个参数传递到方法,如果变量是值类型则进行值拷贝,如果是引用类型则进行地址拷贝。因此需要修改变量本身需要传递指针类型。

如下需要注意函数和方法在使用方式上的不同:

//对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
func test01(p Person) {fmt.Println(p.Name)
}
func test02(p *Person) {fmt.Println(p.Name)
}func main() {p := Person{"tom"}test01(p)	// 只能传递值类型test02(&p)	// 只能传递指针
}//对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法,效果是值拷贝;反过来同样也可以。主要是看方法的定义是什么类型
func (p Person) test03() {	// 接收值类型p.Name = "jack"fmt.Println("test03() =", p.Name)
}func main() {p := Person{"tom"}p.test03()		// 可以直接用值调用(&p).test03() // 也可以用指针调用,但是本质仍然是值拷贝
}

封装

Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题,并通过get/set方法对属性进行设置,这就是Go中对象封装的特性

type student struct{Name stringscore float64
}//因为student结构体首字母是小写,因此是只能在model使用,通过定义外部可见的方法来提供初始化变量
func NewStudent(n string, s float64) *student {return &student{Name : n,score : s,}
}func (s *student) GetScore() float64{	// 由于结构体score是小写,这里提供一个get方法供其他包获取查询return s.score 
}

继承

在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性

type Student struct {	// 父类Name stringAge intScore int
}func (stu *Student) ShowInfo() {	// 父类的公共方法fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n", stu.Name, stu.Age, stu.Score)
}
func (stu *Student) setScore(score int) {stu.Score = score
}type Pupil struct { Student //	在子类中嵌入了Student匿名结构体
}func (p *Pupil) testing() {	// 定义子类的特有方法fmt.Println("小学生正在考试中.....")
}func main() {pupil := &Pupil{}pupil.Student.Name = "tom~"	// 访问父类的属性pupil.Age = 8							// 可以省略父类路径,编译器会自动进行属性查找pupil.testing() 						// 调用子类方法pupil.Student.setScore(70)	// 调用父类方法,不论大小写都可以pupil.Student.ShowInfo()
}

通过子类对象访问父类字段、属性时,父类名可以省略,当子类和父类有相同的字段或者方法时,编译器采用就近原则先访问子类的;如希望访问父类的字段和方法,可以加上父类匿名结构体名来区分。
结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错

如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,在访问组合的结构体的字段或方法时,必须带上结构体的名字

type D struct {a A //有名结构体
}var d D 
d.a.Name = "jack"

嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值

type Goods struct {Name stringPrice float64
}
type Brand struct {Name stringAddress string
}
type TV struct {Goods			// 多重继承Brand	
}func main() {tv2 := TV{ Goods{Price : 5000.99,Name : "电视机002", },  Brand{Name : "夏普", Address :"北京",}, }
}

接口

interface 类型可以定义一组方法,但是不需要实现,并且不包含任何变量。当使用的时候,再根据具体情况进行实现
多态是面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的,即按照统一的接口来调用不同的实现。

type Usb interface {	// 定义接口Start() 		// 声明了两个没有实现的方法Stop()
}type Phone struct {							// 定义结构体
}  
func (p Phone) Start() {				// 实现Usb接口的方法,Go中并不会显示指出实现的接口名,但是在调用的时候会校验是否实现了接口的所有方法fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {fmt.Println("手机停止工作。。。")
}type Camera struct {
}
func (c Camera) Start() {					// 实现Usb接口的方法fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop() {fmt.Println("相机停止工作。。。")
}type Computer struct {		// 通过计算机调用Usb接口
}
func (c Computer) Working(usb Usb) {		// Working方法可以接收所有Usb接口类型变量usb.Start()		// 统一通过usb接口变量来调用Start和Stop方法usb.Stop()
}func main() {computer := Computer{}phone := Phone{}camera := Camera{}computer.Working(phone)		// 统一对实现了接口的变量进行调用computer.Working(camera)
}
  • 一个接口可以继承多个别的接口,这时如果要实现该接口,必须将父接口的方法也全部实现,注意多个接口之间不能有相同的方法名
  • Interface 类型默认是一个指针(引用类型),如果没有初始化就使用,会输出 nil
  • 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口
type Stu struct {
}
type T  interface{
}
func main() {var stu Stuvar t T  = stu	// 将变量stu赋值给空接口类型的tfmt.Println(t)
}

和继承不同,实现接口可以在不改变结构体的基础上,对其功能进行扩展。
继承的价值主要在于:解决代码的复用性和可维护性。
接口的价值主要在于:设计好各种规范(方法),让其它自定义类型去实现这些方法

type Monkey struct {Name string
}
func (this *Monkey) climbing() {fmt.Println(this.Name, " 生来会爬树..")
}type LittleMonkey struct {Monkey //继承
}//声明接口
type BirdAble interface {Flying()
}// 在不改变LittleMonkey的基础上,为其增加了新的方法
func (this *LittleMonkey) Flying() {fmt.Println(this.Name, " 通过学习,会飞翔...")
}func main() {monkey := LittleMonkey{Monkey {Name : "悟空",},}monkey.climbing()monkey.Flying()
}

通过类型断言,可以将一个接口转换成具体的类型

	var x interface{}var b2 float32 = 2.1x = b2  //空接口,可以接收任意类型if y, ok := x.(float32); ok {	// 将接口类型x转换为float32fmt.Printf("y 的类型是 %T 值是=%v", y, y)} else {fmt.Println("convert fail")}

如下所示,在遍历多态数组时,使用类型断言可以对不同的类型进行判断,进而执行不同的代码逻辑

func (computer Computer) Working(usb Usb) {usb.Start()if phone, ok := usb.(Phone); ok {	// 使用类型断言,如果是Phone,额外执行Call方法phone.Call()}usb.Stop()
}func main() {var usbArr [3]UsbusbArr[0] = Phone{"vivo"}		// 多态数组,存放实现了接口的不同类型对象usbArr[1] = Phone{"小米"}usbArr[2] = Camera{"尼康"}var computer Computerfor _, v := range usbArr{		// 统一进行遍历computer.Working(v)fmt.Println()}
}

反射

反射可以在运行时动态获取变量的各种信息, 比如变量的类型(type),类别(kind),结构体的字段、方法;通过反射,可以修改变量的值,并且调用关联的方法

普通的数据类型通过interface可以和reflect.Type之间进行转换

func main() {var num int = 100reflectTest01(num)
}func reflectTest01(b interface{}) {//通过反射获取的传入的变量的 type , kind, 值rTyp := reflect.TypeOf(b)	// 获取 reflect.Typefmt.Println("rType=", rTyp)rVal := reflect.ValueOf(b)	// 获取到 reflect.Value,注意这里rVal并不是真正的变量,而是反射的变量类型n2 := 2 + rVal.Int()		// 通过反射变量定义的方法获取到具体值fmt.Println("n2=", n2)// 反过来,先将 rVal 转成 interface{},再通过断言转成需要的类型iV := rVal.Interface()num2 := iV.(int)fmt.Println("num2=", num2)
}// 结构体的反射
func reflectTest02(b interface{}) {	rTyp := reflect.TypeOf(b)		// 获取到 reflect.TyperVal := reflect.ValueOf(b)kind1 := rVal.Kind()		// 获取 变量对应的Kindkind2 := rTyp.Kind()// 通过反射获取结构体的值iV := rVal.Interface()stu, ok := iV.(Student)if ok {fmt.Printf("stu.Name=%v\n", stu.Name)}
}

5 文件

Go在os.File 封装所有文件相关操作,File 是一个结构体

如下所示为读取文件两个方式

import ("fmt""os""bufio""io"
)func main() {file , err := os.Open("d:/test.txt")		// 打开文件if err != nil {fmt.Println("open file err=", err)}defer file.Close() // 结束时及时关闭file句柄,否则会有内存泄漏.reader := bufio.NewReader(file)	// 使用带缓冲区的读取for {str, err := reader.ReadString('\n') // 每读到一个换行就停止if err == io.EOF { // io.EOF表示文件的末尾break}fmt.Printf(str)}content, err := ioutil.ReadFile(file)	// 使用ioutil.ReadFile一次性将文件读取,不需要手动进行打开关闭if err != nil {fmt.Printf("read file err=%v", err)}fmt.Printf("%v", string(content)) // []byte,把读取到的内容转为字符串
}

如下所示为文件写入的方式

	filePath := "d:/abc.txt"file, err := os.OpenFile(filePath, os.O_WRONLY | os.O_CREATE, 0666)	// 以读写的方式进行打开,不存在则创建if err != nil {fmt.Printf("open file err=%v\n", err)return }defer file.Close()str := "hello,Gardon\r\n"writer := bufio.NewWriter(file)	// 使用带缓存的 *Writer进行写入for i := 0; i < 5; i++ {writer.WriteString(str)}writer.Flush()	// 将缓存中的内容刷入文件

go可以flag 包用来解析命令行参数

//定义几个变量,用于接收命令行的参数值var user stringvar pwd string//"u" ,就是 -u 指定参数//"" , 默认值//"用户名,默认为空" 说明flag.StringVar(&user, "u", "", "用户名,默认为空")flag.StringVar(&pwd, "pwd", "", "密码,默认为空")flag.Parse()		// 转换, 必须调用该方法//输出结果fmt.Printf("user=%v pwd=%v host=%v port=%v", user, pwd, host, port)

使用json包对结构体进行序列化和反序列化

func testStruct() {monster := Monster{Name :"牛魔王",Age : 500 ,}data, err := json.Marshal(&monster) //将monster 序列化if err != nil {fmt.Printf("序列号错误 err=%v\n", err)}fmt.Printf("monster序列化后=%v\n", string(data))
}func unmarshalStruct() {str := "{\"Name\":\"小王\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"Sal\":8000,\"Skill\":\"eat\"}"var monster Monstererr := json.Unmarshal([]byte(str), &monster)		// 反序列化if err != nil {fmt.Printf("unmarshal err=%v\n", err)}fmt.Printf("反序列化后 monster=%v monster.Name=%v \n", monster, monster.Name)
}

6 协程与管道

Goroutine

并发:多线程程序在单核上运行
并行:多线程程序在多核上运行
在 Go 主线程上,可以再起多个协程(Goroutine),它有如下特点:

  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程,与线程实现并发相比资源消耗小

如下所示通过go开启了一个协程,它会和主线程同步一起执行,直到执行完成或者主线程退出

func test() {for i := 1; i <= 10; i++ {fmt.Println("test () hello,world " + strconv.Itoa(i))time.Sleep(time.Second)}
}func main() {go test() // 开启了一个协程for i := 1; i <= 10; i++ {fmt.Println(" main() hello,golang" + strconv.Itoa(i))		// test和main会一起执行,交替输出time.Sleep(time.Second)}
}cpuNum := runtime.NumCPU()
fmt.Println("cpuNum=", cpuNum)	// check num of cpu
runtime.GOMAXPROCS(cpuNum - 1)	// 设置使用多个cpu

MPG模型:如图所示,有两个物理线程 M,每个 M 绑定一个处理器 P,并运行一个 Goroutine。
在这里插入图片描述

  • P 的数量可以通过 GOMAXPROCS() 设置。它实际上表示真正的并发级别,即可以同时运行的 Goroutine 数量。
  • 图中灰色的 Goroutine 尚未运行,处于就绪状态,等待被调度。P 维护了这些 Goroutine 的队列(称为运行队列 runqueue)。
  • 每次执行 go 语句时,都会将一个 Goroutine 添加到运行队列末尾。在下一个调度点,会从运行队列中取出一个 Goroutine 执行
  • 当某个操作系统线程(如 M0)被阻塞时(如下图所示),P 会切换到另一个线程(如 M1)。M1 可能是新创建的,也可能是从线程缓存中取出的。
  • 当 M0 返回时,它需要尝试获取一个 P 来运行 Goroutine。如果无法获取 P,它会将 Goroutine 放入全局运行队列,并进入休眠状态(进入线程缓存)。所有 P 会定期检查全局运行队列,并运行其中的 Goroutine;否则,全局运行队列中的 Goroutine 将永远无法执行。

在使用多个协程运行程序的时候,很容易出现同时写入一个资源的冲突,有两种解决方式

  1. 使用全局互斥锁对资源进行加锁
  2. 使用管道 channel 来解决

如下所示通过启动多个协程计算 1-200 的各个数的阶乘

package main
import ("fmt"_ "time""sync"
)var (myMap = make(map[int]int, 10)  lock sync.Mutex	//声明一个全局的互斥锁
)func calculateN(n int) {	// 计算n的阶乘res := 1for i := 1; i <= n; i++ {res *= i}lock.Lock()		// 操作myMap前加锁myMap[n] = res lock.Unlock()
}func main() {for i := 1; i <= 20; i++ {	// 开启多个协程完成这个任务go calculateN(i)}time.Sleep(time.Second * 5)	// 主线程休眠等待协程运算结束lock.Lock()		// 读取myMap时也需要加锁for i, v := range myMap {fmt.Printf("map[%d]=%d\n", i, v)}lock.Unlock()
}

上面使用互斥锁的方式并不优雅,需要手动估算协程运行结束时间,通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作

服务器通常启动一个协程对请求进行响应

package main
import ("fmt""net"_"io"
)func process(conn net.Conn) {defer conn.Close() //关闭conn// 循环接收客户端发送的数据for {buf := make([]byte, 1024)// 等待客户端通过conn发送信息,如果客户端没有wrtie[发送],那么协程就阻塞在这里n , err := conn.Read(buf) //从conn读取if err != nil {fmt.Printf("客户端退出 err=%v", err)return //!!!}fmt.Print(string(buf[:n])) 	// 打印接收到的内容}
}func main() {fmt.Println("服务器开始监听....")listen, err := net.Listen("tcp", "0.0.0.0:8888")	// 监听本地TCP的8888端口if err != nil {fmt.Println("listen err=", err)return }defer listen.Close() //延时关闭listen//循环等待客户端来链接for {fmt.Println("等待客户端来链接....")conn, err := listen.Accept()if err != nil {fmt.Println("Accept() err=", err)} else {fmt.Printf("Accept() suc con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())}// 启动协程处理客户端连接go process(conn)}
}

channel

channel本质就是一个先进先出的队列,它本身就是线程安全的,支持多 goroutine 同时访问

	// 创建一个可以存放3个int类型的管道var intChan chan intintChan = make(chan int, 3)num := 211intChan<- num	// 向管道写入数// 管道的实际长度和总cap(容量),写入数据时,不能超过其容量,超过再写入的话就会阻塞而报deadlockfmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3var num2 intnum2 = <-intChan // 从管道中读取数据,如果数据已经全部取出,再取就会报告 deadlockfmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))  // 2, 3close(intChan)	// 使用内置函数 close 可以关闭 channel, 关闭后就不能再向 channel 写数据了close(intChan2)	//在遍历时,如果channel没有关闭,则会出现deadlock的错误for v := range intChan2 {		// 需要使用for range进行遍历管道fmt.Println("v=", v)}

当使用interface类型的管道时,需要注意从管道中取出来的还是interface类型,需要做一下转换

	//定义一个存放任意数据类型的管道 3个数据allChan := make(chan interface{}, 3)cat := Cat{"小花猫", 4}allChan<- catallChan<- 10allChan<- "tom jack"newCat := <-allChan	//fmt.Printf("newCat.Name=%v", newCat.Name)		// 编译不通过a := newCat.(Cat) 	//使用类型断言fmt.Printf("newCat.Name=%v", a.Name)

管道的一些使用细节:

	//1. 在默认情况下下,管道是双向的,也可以声明为只读/只写var chan2 chan<- int	// 声明为只写chan2 = make(chan int, 3)chan2<- 20var chan3 <-chan int	// 声明为只读num2 := <-chan3// 2 在实际开发中,可能我们不好确定什么关闭该管道.可以使用select 方式可以解决for {select {// 这里如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配case v := <-intChan : fmt.Printf("从intChan读取的数据%d\n", v)case v := <-stringChan :fmt.Printf("从stringChan读取的数据%s\n", v)default :return }}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com