Go基础入门

Go语言介绍

解释型语言和编译型语言的区别。

image-20210625104252453

Go语言是编译型语言。

Go语言的特点(自带垃圾回收,天生支持并发)

  • 语法简洁
  • 开发效率高
  • 执行性能好

Go的依赖管理

为啥需要依赖管理

最早的时候,Go所依赖的所有第三方库都在GOPATH目录下,这就导致同一个库只能保存在一个版本的代码,如果不同的项目依赖同一个库的不同版本,就没办法了。

godep

从1.5开始引入vendor模式,如果项目目录下有vendor目录,那么go工具链就会优先使用vender内的包进行编译,测试等。

godep是一个通过vender模式实现的Go语言的第三方依赖管理工具,类似的还有社区维护的包管理工具dep。

安装godep

go get github.com/tools/godep

在终端输入godep查看支持的所有命令

image-20210625114049750

使用godep save命令,会在当前项目中创建Godeps和vender两个文件夹,其中Godeps文件夹下有一个Godeps.json的文件,里面记录了项目所依赖的包信息。vender文件夹下是项目依赖的包的源代码文件。

vender机制

Go1.5版本之后开始支持,能够控制Go语言程序编译时依赖包搜索路径的优先级。

例如查找项目的某个依赖包,首先会在项目根目录下的vender文件夹中查找,如果没有找到就会去$GOAPTH/src目录下查找。

包和文件

在Go语言中,如果导入了一个包但是没有使用该包,将被当做一个编译错误处理,这种强制规则可以有效减少不必要的依赖。goimports工具可以根据需要自动添加或者删除导入的包,许多编辑器都可集成这个工具,然后保存文件自动运行,gofmt工具可以用来格式化Go文件。

包的初始化首先是解决包级别变量的依赖顺序,然后按照包级别变量声明出现的顺序依次初始化。

包中一般含有多个go源文件,他们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将go文件根据文件名进行排序,然后依次调用编译器编译。

init初始化函数

每个文件都可以包含多个init初始化函数,这样的初始化函数除了不能被调用或者引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照他们声明的顺序被自动调用。

func init(){
    
}

每个包只会被初始化一次,如果p包导入了另一个包p,那么p在初始化的时候q就必然已经初始化过了。所以main包是最后被初始化的。

变量和常量

函数外面只能放标识符(变量,常量,函数,类型)的声明。

Go语言必须先声明再使用。

常量表达式的值在编译期进行计算,而不是在运行期,每种常量的潜在类型都是基础类型或数字。常量一旦赋值不可修改,值不会改变。

标识符

在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名、常量名、函数名等等。 Go语言中标识符由字母数字和_(下划线)组成,并且只能以字母和_开头。 举几个例子:abc, _, _123, a123

关键字

关键字是指编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名。

Go语言中有25个关键字:

    break        default      func         interface    select
    case         defer        go           map          struct
    chan         else         goto         package      switch
    const        fallthrough  if           range        type
    continue     for          import       return       var

此外,Go语言中还有37个保留字。

    Constants:    true  false  iota  nil

        Types:    int  int8  int16  int32  int64  
                  uint  uint8  uint16  uint32  uint64  uintptr
                  float32  float64  complex128  complex64
                  bool  byte  rune  string  error

    Functions:   make  len  cap  new  append  copy  close  delete
                 complex  real  imag
                 panic  recover

变量

变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。 举个例子:

// 标准声明
var studentName string

// 批量声明
var (
	age  int
	isOk bool
)

变量的初始化

Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil。出现在赋值语句的右边时,并不一定是产生两个结果,也可能产生两个结果。对于值产生一个结果的情形,map 查找失败时会返回零值,类型断言失败时会发送运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。

var name string = "Q1mi"
var age int = 18
var name, times = "Q1mi", 20

我们可以直接将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。

var name = "Q1mi"
var age = 18

短变量声明

在函数内部,可以使用更简略的 := 方式声明并初始化变量,函数外面不能使用简短声明。

func testNum() {
	n := 10
	m := 200 // 此处声明局部变量m
	fmt.Println(m, n)
}

匿名变量

在使用多重赋值的时候,如果想要忽略某个值,可以使用_接收_,表示一个匿名变量。

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。在lua编程语言中,匿名变量也叫做哑元变量。

func foo() (int, string) {
	return 100, "hehe"
}
func main() {
	// _ 匿名变量,相当于占位符
	_, str := foo()
	fmt.Println(str)
}

