基础语法
前言
我是一个小白,有一定的python基础,初学GO语言,本学习笔记为我学习之余总结出来的,由于水平有限可能会有很多疏漏和错误,恳请指正!
变量声明和赋值
GO中有三种声明方式,第一种为正常声明变量类型(记住在Golang中所有变量声明就必须要使用!声明又不确定用不用的话最简单的输出一下也好):
var a int
a = 10
第二种为推导式,即不声明变量的类型,直接赋值,由系统自己判断值的类型
var a = "20" //系统自己判断出变量类型是string
第三种为简短式,但是这种只能在函数内部进行声明,对于只在函数内部使用的变量使用比较广泛
func main() {
a := 1 //简短声明使用方法,类型为int
}
这里需要注意,每个英文字母占一个字节,每个中文占三个字节
"_"是哑元变量,所有你不想要的变量都可以丢给他,相当于垃圾桶,在for循环的讲解里会介绍用法
常量的声名赋值
常量不要求声名必须使用但是常量也无法进行修改,而且需要在函数外部声名,下面是定义常量的最基本方法:
const a = 123
需要大量定义常量,则可以使用批量定义
const( //一种定义方式
a1 = 1
b1 = 2
c1 = 3
)
const(
a1 = 1 //如果a1,b1,c1你都想赋值为1,那么只需要给最上面的变量进行赋值
b1
c1
)
iota
这个可以作为扩展内容,iota是在常量内的一个标志,每出现一次const,iota就会被清零,在一个常量内,每出现一行赋值,iota就会加一
const(
a1 = iota //每次const出现都将iota记为0
b1 = iota //新加了一行赋值,iota加一
c1 = iota //到这里iota的值已经加到2
)
const(
a2 = iota //const再次出现,iota重新记为0,但不影响前一个const内iota的计数
b2 //iota也支持批量赋值
c2
)
const(
a1,a123 = iota,iota //这里虽然出现两次赋值,但是这两个都在同一行,所以这两个iota都是0
b1 = iota //新加了一行赋值,iota加一
c1 = iota
)
变量的类型
变量类型主要有int(整形)、float(浮点型)、bool(布尔型,只有true和false)、string(字符串类型) ***注意如果只声明不赋值,那么变量里存的是0值(int对应0,string对应"",bool对应false)***
变量的进制
变量值以0开头的是八进制,0x开头的是十六进制
a := 011 //八进制的数字
b := 0x11 //16进制的数字
这说一下怎么输出相应进制:用format进行字符串格式化
a := 10
fmt.PrintF("%d",a) //输出十进制的a
fmt.PrintF("%b",a) //输出二进制的a
fmt.PrintF("%d",a) //输出八进制的a
fmt.PrintF("%o",a) //输出十六进制的a
//既然都说到字符串格式化了,就顺便记一下常用的格式化操作符
fmt.PrintF("%s",a) //字符串
fmt.PrintF("%T",a) //输出a的变量类型
fmt.PrintF("%v",a) //输出a的值,无视变量类型
字符串的拼接和拆分
要使用拼接功能(包括显示),都需要导入一个库"fmt",将两个字符串拼接的函数时fmt.SPrintf(), 拆分是strings.Split,具体使用方法如下:
a := "123"
b := "456"
c := fmt.Sprintf("%s%s",a,b) //字符串拼接,返回为一个变量值
d := "12131415"
f := strings.Split(d,"1") //字符串的分隔(根据字符中的1进行分割),返回一个数组
控制语句
if语句
if语句可以进行条件判断并执行相应的语句:
a := 18
if a > 10{ //go中if判断语句,不用加括号
fmt.Println("a 大于10") //a大于10 ,会执行这一句
}else{
fmt.Println("a 小于10")
}
if a > 10{
fmt.Println("a 大于10") //a大于10 ,会执行这一句
}else if a > 5{ //如果想进行多次判断写else if
fmt.Println("a 大于5") //同时满足两个条件只会执行上面的
}
switch
当有多个判断条件时,推荐使用switch
a := 5 //switch语句
switch a{
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
case 3:
fmt.Println("3")
default: //当以上语句都不成立时执行default,相当于else
fmt.Println("no")
switch a:=6; a{ //switch变种, a的作用域只在该switch里面
case 1,5,6:
fmt.Println("1")
case 2:
fmt.Println("2")
case 3:
fmt.Println("3")
default:
fmt.Println("no")
}
for
for语句可以循环执行括号内的语句
for a:= 0; a<10; a++{ //for语句,不用加括号
fmt.Println(a) //会输出0到9
}
s := "哈哈哈" //遍历字符串和索引,注意中文占三个字节
for i,v := range s{ //i为索引,v是值,注意这里i和v都要输出
fmt.Println(i, string(v))
}
//输出样式如下,因为一个中文占三个字节,所以会出现0,3,6
0 哈
3 哈
6 哈
for _,v := range s{ //v是值,"_"为哑元变量,将字符串的索引丢给它,就可以不用输出该变量
fmt.Println(string(v))
}
for 循环里有两个跳出语句, break和continue,break可以直接终止此循环,continue则是跳过此次循环直接开始下一次循环
for i:=1; i<=10; i++{
fmt.Println("yes")
if i == 5{
fmt.Println("I=5")
break
}
数组
定义数组
和定义变量类似,但是数组的定义需要加上数组的长度
var a [3]bool //数组如若不填写其中的值,内部都是0值
var b [4]bool //数组长度和元素类型是数组类型的一部分,比如数组a和b虽然元素类型一样,但是长度不一样,所以这两个数组的类型不一样
//给数组复制的方法
a = [3]bool {true,true,true} //初始化的一种方式,用于知道有几个元素
c := [...]int{1,2,32,3,34,5,5,32,65,2356,1} //初始化的另一种方式,用于不知道到底有多少个元素
遍历数组
我们可以用for循环来遍历数组
c := [...]int{1,2,32,3,34,5,5,32,65,2356,1}
for i:=0; i<len(c); i++ { //根据索引遍历列表
fmt.Println(c[i])
}
for v,i := range c{ //用range遍历数组,这里也可以用哑元变量来接受索引值
fmt.Println(v,i)
}
二维数组
二维数组的定义:
d := [2][2]int{{1,2},{3,4}} //二维数组
fmt.Println(d)
切片
其实切片一定程度来说也是数组,但是考虑到知识比较多,而且也有其特殊性,所以把它放到这里来单独记一下
切片的定义
切片有若干个定义方法,直接上代码
a := [...]int{1,3,5,7,9}
c := a[1:3] //由数组得到切片,左闭右开 切片的容量等于原数组的容量,长度等于切之后的长度
fmt.Println(len(c),cap(c)) //len表示数组a的容量,cap表示a的容量(因为a是切片所以容量和长度相等)
b := []int{1,2,3,4,5} //直接定义切片的一种方式
var d []int //定义一个int元素类型的切片,切片里面有多少元素就有多少容量,默认为空,里面没有东西
var e = make([]string,5,10) //创建一个长度为5,容量为10,元素类型为string的切片,里面不是空
注意:GO里切片只是指向底层数组的内存地址把这个范围框起来用,并不是独立的数组,区别于python,python的切片是成为一个独立的数组。但是两个数组相等是变成一个全新的数组,原数组的变化并不影响新数组,区别于python,两个正好反过来。例外:但是如果新的变量等于一个切片,那么如果切片变化,这个新数组也会变化
切片增加删除元素
切片可以直接加入元素,但是如果想删除元素就只能在通过切片再拼接的方式。
a = []int{1,2,3,4,5}
b := []int{1,2,3,4,5}
c := []int{7,8,9,0}
b = append(b, 6) //append 必须用变量接收,当原数组放不下时候,GO会重新分配内存地址
b = append(b, 6) //扩容策略,如果原数组长度小于1024,则容量翻倍,如果大于1024,则循环增加1/4直到最终容量大于新申请的长度。如果新申请的容量大于原容量的两倍,则新容量等于新数组的长度
fmt.Printf("b = %v,len = %d, cap = %d",b,len(b), cap(b))
b = append(b,c...) //...表示将c数组的每个元素拆开,依次放入b中
a := []int{1,2,3,4,5}
a = append(a[:1], a[2:]...) //...表示将c数组的每个元素拆开,依次放入b中
--------------------------------------------------------------------------------------------
a := [...]int{1,2,3,4,5,6,7,8} //对切片数组进行增删,实际上是操作原数组,原数组被删除元素后面会前移,但是后面整体不变
b := a[:] //难点!这部分需要自己动手操作理解
b = append(b[:1],b[4:]...)
指针
虽说GO中也有指针,但是比C的要简单,只有&(取地址)和*(根据地址取值)两个符号
n := "str"
n1 := &n //得到n的内存地址,指针的类型和存储变量的类型一样
fmt.Printf("%T",&n) //因为原变量的类型为string,所以指针的类型也是string
m := *n1 //将这个内存地址的值取出来
fmt.Printf("m:%v",m)
定义指针
var a *int //定义一个空指针(nil pointer)但是此时取值会报错 ***很少用***
fmt.Println(a)
var b = new(int) //申请一个有地址的指针
fmt.Println(b)
*b = 100 //给该地址进行赋值
fmt.Println(*b)
new和make
这里关于new和上面数组哪里提到的make需要注意:
make和new都是用来申请内存的,new很少用,一般都是给基本数据类型(string,int)等用,new返回的是对应类型的指针(给string类型申请指针得到的是string)。make是给切片(slice)、chan、表(map),make返回的是三个类型对应的本身(即切片还是切片不变)
*
map
map是一种无序的键值相对应的数据结构,map里每个元素都包含一对相对应的键值对(类似于python的字典)
定义map
var a map[string]int //定义一个键的类型为string型,值的类型为int型的map
注意此时的map里面没有任何的值也没有内存空间,不能存储任何数值。如果想让其能存储数值,需要用make()对它进行初始化
map的初始化
a = make(map[string]int ,10)//在初始化时尽量估算map的大概容量避免二次扩容
map赋值
map可以通过指定键来给键赋值,前提是键值的类型都符合规定的类型
var a map[string]int
a = make(map[string]int)
a["你好"] = 123 //你好是键,123是值
输出map
这里我分为输出特定键指定值和遍历输出整个map
输出指定键的值可以直接这样写
//借用上面代码块的a
fmt.Println(a["你好"]) //此时会输出你好对应的值 123
但是此时有一个问题,如果map中没有指定的键怎么办,这时我们可以用一个额外的变量ok(叫什么都可以,不过一般都是这么叫)的状态标志来检测map中是否有这个键:
//还是借用上面的a
v,ok := a["我也好"] //ok此时是一个布尔值,其标志着a中是否有这个键,有的话返回true
if ok{
fmt.Println(v) //如果有就返回这个值
}else{
fmt.Println("查无此值")
}
遍历输出map和输出列表相似,都是使用for循环来进行输出
//依旧借用上面的a
for key,value := range a{ //不同于遍历数组,这里的key是map的键而非索引
fme.Printf("%v:%v"key,value)
}
删除map元素
删除map元素比删除切片元素方便多了,直接用delete即可
//继续借用上面的a
delete(a,"你好")//根据键来删除这个键值对,如果没有这个键则不执行这句话
map指定顺序输出
由于map是无序的,所以想要将它有序输出是非常麻烦的,需要先将map中的键都拿出来放到一个切片中,再将这个切片进行排序,在通过这个已经排好序的键来输出对应的值:
//新建一个map吧还是
var a = make(map[int]string) //新建一个map顺便直接将其初始化
for i:=0; i<100; i++{
b:= rand,Intn(100) //生成一个100内的随机数当值用
a[i] = b
} //这个循环用完之后,这个map里面就会有100个键值对了
keys := make([]int ,len(map)) //新建一个slice,长度等于map长度用于存放键
for i,_ : = range a{
keys = apped(keys,i) //用append将每个键都放入slice中
} // 循环结束后,map中的所有键都会放进这个slice中
sort.Ints(keys) //将这个slice进行排序(里面都是数字,所以用Ints)
for _,key := range keys{ //_接受的是slice的索引
fmt.Println(key, a[key]) //根据排序好的键输出对应的值
}
定义一个元素类型为map的切片
之所以会说这个是因为这里有一个易错点,我们都知道定义一个个切片或者map都需要进行初始化,但是如果你定义了一个元素类型为map的切片,就需要同时对切片和里面的每个map元素进行初始化。
//注意此时是我们只对切片进行了初始化,里面的map元素还没有
var a := make([]map[string]int,10) //定义一个元素类型是map,长度为10的切片,而且里面每个map的键都是string,值是int类型
//下面对每个map进行初始化
for i,_ := range a{
a[i] = make(map[string]int) //通过索引定位到每个map进行初始化
}
a[0]["你好"] = 123 //这时就可以为每个map进行赋值了,0是切片的索引,"你好"是索引对应切片的键
函数
函数是减少代码冗余,增加可读性的一个重要的结构体
函数的定义
GO中函数定义有多种变体,下面列举几个常用的
func a(){ //最常用的一个函数定义放大,a是函数名称,没有参数和返回值
fmt.Println("你好")
}
func b(name string, age int){ //一个有参数但是没有返回值的函数,go中参数也需要指明类型
fmt.Printf("name:%s,age:%d",name,age)
}
func sum(x int, y int)(ret int){ //一个有返回值的函数,返回值也要指定类型,但是返回值可以不命名,一旦命名了,go会自动在函数内部创建这个名字的变量
ret = x+y
return ret
}
func sum2(x,y int)int{ //如果多个连续的参数类型一样,那么可以只指定最后一个变量的类型
return x+y
}
func sum3 ()(int, int){ //可以有多个返回值,也可以只有返回值没有参数
return 1+2, 2+3
}
func sum4(x string, y ...int){ //...表示i可以接受多个值,但是这个参数只能放在参数的最后,其次这些纸也需要指定类型(这个函数指定的是int),这些多个参数会被整合成一个切片,y也可以不传递值
fmt.Println("x:%s, y:%v",x,y)
}
func main(){
b("小明",12) //调用a,因为a需要参数,所以括号内需要加参数
}
这里需要注意:
1.函数里面不能定义新函数
2.函数既可以作为参数被传递,也可以返回值被返回,如果这个函数没有返回值,则这个函数的类型就为func(),如果有返回值(比如是int),则这个函数的类型就为func()int
3、Go语言中函数没有默认参数
匿名函数
匿名函数适用于只用一次的场合,快速创建,用完即丢,匿名函数不用起名字,但是必须在函数内部创建
定义也分两种:
func lambda(){
func(){
fmt.Println("你好")
}() //快速执行,直接在函数后面加括号
a:= func(){
fmt.Println("你好")
}
a() //一般定义方式,调用运行就好
}
结构体
自定义类型和别名
自定义类型和别名链两个在定义时候非常相似,但是这两个有根本上的不同,自定义类型是将某个类型的名字改变
type myint int //将int类型的名字变为myint
var a myint //以后定义int类型这样就可以了
a = 100
fmt.Printf("%T",a) //这样输出的类型是main.myint
类型别名是给某个类型起一个别名,在用的时候这两个都可以用
type yourint = int //注意这里用了一个等号,区别就在这里
var a yourint
var b int
a = 100
b = 200
fmt.Printf("%T",a) //注意这里输出a的类型还是int,又是一个和自定义类型不同的地方
fmt.Printf("%T",b)
结构体
由于GO并不是面向对象语言,所以没有类这个概念,但是为了实现这一功能,结构体这个概念那就应运而生,它可以实现和类差不多的功能,比如新建一个对象,初始化对象等
type dog struct{ //定义一个结构体,名字是dog
name string //定义结构体里面的参数,也需要指明类型
age int
}
var a dog //建一个dog类型的变量,类似于创建一个对象
a.name = "小白" //为结构体内的参数传值
a.age = 3
至此,一个结构体就创建好了,结构体也可以作为一个参数被传进函数中,但是这里有一点需要注意,任何被传进函数的变量都只是一个复制体,在函数内部更改该变量并不会影响函数外的该变量
//用一下上面的结构体变量a
func test(x dog){ //注意整理传进来的变量的类型也要是对应结构体的类型
x,name = "小黑"
}
func main(){
fmt.Println(a.name)
test(a)
fmt.Println(a.name)
}
此时运行会发现,虽然我们在test()中修改了a的name,但是输出a.name还是没有变化,证明了函数传进的值是一个复制体,和原变量是互不影响的
如果想影响原变量,那么就要传进该变量的指针即内存地址
//还是用一下a
func test(x *dog){ //注意这里传进去的是指针
x.name = "小黑"
}
func main(){
fmt.Println(a.name)
test(&a)
fmt.Println(a.name) //这是在看就已经变了
}
会变化是因为我们通过指针找到了这个变量的内存地址,直接修改该内存地址上的该变量,就可以达到修改原变量的目的
构造函数
构造函数可以让我们更加快捷的新建一个结构体,也是一个函数,只不过返回的是一个结构体
type person struct{
name string
age int
}
//构造函数的命名一般都是new + 结构体的类型
func newPerson(name string age int)person{ //传进去的参数是结构体需要的元素
return person{ //这里是大括号
name :name, //逗号不能忘
age: age,
}
]
func main(){
a := newPerson("小白",20) //要用变量接收
fmt.Println(a)
}
这里需要注意,如果结构体东西比较少,直接返回结构体也可以,但是如果东西比较多,那返回结构体的指针更好一些
type person struct{
name string
age int
}
func newPerson(name string, age int) *person{ //返回指针
return &person{ //取出person的地址
name : name,
age : age,
}
}
func main(){
a := newPerson("小白",20)
fmt.Println(*s) //根据地址取值,输出a
}
方法
方法时作用于特定类型的函数,类似于类中的函数
type dog struct {
name string
age int
}
func (d dog)talk(){ //方法,在函数名前面添加括号,代表只有括号中的东西可以使用这个函数,dog是一个结构体,d是代表dog的一个函数,一般都用类型的首字母小写
fmt.Printf("%s在汪汪叫",d.name) //直接使用dog结构体中的参数name
}
func main(){
a := dog{"你",12} //新建一个结构体变量
a.talk() //可以使用这个方法
}
匿名结构体
匿名结构体适用于结构体字段比较简单,而且结构体使用常见不复杂的情况下,方便好用
var cat struct { //定义匿名结构体
string //直接用类型替代字段名称
}
func lambda_struct(){
p1 := cat{
"小喵"
}
fmt.Println(p1,string) //查找字段时直接找到对应的字段类型
}
这里需要注意的是没如果一个匿名结构体有两个字段是相同类型是不可以的,就不能用匿名结构体了
结构体嵌套
结构体之间可以进行相互嵌套
type detailAdress struct{
street string
home string
}
type address struct { //将详细地址的结构体嵌套到地址结构体里
province string
city string
town string
detail detailAdress
}
//用构造函数新建一个结构体,由于结构体太复杂, 所以返回指针
func newAdress(province, city, town, street, home string, ) *address{
return &address{
province: province,
city: city,
town: town,
detail: detailAdress{
street: street,
home:home,
},
}
}
嵌套匿名函数
字面意思就是将匿名函数嵌套到其他结构体中
type detailAdress struct{
street string
home string
}
type address struct { //将详细地址的结构体嵌套到地址结构体里
province string
city string
town string
detailAdress //由于匿名结构体没有名字,这里直接写类型名称即可
}
//用构造函数新建一个结构体,由于结构体太复杂, 所以返回指针
func newAdress(province, city, town, street, home string, ) *address{
return &address{
province: province,
city: city,
town: town,
detailAdress: detailAdress{ //这里直接用匿名结构体的类型代替名字
street: street,
home:home,
},
}
}
结构体模拟实现继承
因为GO中没有面向对象没有类,所以这里的继承只是在功能上达到对继承的实现
type animal struct{ //定义一个结构体,这里是被继承的对象
name string
}
type (a animal) doSomething(){ //为animal创建一个方法
fmt.Printf("%s在叫!", a.name)
}
type dog struct{ //定义另一个结构体,想让这个结构体继承animal的属性和功能
feet int
animal //直接将animal作为匿名结构体传进dog中
}
type (d dog)countFeet(){
fmt.Printf("这只狗有%d条腿",d.feet)
}
func main(){
s := dog{ //新建一个dog,注意这时也要将animal一起定义
feet : 4,
animal:animal{
name:小白
},
}
//这时就会发现s可以同时用到dog和animal的所有字段了
fmt.Printf("%s在用%d条腿跑步!", s.name, s.feet)
s.doSomething() //s也可以用animal的所有方法
}
结构体和json
json是前端和后端的一种沟通桥梁,前端会用js,但是后端会用各种语言,java,python,go等等,所以需要一种中意的格式来规定前后端的传输信息,那就是json
结构体和json的合适有些类似,都键值一一对应,所以将结构体转为json是最合适的了
将结构体和json相互转化的过程叫做序列化和反序列化
tpye person struct{ //定义一个结构体
Name string `json:"name"` //结构体只能被当前包使用,如果想让json报访问,那就需要将首字母大写
Age int `json:"name"` // 用反引号可以指定骑在对应包里的名字
}
//将结构体转换成json
func struct_to_json(){
a:= person{
name:"小白",
age:18,
}
//这里需要点用到json报,欣慰json是外部包,不能直接访问这个包内的结构体,所提需要将person的字段首字母大写
p,err := json.Marshal(a)//用Marshal()将a转换成json格式,err为转换是否陈宫的一个值,如果转换成功则为nil
if err != nil{
fmt.Println(p)
}else{
fmt.Println("转换失败")
}
}
//将json转换成结构体
func json_to_struct(){
var p1 person //先定义一个结构体进行接收
json_data := `{"name":"小白","age":18}` //这里的键名必须要和结构体里面的键名字一样,不然会输出空值
json.Unmarshal([]byte(json_data),&p1)//欣慰函数只会对参数的复制体进行操作,所以需要直接指针传入参数,直接修改变量
fmt.Printf("%#v",p1)//%v只输出值,%+v先输出类型,在输出值,%#v输出类型和结构体的字段和值
}
接口
定义接口
接口是一种类型
type dog struct{} //定义一个狗的结构体
func (d dog)speak(){ //给狗加一个方法
fmt.Print("汪汪汪")
}
type cat struct{} //定义一个人的结构体
func (d dog)speak(){ //给人家一个方法
fmt.Print("喵喵喵")
}
type person struct{} //定义一个人的结构体
func (d dog)speak(){ //给人家一个方法
fmt.Print("啊啊啊")
}
这三个结构体都有一个speak(),但是这三个结构体的方法都只能被自己特定的接收者执行,是否可以有一种方法可以同时调用这三个方法呢,这就是接口的作用
//接上面的代码
type speaker interface{ // 定义一个接口,这个接口只要求穿进去的结构体有这个方法
speak()//只要实现了speak()方法就可以说是speaker类型
}
func hit(a speaker){
a.speak() //执行speaker接口的speak()方法
}
func main(){
cat := cat{}
hit(cat) //调用hit方法,传入任意一个有speak()方法的结构体
}
空接口
空接口可以不用起名字,适用于不知道传进的变量是什么类型
a := make(map[string]interface{},10) //初始化一个map,空接口interface{}可以放进任何类型的值
//任何类型的都可以塞进空接口
a["123"] = 123
a["456"] = "asd"
a["asd"] = people{
name:"小白",
age:21,
}
空接口固然好用,打破了必须传入某一种特性的值的解锁,但是GO是一种强类型的语言,如果传入一个值不符合规定会报错(比如我想让两个int相加,但是空接口传入一个string,会报错),这时就需要有一种东西判断传进来的值到底是什么类型,就是类型断言
//类型断言1
func do(a interface{}){
s,OK := a.(string) //可以尝试将a转换为string类型,OK为是否转换成功
if OK{
fmt.Println(a)
}else{
fmt.Println("转换失败!")
}
}
//类型断言2
func do(a interface{}){
switch v := a.(type) //得到a的类型
case int:
fmt.Println("是int类型")
case string:
fmt.Println("是string类型") //只写这两个例子
}
包package
调用包
一个文件夹下的所有go文件的package名字都要是上一级的文件名字,根目录下的包名字是main
如果想要让这个文件的内部标识符(比如函数名,结构体等)对外部文件可见,需要对标识符的首字母进行大写
同一个包内的文件相互调用表示负不用导入包和写包名字,但是同样需要对标识符首字母大写
//在这里新建一个文件夹right,在文件夹里新建一个go文件,这里的package名字就是right
package right
import "fmt"
func Add(x,y int)int{ //这里如果想让外部的包可见这个函数,就要
return x+y
}
func init(){
fmt.Println("asdas")
}
这是这个包就可以被其他包所调用了,注意如果一个包里有多个go文件,那么被导入的时候这些文件里的标识符都会被导入进去
import (
eighth "awesomeProject/right" //导入包可以为他制定一个别名
"fmt"
_ "awesomeProject/right" //匿名导入,适用于我不需要使用这个包里任何的函数,只需要单纯的导入
)
init函数
init()函数没有参数和返回值,不能被手动调入,但是会在这个包被导入或运行的时候自动运行
文件运行过程: 先对变量常量结构体之类进行声明,然后是init()执行,最后是main()执行
如果被导入的包还导入了其他包,则最后被导入的包的init()最先执行,最先导入的最后执行
//这里是right包里的init函数
func init(){
fmt.Println("asdas")
}
并发
并发,同一时间段执行多个任务,并行,同一时间执行多个任务
go语言中要实现并发非常简单,只需要在调用函数前面加上一个关键词go,一个goroutine必定对应一个函数
package main
import(
"fmt"
)
func hello(){ //这里先定义一个最简单的函数
fmt.Println("hello")
}
func main(){
go hello() //这里只需要在前面加上一个go就可以并发了
fmt.Println("运行结束")
}
但是这是你如果运行的话就会发现,程序只会输出“运行结束”,这是因为这个并发(goroutine)还没来得及执行,main函数就结束运行了,这是需要让main函数等一下goroutine
func main() {
//goroutine执行需要时间所以需要等一下
for i:=0; i<100;i++ {
//go hello() //开启一个单独的goroutine去执行这个函数
go func(i int){
fmt.Println(i)
}(i)
}
fmt.Println("main")
time.Sleep(1)
//main 函数结束了 由main 函数启动的goroutine也都结束了
}
如果想让main智能的去等待goroutine去执行完毕(自定义时间的话会出现待完毕goroutine还没执行完毕或者执行完毕还剩很多时间),需要用到sync.WaitGroup ,里面只有Add(),Wait(),Done()三个函数
var wg sync.WaitGroup //新建一个wg 类型是sync.WaitGroup
func wait(i int){
defer wg.Done() //执行完毕后给出一个完成的信号
time.Sleep(time.Second * time.Duration(rand.Intn(3)))
fmt.Println(i)
}
func main(){
for i:= 0; i<10;i++{
wg.Add(1) //给每个goroutine都加一个Add(),类似于给每个人发一个号牌
go wait(i)
}
wg.Wait() //等待wg的计数器减为零
}
这里还有一点理论知识,以后在面试的时候可能会问到:
goroutine调度模型
goroutine是一个栈,其初始内存很小(大概2kb)但是栈的最大可限制为1GB
GMP:G就是goroutine,这里面除了存放goroutine的信息外,还有与所在P的绑定信息等
P管理者一组goroutine队列,goroutine的向上下文和将来要做的一些任务,会对goroutine做一些调度(比如暂停占用cpu较长时间的goroutine,优先运行后续的goroutine)如果一个p的任务都做完了就去全局任务里去取,全局任务也都做完了就去别的P里拿。M是go在运行时对操作系统线程的虚拟。M与内核线程的映射是一对一关系,所有的goroutine最终都是放到M上执行的
P与M一般都是一一对应的,他们的关系是:P管理者一组G在M上运行。当一个G的goroutine长期挂在M上卡住时runtime会新建一个M,把G上其他的goroutine都放到这个M上运行,当阻塞的G运行完毕或者确认死掉的时候这个M会被回收,P的个数是通过runtime.GOMAXPROCS()设定的(最大256,cpu有几个核心就最多是几)默认是跑满cpu
GO语言并发的优势在于,其他语言的并发需要靠系统进行调度,GO已经实现了靠自己的runtime进行调度