注意事项:

  1. 函数外的每个语句都必须以关键字开始(var、const、func等)
  2. :=不能使用在函数外。
  3. _多用于占位,表示忽略值。
  4. 局部变量声明后,必须使用,否则会编译报错。

类型

在赋值语句两边的变量最终的值必须有相同的数据类型。

新命名的类型提供了一个方法,用来分割不同概念的类型,这样即使他们底层类型相同也是不兼容的。

type 类型名称 底层类型
type num int

类型声明的语句一般出现在包一级,如果新创建的类型名字的首字母大写,那么在外部包也可以使用。

对于中文汉字,Unicode标志都作为小写字母处理,因此中文的命名默认不能导出。

对于每个类型T,都有一个类型转换操作T(x),将x转换成T类型,如果是指针烈性,可能要用小括号包装T,(*int)(0)。只有当两个类型的底层基础类型相同时,才允许这种转型,或者是两者都是指向相同底层结构的指针类型,这些转换值改变类型而不会影响值本身。

如果两个值有着不同的类型,则不能直接比较。编译会报错。

var	c	Celsius 
var	f	Fahrenheit
fmt.Println(c	==	0)										//	"true"
fmt.Println(f	>=	0)										//	"true"
fmt.Println(c	==	f)										//	compile	error:	type	mismatch
fmt.Println(c	==	Celsius(f))	//	"true"!

常量

对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。

const pi = 3.1415
const e = 2.7182

声明了pie这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。

多个常量也可以一起声明:

const (
    pi = 3.1415
    e = 2.7182
)

const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如:

const (
    n1 = 100
    n2
    n3
)

上面示例中,常量n1n2n3的值都是100。

常量计数器iota

iota是go语言的常量计数器,只能在常量的表达式中使用。

iota在const关键字出现时将被重置为0,const中每次增加一行常量声明将使iota计数一次,使用iota能简化定义,在定义枚举时很有用。

const (
	n1 = iota //0
	n2        //1
	_
	n4 //3
	_  = iota
	KB = 1 << (10 * iota)
	MB = 1 << (10 * iota)
	GB = 1 << (10 * iota) //从这开始会溢出
	TB = 1 << (10 * iota)
	PB = 1 << (10 * iota)
)

func main(){
    fmt.Println(n1)
	fmt.Println(n2)
	fmt.Println(n4)
	fmt.Println(KB)
	fmt.Println(MB)
}
// 输出
0
1
3
1125899906842624
1152921504606846976

进制输出

%d:十进制

%o:八进制

%x:十六进制

%b:二进制

%T:输出数据类型

%c:字符

%s:字符串

%p:指针

%v:值

%#v:详细值

%f:浮点数

%t:bool值

func main() {

	i2 := 077
	fmt.Printf("十进制:%d\n", i2)
	fmt.Printf("二进制:%b\n", i2)
	fmt.Printf("八进制:%o\n", i2)
	fmt.Printf("十六进制:%x\n", i2)
	fmt.Printf("数据类型:%T\n", i2)
	//强制类型转换
	fmt.Printf("%d", int32(i2))
}
/**十进制:63
二进制:111111
八进制:77
十六进制:3f
数据类型:int
63**/

数据类型

uint8 无符号 8位整型 (0 到 255)
uint16 无符号 16位整型 (0 到 65535)
uint32 无符号 32位整型 (0 到 4294967295)
uint64 无符号 64位整型 (0 到 18446744073709551615)
int8 有符号 8位整型 (-128 到 127)
int16 有符号 16位整型 (-32768 到 32767)
int32 有符号 32位整型 (-2147483648 到 2147483647)
int64 有符号 64位整型 (-9223372036854775808 到 9223372036854775807)

特殊整型

类型 描述
uint 32位操作系统上就是uint32,64位操作系统上就是uint64
int 32位操作系统上就是int32,64位操作系统上就是int64
uintptr 无符号整型,用于存放一个指针

注意: 在使用intuint类型时,不能假定它是32位或64位的整型,而是考虑intuint可能在不同平台上的差异。

获取对象长度的内建len()函数返回的长度可根据不同的平台的字节长度进行变化,实际使用中,切片或map的元素数量等都可以用int来表示,但是在进行二进制传输,读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用int和uint。

浮点类型

Go语言支持两种浮点类型:float32和float64,这两种浮点类型数据格式遵循IEEE754标准,float32的浮点数的最大范围是3.4e38,可以使用常量定义:math_MaxFloat32,float64的浮点数的最大范围大约是1.8e308,可以使用常量定义:math.MaxFloat64。默认是float64位类型。

%f:表示浮点数输出类型

math包中除了提供大量常用的数学函数之外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果,NaN表示无效的除法操作0/0或者Sqrt(-1)

NaN和任何数都是不相等的(译注:在浮点数中,NaN、正无穷大和负无穷大都不是唯一 的,每个都有非常多种的bit模式表示)

数组是值类型,值传递(复制一份,在新数据上修改,原先数据不受影响)

复数

complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和image函数分别返回复数的实部和虚部。

var	x	complex128	=	complex(1,	2)	//	1+2i 
var	y	complex128	=	complex(3,	4)	//	3+4i
fmt.Println(x*y)																	//	"(-5+10i)"
fmt.Println(real(x*y))											//	"-5"
fmt.Println(imag(x*y))											//	"10"

复数也可以进行比较,只有当两者的实部和虚部全都一样的时候才相等。

复数的简化声明

x:=1+2i
y:=3+4i

字符串

字符串是不可变的字节序列,字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF-8编码的Unicode码点序列。

len(s)会返回字符串s中的字节数目,字符串之间可以通过“+”连接,因为字符串是不可变的所以不能进行修改。例如

s[0]='L'// compile	error:	cannot	assign	to	s[0]

不变性意味如果两个字符串共享相同的底层数据也是安全的,这使得复制任何长度的字符串代价是低廉的。

原生字符串:`...`,使用反引号代替双引号,在原生的字符串中,没有转义操作,全部的内容都是字面的意思,包含退格和换行。原生字符串面值用于编写正则表达式很方便,因为正则表达式往往会包含很多反斜杠。同时适用于HTML模板,JSON返回格式,命令行信息以及需要扩展的多行场景。

Go语言的源文件采用UTF-8编码,并且Go语言处理UTF8编码的文本很出色,包含处理字符相关功能的函数,Unicode/uft8包则是提供了用于rune字符序列的UTF8编码和解码的功能。

有很多Unicode字符很难直接从键盘输入,并且还有很多字符有着相似的结构。

go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。中文在字符串中的大小是3个字节。

如果在输出结果中看到一个黑色六角形或者钻石形状的图标,比嗾使解码失败,原因是输入的字符串中有错误的编码。

数组

数组的长度是数组类型的一个组成部分,例如[3]int和[4]int是两种不同的数组类型,数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

数组可以通过下标来访问,下标是从0-n-1,n代表数组的长度

默认情况下数组中的元素都会被初始化为零值。

如果出现[...]这种形式,表示数组的长度是根据初始化值的个数决定的。

func initArray() {
	// 默认情况下数组中的每个元素都会被初始化为零值
	var array1[3] int=[3]int{1,2,3}
	for i, v := range array1 {
		fmt.Println(i,v)
	}
	//如果[...]这种形式表示数组的长度是根据初始化值的个数决定的
	queue:=[...]int{5,4,3,2,1}

	fmt.Printf("%T\n",queue)
	fmt.Println(queue)


}

切片

数组切片类似于一个指向数组的指针,实际上他拥有自己的数据结构。Slice(切片)代表可变长的序列,序列中每个元素都有相同的类型。一个slice类西行一般写作[]T,其中T代表slice中元素的类型。

  • 一个指向原生数组的指针;
  • 数组切片中的元素个数
  • 数组切片已分配的存储空间

创建数组切片的方式

  • 基于数组

切片是引用类型,底层指向数组的指针,数组中的元素被改变,切片也会改变。

slice的切片操作s[i:j],其中0≤i≤j≤cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。因此,months[1:13]切片操作将引用全部 有效的月份,和months[1:]操作等价;months[:]切片操作则是引用整个数组。

func main() {
	//切片的定义
	a:=[5]int{5,66,77,88,99}
	b:=a[:4] //切片是前闭后开【5,66,77,88】
	fmt.Println(b)
	fmt.Printf("slice type is %T\n",b)
	fmt.Printf("b:len=%d,cap=%d\n",len(b),cap(b))
	//修改原先数组的值,引用该数组的切片都会改变
	c:=a[1:3]
	fmt.Println(a)//[5 66 77 88 99]
	fmt.Println(c)//[66 77]
	a[1]=10000
	fmt.Println(a)//[5 10000 77 88 99]
	fmt.Println(c,b)//[10000 77] [5 10000 77 88]
	
}

切片还可再次被切割。

需要注意的一点是,如果切片操作超出了cap()的上限,将会导致一个panic异常,但是超出len()则是意味着扩展了slice,因为新slice的长度会变大。

func slice() {
	months	:=	[...]string{1:	"January",2:"February",3:"March",4:"April",5:"May",6:"June",7:"July",8:"August",9:"September",10:"October",11:"November",	12:	"December"}
	summer:=months[6:9]
	fmt.Println(summer[:20])	//	panic:	out	of	range
	endlessSummer	:=	summer[:5]	//	extend	a	slice	(within	capacity)
	fmt.Println(endlessSummer)//[June July August September October]
}

一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都
是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int,3)[3:]

切片的比较

切片不能直接比较,因为他是引用类型,可以和nil比较,可以理解为,一个nil值的切片没有底层数组,一个nil值的切片的长度和容量都是0,但是长度和容量都是0的切片不一定都是nil。

	s1:=make([]int,0)
	s2:=[]int{}

	fmt.Printf("s1=%v,len=%d,cap=%d\n",s1,len(s1),cap(s1))
	fmt.Printf("s2=%v,len=%d,cap=%d\n",s2,len(s2),cap(s2))
	fmt.Println(s1==nil)
	fmt.Println(s2==nil)
	/*
	s1=[],len=0,cap=0
	s2=[],len=0,cap=0
	false
	false
	 */

因此如果要判断一个切片是否为空,不能使用slice==nil,要使用len(s)==0来判断。

切片的赋值拷贝

拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容。也就是说切片和原数组任何一个被改变,对方也会受到影响,跟着改变。

func copySlice() {
	s := []int{1, 3, 5, 7, 9}
	s2 := s
	//修改切片的值
	for i := 0; i < len(s2); i++ {
		s2[i] = 0
	}
	fmt.Println(s2) //[0 0 0 0 0]
	fmt.Println(s)  //[0 0 0 0 0] 原先数组的值也会被改变

}

切片的遍历和数组一样,使用for i/range

切片的扩容

  • 首先判断,如果新申请的容量大于原先容量的2倍,最终容量就是新申请的容量。
  • 如果旧切片的长度小于1024,则最终容量就是之前的2倍
  • 如果旧切片的长度大于等于1024,最终容量增加为原先的1/4
  • 如果最终容量溢出,则最终容量就是新申请的容量

append可以帮助切片分配内存进行初始化

指针

指针保存的是变量的内存地址,通过&变量名,*变量,表示取出指针所指向内存地址中的值

new和make

  • 两者都是用来申请内存的
  • new一般用于基本数据类型的申请,string,int这些,返回类型指针(内存地址)
  • make是用来给slice,map,chain申请内存的,返回对应类型本身

map

map中的所有key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中key必须支持==比较运算符,所以map可以通过测试key是否相等来判断是否已经存在。

map是一种键值对类型的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

map类型的变量,初始值是nil,需要提供make来申请内存。cap表示初始容量

如果访问map中不存在的key,会返回对应的零值。

通过delete删除map中的指定key,如果key不存在,什么也不做。

map,slice,chain这些使用前一定要先通过make申请内存空间。

	//map 的声明 map在使用之前一定要先申请内存再添加数据,否则会报错
	m1:=make(map[string]int,10)

	//map 设置值
	m1["zhangsan"]=80
	m1["lisi"]=70
	m1["tom"]=90
	fmt.Println(m1)
	//map 判断key是否存在
	score,ok:=m1["jerry"]
	if !ok{
		fmt.Println("key not exit!")
	}else{
		fmt.Println(score)
	}
	//map 删除值
	delete(m1,"tom")
	fmt.Println(m1)

所有的这些操作都是安全,即使不存在也不会报错。对于不存在的key,map返回这个value类型的零值。但是map中的元素并不是一个变量,因此我们不能对map的元素进行取地址操作。还有就是随着元素数量的增加,需要分配更大的内存空间,从而可能导致之前的地址无效。

map的遍历可以通过for循环,map是无序的每次遍历的顺序可能都不一样。

func main() {
   ages:=map[string]int{
      "xifeng":19,
      "tom":22,
      "bing":24,
   }
   for    name,  age    := range  ages   {
      fmt.Printf("%s\t%d\n", name,  age)
   }
}

如果想要有序的输出map,可以先将key进行排序,然后依次从map中获取。

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合而成的实体,每个值被称为结构体的成员。结构体的成员可以通过点操作符进行访问和赋值。也可以对成员变量通过指针访问。

结构体的成员输入顺序具有重要意义,如果交换顺序,就是不同的结构体类型了。通常将相关的成员写到一起。

如果成员的名字是大写字母开头,那么该成员就是导出的,这是Go语言导出规则。

如果考虑效率,较大的结构体通常会用指针的方式传入和返回。如果是想要修改结构体成员变量,用指针传入是必须的。

结构体的比较

如果结构体的全部成员是可以比较的,那么结构体也是可以进行比较的,那样的话两个结构体将可以使用==或!=进行比较。

type	Point	struct{	X,	Y	int	} 
p	:=	Point{1,	2}
q	:=	Point{2,	1}
fmt.Println(p.X	==	q.X	&&	p.Y	==	q.Y)	//	"false" 
fmt.Println(p	==	q)			//false

匿名成员

Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就
叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构
体,同时Circle类型被嵌入到了Wheel结构体。

type Circle	struct	{ 
			Point
			Radius	int 
}
type Wheel	struct	{ 
			Circle
			Spokes	int 
}

等价下面的情况

package main

import "fmt"

type Employee struct {
	ID        int
	Name      string
	Address   string
	Postition string
	Salary    int
	ManagerID int
}

var dilbert Employee

type Point struct {
	X int
	Y int
}

type	Circle	struct	{
	Point
	Radius	int
}
type	Wheel	struct	{
	Circle
	Spokes	int
}

func main() {
	dilbert.Address = "beijing"
	dilbert.ID = 10086
	dilbert.Name = "jerry"
	dilbert.Postition = "backend"
	dilbert.Salary = 1000
	dilbert.ManagerID = 10000
	//需要注意的是Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于
	//结构体类型来说,将包含每个成员的名字。
    //main.Employee{ID:10086, Name:"jerry", Address:"beijing", Postition:"backend", Salary:1000, ManagerID:10000}
	fmt.Printf("%#v\n",dilbert)
	var w    = Wheel{
		Circle:    Circle{
			Point:        Point{X:    8, Y:    8},
			Radius:    5,
		},
		Spokes:    20, //	NOTE:	trailing	comma	necessary	here	(and	at	Radius)
	}
	fmt.Printf("%#v\n", w)
    
//main.Wheel{Circle:main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}, Spokes:20}
}

defer

defer修饰的函数或者语句,会在返回值赋值操作之后执行,当有多个defer修饰的语句或者函数时,会被放到栈中,先声明的最后执行。

在Go语言的函数中,return语句在底层不是原子操作,他会分给返回值赋值和RET指令两步,defer语句执行的时机就在返回值赋值操作之后,RET指令执行之前。

image-20210718222940918

函数内部变量的查找:先在函数内部找用到的变量,如果没有局部变量,就在全局中找;局部变量只能在函数内部使用。

由于defer语句延迟调用的特点,所以defer语句能非常方便的处理资源释放的问题,比如:资源清理,文件关闭,解锁以及记录时间等。

函数

函数的形参是实参的拷贝,go中只有值传递。对形参进行修改不会影响实参,但是如果实参包含引用类型,例如指针,slice,map,function,channel等,实参坑你会由于函数的简介引用被修改。

在go语言总,一个函数可以返回多个值。

func findLinks(url string) ([]string, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, fmt.Errorf("getting	%s:	%s", url, resp.Status)
	}
	doc, err := html.Parse(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil, fmt.Errorf("parsing	%s	as	HTML:	%v", url, err)
	}
	return visit(nil, doc), nil
}

在Go中,函数被看做第一类值:函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数中返回。对函数值的调用和函数没什么区别。

func power(n int) int {
	return n*n
}

func main() {
	f:=power
	fmt.Println(f(3))
	fmt.Printf("%T\n",f)// func(int) int
}

函数类型的零值是nil,调用值为nil的函数值会引起panic错误。

函数之间是不可比较的,也不能用函数值作为map的key。

匿名函数

函数值字面量是一种表达式,他的值被称为匿名函数,在使用函数的时候,再定义他。

func squares() func()int {
	var x int
	return func() int {
		x++
		return x*x
	}
}

func main() {
	sq:=squares()
	fmt.Println(sq())//1
	fmt.Println(sq())//4
	fmt.Println(sq())//9
}

在squares中定义的匿名内 部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引 用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实
现函数值,Go程序员也把函数值叫做闭包。

通过匿名函数的例子可以说明变量的生命周期不由他的作用域决定。

内置函数

  • close:主要用来关闭channel
  • len:用来求长度,string,array,slice,map,channel
  • new:用来分配内存,主要用来分配值类型,int,struct。返回的指针。
  • make:用来分配内存,主要用来分配引用来兴,比如chan,map,slice
  • append:用来追加元素到数组,slice
  • panic/recover:用来错误处理

Go语言目前没有异常机制,但是使用panic/recover模式来处理错误,panic可以在任何地方引发,但是recover只有在defer调用的函数中有效。

注意:

  • recover()必须搭配defer使用
  • defer一定要在可能引发panic的语句之前定义

error

内置的error是接口类型,error有可能是nil或者non-nil,nil意味着函数运行成功,否则就运行失败。在go中,函数运行失败的时候会返回错误信息,这些错误信息被认为是一种预期的值而不是异常,这就是go和java在异常机制上的区别,但这些机制仅被使用在处理哪些未被预料到的错误也就是bug。

处理策略

  1. 传播错误,一旦出现失败,就代表该函数的失败。将错误信息返回给调用者
  2. 如果错误是偶然性的,或者是不可预知的,那就重新尝试失败。
  3. 如果错误出现后程序无法继续运行,这时可以输出错误信息并结束程序,这种策略只在main中进行。
  4. 通过log输出错误信息,并不停止程序。
  5. 直接忽略错误。

接口

很多面向对象的语言都有相似的接口概念,但是Go语言中接口类型的独特之处在于他是满足隐式实现的。也就是说,没必要对于给定的具体烈性定义所有满足的接口类型。

接口的约定

接口类型是一种抽象的理性,他不会暴露出对象的内部值的结构和这个对象支持的基础操作的集合,他们只会展示出他们自己的方法,也就是说你不知道他是什么,但是你知道他是做什么的。

接口类型

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

type Reader interface {
	Read(p []byte) (n int, err error)
}
type Writer interface {
	Write(p []byte) (n int, err error)
}


//组合接口
type ReadWriter interface {
	Reader
	Writer
}

实现接口的条件

一个类型如果有一个接口需要的所有方法,那么这个类型就实现了这个接口。Go的程序员经常会简要的把一个具体的类型描述成一个特定的 接口类型。举个例子, *bytes.Buffer 是io.Writer; 是io.ReadWriter。

空接口类型对实现他的类型没有要求,所以我们可以将任意一个值赋值给空接口类型。

var any interface{} 
any = true
any = 12.34
any = map[string]int{"one":1}
fmt.Println(any)

每个具体类型的组基于他们相同的行为可以表示成一个接口类型,不像基于类的语言,他们一个类实现的接口集合需要进行显示定义,在Go语言中我们可以在需要的时候定义一个新的抽象或者特定特点的组,而不需要修改具体类型的定义。

接口值

接口值被称为接口的动态类型和动态值。一个具体的类型和那个类型的值。go是静态类型语言,类型是编译期的概念。

在Go语言中,变量总是被一个定义明确的值进行初始化,即使接口也不例外。对于一个接口的零值就是它的类型和值部分都是nil。

image-20211231120119442

上图是一个空的接口值,你可以通过使用w==nil或者w!=nil来判断接口值是否为空。调用一个空接口值上的任意方法都会产生panic。

两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因此接口值是可比较的。如果两个接口值的类型相同但是这个类型不可比较(例如slice,函数)因此将会比较失败并抛出panic。

var	x interface{}	=	[]int{1,	2,	3}
fmt.Println(x	==	x)	//	panic:	comparing	uncomparable	type	[]int

一个不包含任何值的nil接口和一个包含nil指针的接口值是不相同的。

表达式求值

构建一个简单的算术表达式求值器。

package main

import (
	"fmt"
	"math"
)

type Var string
type literal float64
type Env map[Var]float64
type unary struct {
	op rune
	x  Expr
}

type binary struct {
	op   rune
	x, y Expr
}
type call struct {
	fn   string
	args [] Expr
}

type Expr interface {
	Eval(env Env) float64
}

func (v Var) Eval(env Env) float64 {
	return env[v]
}

func (l literal) Eval(_ Env) float64 {
	return float64(l)
}

func (u unary) Eval(env Env) float64 {
	switch u.op {
	case '-':
		return -u.x.Eval(env)
	case '+':
		return +u.x.Eval(env)
	}
	panic(fmt.Sprintf("unsupported unoperator:%q", u.op))
}

func (b binary) Eval(env Env) float64 {
	switch b.op {
	case '+':
		return b.x.Eval(env) + b.y.Eval(env)
	case '-':
		return b.x.Eval(env) - b.y.Eval(env)
	case '*':
		return b.x.Eval(env) * b.y.Eval(env)
	case '/':
		return b.x.Eval(env) / b.y.Eval(env)
	}
	panic(fmt.Sprintf("unsupported unoperator:%q", u.op))
}

func (c call) Eval(env Env) float64 {
	switch c.fn {
	case "pow":
		return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))
	case "sin":
		return math.Sin(c.args[0].Eval(env))
	case "sqrt":
		return math.Sqrt(c.args[0].Eval(env))
	}
	panic(fmt.Sprintf("unsupported	function	call:	%s", c.fn))
}

类型断言

类型断言是一个使用在接口值上的操作,x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查他操作对象的动态类型是否和断言的类型匹配。

第一种,如果断言的类型T是一个具体的类型,然后类型断言检查x的动态类型是否和T相同,如果判断失败就会抛出panic。

	var w io.Writer
	w= os.Stdout
	f:=w.(*os.File)
	fmt.Println(f)
	c:=w.(*bytes.Buffer)// panic interface conversion: io.Writer is *os.File, not *bytes.Buffer
	fmt.Println(c)

第二种,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法的集合,但是它保护了接口值内部的动态类型和值的部分。

var	w	io.Writer 
w	=	os.Stdout
rw	:=	w.(io.ReadWriter)	//	success:	*os.File	has	both	Read	and	Write 
w	=	new(ByteCounter)
rw	=	w.(io.ReadWriter)	//	panic:	*ByteCounter	has	no	Read	method

如果断言的接口是一个nil,那么不论断言类型是什么这个类型的断言都会失败。我们几乎不需要对一个更少限制性的接口类型做断言。

使用专门的类型来描述结构化的错误。os包中定义一个pathError类型来描述在文件路径操作中涉及到的失败,并定义LinkError的变体来描述涉及到两个文件路径的操作。

import (
   "errors"
   "fmt"
   "os"
   "syscall"
)

// 基于类型断言识别错误类型
var ErrorNotExist = errors.New("file not exist")

func IsNotExist(err error) bool {
   if pe,ok:=err.(*os.PathError);ok{
      err =pe.Err
   }
   return err==syscall.ENOENT||err==ErrorNotExist
}

func main() {
   _,err:=os.Open("/no/such/file")
   fmt.Println(os.IsNotExist(err))//true
}

并发编程

Goroutines

在Go语言中,每个并发的执行单元叫做一个goroutines,可以类比其他编程语言中的线程理解。当一个程序启动的时候,主函数在一个单独的goroutines中运行,我们叫他main goroutine。新的goroutine会用go语句来创建。

主函数返回时,所有的goroutine都会被打断,程序退出。除了子函数退出和程序终止之外,还由一种方法就是goroutine之间进行通信也可以终止执行。

func fib2(x int64) int64 {
   if x < 2 {
      return x
   }
   return fib2(x-1)+fib2(x-2)
}
func spinner(delay time.Duration) {
   for    {
      for    _, r  := range  `-\|/` {
         fmt.Printf("\r%c", r)
         time.Sleep(delay)
      }
   }
}
func main() {
   go spinner(100*time.Millisecond)
   n:=fib2(45)
   fmt.Println(n)
}

runtime.GoExit():终止所在协程。

func test() {
   defer fmt.Println("ffffffffff")
   runtime.Goexit()
   fmt.Println("eeeeeeeeeeeeeee")
   //终止所在协程
}
func main() {
   go func() {
      fmt.Println("aaaaaaaaaaa")
      test()
      fmt.Println("bbbbbbbbbbbbbbbbb")
   }()
   for i := 1; i > 0; i=1{

   }
}

输出结果:

aaaaaaaaaaa
ffffffffff

runtime.GOMAXPROCS():设置CPU最大核心数,并返回之前设置的值。

channel类型

通过make创建的引用,channel的零值也是nil,方法参数类型是channel时,调用者和被调用者将引用同一个channel对象。

func main() {
   // channel的创建
   ch:=make(chan string)
   defer fmt.Println("main协程调用完毕...")
   go func() {
      defer fmt.Println("子协程调用完毕...")
      for i := 0; i < 2; i++ {
         fmt.Println("子协程:",i)
         time.Sleep(time.Second)
      }
      //输入数据
      ch<-"game over"

   }()
   //从管道中取出数据
   str:=<-ch
   fmt.Println(str)
}

无缓冲的channel是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作,如果两个goroutine没有同时准备好,通道会导致先执行发送或接受操作的goroutine阻塞等待。

通道的发送和接收行为是同步的,其中任意一个操作都无法单独存在。

有缓冲的通道再创建的时候,传入capacity。他要等到缓冲区写满之后才会进行读取。

ch	=	make(chan	string,	3)// 缓冲区大小为3
fmt.Println(cap(ch))	//	"3"
ch	<-	"A" 
ch	<-	"B"
fmt.Println(len(ch))	//	"2" 返回channel内部缓存队列中有效元素的个数

向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部 删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收 操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个 goroutine执行发送操作而向队列插入元素。

单通道生产者消费者

package main

import (
	"fmt"
)

func producer(out chan<- int) {
	//out 引用传递
	for i := 0; i < 10; i++ {
		out <- i * i
	}
	close(out)
}
func consumer(in <-chan int) {
	//从channel中取出数据输出
	for e := range in {
		fmt.Println(e)
	}

}

func main() {
	ch := make(chan int)
	go producer(ch)
	consumer(ch)
}

Timer

Timer是一个定时器,代表未来的一个单一事件,你可以告诉timer要等待多长时间,它提供一个channel,在将来固定时间后向channel中输入一个时间值。timer只能执行一次。

func main() {
	//创建定时器,2秒后定时器就会向自己的C通道中发送一个当前的时间戳
	timer := time.NewTimer(2 * time.Second)
	fmt.Println("start时间:",time.Now())
	t1:=timer.C
	fmt.Printf("t1时间::%v\n",<-t1)

	//单纯等待,什么也不做
	timer2:=time.NewTimer(2*time.Second)
	<-timer2.C
	fmt.Println("2s后",time.Now())

}

结果输出

start时间: 2022-01-02 11:24:37.9578473 +0800 CST m=+0.003826801
t1时间::2022-01-02 11:24:39.9707637 +0800 CST m=+2.016743201
2s后 2022-01-02 11:24:41.9723786 +0800 CST m=+4.018358101

延时程序的三种方法

func delay() {
   // 第一种直接睡眠固定时间
   time.Sleep(2*time.Second)
   // 第二种通过timer
   <-time.NewTimer(2*time.Second).C
   // 第三种 通过timer.After
   <-time.After(2*time.Second)
}

停止定时器:time.Stop()

重置定时器时间:time.Reset()

Ticker

Ticker是一个定时触发计时器,周期性的执行任务,因此channel的接受者可以在指定时间内从中读数据。

func main() {
   ticker := time.NewTicker(time.Second)
   for i := 0; i < 100; i ++ {
      //每隔1s输出
      <-ticker.C
      fmt.Println(i)
   }

}

select

Go中的select可以监听channel上的数据流动。

select的用法与switch语言非常相似,由select开始一个新的选择块,每个选择条件由case语句来描述。

每个case都是一个针对channel的IO操作。

func fibonacci(ch chan<-int,quit <-chan bool)  {
	x,y:=1,1
	for {
		select {
		//x写入channel
		case ch<-x:
			x,y=y,x+y
		case flag:=<-quit:
			fmt.Println(flag)
			return
		}
	}
}

func main() {
	ch:=make(chan int)
	quit:=make(chan bool)
	go func() {
		// 输出8次斐波那契结果
		for i := 0; i < 8; i++ {
			//当ch有数据写入完成后,就取出数据并输出
			num:=<-ch
			fmt.Println(num)
		}
		quit<-true
	}()
	//主协程先开始向ch中输入x
	fibonacci(ch,quit)
}

posted @ 2022-01-04 10:33  起个名字都这么男  阅读(294)  评论(0编辑  收藏  举报