Go基础
下载Go语言开发工具
下载Go语言环境
下载地址:https://golang.google.cn/dl/
下载Go语言开发工具
下载地址:https://www.jetbrains.com/go/
第一个Go语言代码
package main
import "fmt"
/*
这是一个main函数,这个是给go语言启动的入口
*/
func main() {
//fmt.Println;打印一句话,然后执行完毕后进行换行
fmt.Println("hello,world")
}
主意:go语言需要手动import进行导包,fmt是打印函数,类似于java的Systrm.out.println()
定义变量
package main
import "fmt"
func main() {
//var 变量名称 数据类型 = 变量的值,这里的=号是赋值的,不是比较的
var name string = "杨星辰"
//打印输出变量的值
fmt.Println(name)
//输出的是 杨星辰
//修改变量的值
name="zhangsan"
//再次输出变量的值
fmt.Println(name)
//输出的是 zhangsan
}
从而可以验证,变量是一个可以变化的值
一次定义多个变量
package main
import "fmt"
func main() {
var (
name string
age int
address string
)
//这里没有定义name,age,address 那么就输出默认值
// string 默认值是空,int默认值是0,
fmt.Println(name, age, address)
// 输出 0
name = "yangxingchen"
age = 19
address = "hubei"
//定义变量的值,定义是什么那么输出的就是什么
fmt.Println(name, age, address)
//输出yangxingchen 19 hubei
}
Go语法糖 简化定义变量(自动推送变量类型)
/**
第二种 根据我们赋值的内容自动推导出变量类型
变量类型%T
*/
name := "星辰"
age := 19
address := "武汉"
fmt.Println(name, age, address)
//输出变量内容 星辰 19 武汉
fmt.Printf("%T,%T,%T",name,age,address);
//string age string
星辰 19 武汉
package main
import "fmt"
func main() {
name := "yangxingchen"
fmt.Println(name)
//这里:=已经声明了,那么就不能在给name声明变量了,只能通过=号修改变量的值
name := "zhangsan"//no new variables on left side of :=
fmt.Println(name)
}
获取变量数据类型
/*
第一个参数填写标识符,%T是按照第二个参数开始按顺序对于的变量
这里我需要打印三个变量的数据类型,我就需要在第一个参数里面输入三个%T(是在一个""里面写着)
然后从第二个参数开始,一个参数对应一个变量类型。
*/
name := "星辰"
age := 19
address := "武汉"
fmt.Printf("%T,%T,%T", name, age, address) //输出变量类型%T
string,int,string
获取变量内存地址
package main
import "fmt"
/*
打印变量的内存地址
*/
func main() {
var age int = 18
//使用Printf 可以根据占位符打印内容,%d是打印变量内容,%p是打印变量内存地址,
fmt.Printf("变量内容%d,变量内存地址:%p", age, &age)
//这里验证一下更改变量的值,会不会更改变量的内存地址
age = 20;
fmt.Printf("变量内容%d,变量内存地址:%p", age, &age)
//变量内容18,变量内存地址:0xc0000180b8
//变量内容20,变量内存地址:0xc0000180b8
}
var num int
var num1 int
num = 100
num1 = 200
num, num1 = num1, num
//%d是获取变量内容,%p是获取内存地址
//%p因对应&num
fmt.Printf("num=%d 内存地址=%p", num, &num)
fmt.Printf("num1=%d 内存地址=%p", num1, &num1)
//num=100 内存地址=0xc0000140d0
//num1=200 内存地址=0xc0000140d8
变量值交换
在其他语言上,变量交换是这样的,(这里以java演示)
int a=10;
int b=20;
System.out.println("a="+a+",b="+b);
//a=10,b=20
int temp=0;
a=temp;
a=b;
b=temp;
System.out.println("a="+a+",b="+b);
//a=20,b=10
在Go语言中实现
package main
import "fmt"
/*
变量值交换
*/
func main() {
var a int = 10
var b int = 20
a, b = b, a
fmt.Println(a, b)
//20 10
}
匿名变量
package main
import "fmt"
/*
匿名变量
匿名变量就是可以丢弃的变量,我们不想接受这个变量,就可以使用匿名变量
*/
func main() {
name1, age1 := user()
fmt.Println(name1, age1) //杨星辰 19
//如果我这里执行接受name,不想接收age
name2, _ := user()
_, age2 := user()
fmt.Println(name2) //杨星辰
fmt.Println(age2) //19
}
func user() (string, int) {
return "杨星辰", 19
}
变量作用域
package main
import "fmt"
//定义全局变量
var name string = "杨星辰"
func main() {
//定义局部变量,只能在本方法里面使用
var name string = "yangxingchen"
//局部变量名字和全局变量名字可以相同,但是以就近原则,
//如果局部变量和全局变量名字相同,那么会优先选择局部变量
fmt.Println(name)
test1()
}
func test1() {
//这里拿到的是全局变量
fmt.Println(name)
}
yangxingchen
杨星辰
常量
变量是一个可以变化的值,常量就是一个不可变化的值
是一个值得标识符,在程序运行时,不可以会被修改的变量
数据类型:布尔型,数字型,(整数型、浮点型和复数型)和字符串型
package main
/**
是一个简单值得标识符,在程序运行时,不会被修改的量
数据类型:布尔型,数字型,(整数型、浮点型和复数型)和字符串型
*/
import "fmt"
func main() {
const qq string = "5655470" //显示定义
const wx = "qiang030507" //隐式定义
const a, b, c = 1, "www.baidu.com", false //同时定义多个常量
fmt.Println(qq)//5655470
fmt.Println(wx)//qiang030507
fmt.Println(a, b, c)//1 "www.baidu.com" false
}
5655470
qiang030507
1 “www.baidu.com” false
iota
特殊常量,可以认为是一个被编译器修改的常量
iota是go语言的常量计数器 从0开始 依次递增
package main
/*
特殊常量,可以认为是一个被编译器修改的常量
iota是go语言的常量计数器 从0开始 依次递增
*/
import "fmt"
func main() {
const (
a = iota
b = iota
c = iota
d
e
)
fmt.Print(a, b, c)
//这里d不顶用iota那么也会自动往下计数
fmt.Print(a, b, c,d,c)
}
0 1 2
0 1 2 3 4
如果声明iota常量,如果没有下一个const那么就会一直递增,即使后面创建的常量赋值了那么这个iota还是会递增,直至const出现之时将被重置
package main
/*
特殊常量,可以认为是一个被编译器修改的常量
iota是go语言的常量计数器 从0开始 依次递增
*/
import "fmt"
func main() {
const (
a = iota
b = 100
c = 123412
d = 123123123
e = 123123
f = 123123
g = iota
)
const (
q = iota
h
)
fmt.Print(a, b, c, d, e, f, g, q, h)
}
0 100 123412 123123123 123123 123123 6 0 1
定义方法
package main
import "fmt"
func main() {
//特点是"_","_"本身就是一个特殊字符
//被称为空白标识符,任何赋值给这个标识符的值都会被抛弃,这样可以加强代码的灵活性
//当我们不想接收第二个值时可以废弃掉
num, _ := test()
fmt.Println(num)//10
//也可以参数一对一生成变量
num, size:= test()
fmt.Println(num,size)//10,200
//也可以直接输出对象返回值
fmt.Println(test())//10,200
}
//func 方法名称(),(返回类型)
func test() (int, int) {
var num int = 10
var size int = 200
return num, size
}
基本数据类型
在Go编程语言中,数据类型用于声明函数和变量。
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。
编译器在进行编译的时候,就要知道每个值的类型,这样编译器就知道要为这个值分配多少内存,并且知道这段分配的内存表示什么。
布尔型
package main
import "fmt"
/*
布尔值,只有两个值,true,false
默认值是false
*/
func main() {
var isFlag bool = false
var isFlag2 bool = true
fmt.Printf("%T,%t\n", isFlag, isFlag)
//bool,false
fmt.Printf("%T,%t \n", isFlag2, isFlag2)
//bool,true
}
false
整形
package main
import "fmt"
/*
整型
*/
func main() {
//无符号(没有负数)
var num uint8 = 255 //取值范围(0到255)
var num1 uint16 = 65535
var num2 uint32 = 1324234
var num3 uint64 = 34563453245345345
fmt.Println(num, num1, num2, num3)
//有符号(既包含负数也包含正数)
var num4 int8 = -55 //取值范围(-127到128)
var num5 int16 = -5535
var num6 int32 = -1324234
var num7 int64 = -34563453245345345
fmt.Println(num4, num5, num6, num7)
//byte(uint 8) 取值范围0到255
var num8 byte = 119
//int(int64)
var num9 int = 9999999
fmt.Printf("%T,%d", num8, num8)
fmt.Printf("%T,%d", num9, num9)
}
浮点型
package main
import "fmt"
/*
浮点型
*/
func main() {
var num float32 = 3.14050001
var num1 float64 = 3.14050001
//%.3f 这里我们想保留几位小数就.几就可以了,会自动的四舍五入
fmt.Printf("%.3f\n", num)//3.141
fmt.Printf("%f", num1)//3.140500
}
使用float32会精度丢失
package main
import "fmt"
/*
浮点型
*/
func main() {
var num float32 = -3.1405001
var num1 float64 = -3.1405001
//如果使用float32会精度丢失
fmt.Println(num) //-3.1405
fmt.Println(num1) //-3.1405001
}
字符串类型
package main
import "fmt"
/*
字符串类型
*/
func main() {
var str string
str = "中" //双引号是字符串
str1 := '中' //单引号是unicode编码表
fmt.Printf("%T,%s\n", str, str)
fmt.Printf("%T,%s", str1, str1)
fmt.Println("hello" + "world") //字符串链接
fmt.Println("hello\"world") // /“是输出”
fmt.Println("hello\nworld") // /n是换行
fmt.Println("hello\tworld") // /n是tab间隔
}
数据类型强转
package main
import "fmt"
/*
数据类型强转
*/
func main() {
var a int = 1
var b float64 = 1.0
//这里在需要被强转的变量前面加上类型
var c float64 = float64(a)
fmt.Printf("%T,%d\n", a, a) //int,1
fmt.Printf("%T,%f\n", b, b) //float64,1.000000
fmt.Printf("%T,%f", c, c) //float64,1.000000
}
运算符
算数运算符
package main
import "fmt"
/*
算数运算符
*/
func main() {
var a = 10
var b = 3
fmt.Println(a + b) //13
fmt.Println(a - b) //7
fmt.Println(a * b) //30
fmt.Println(a % b) //1
a++
fmt.Println(a) //11
a--
fmt.Println(a) //10
}
关系运算符
package main
import "fmt"
/*
关系运算符
*/
func main() {
var a = 10
var b = 3
fmt.Println(a == b) //false
fmt.Println(a != b) //true
fmt.Println(a > b) //true
fmt.Println(a < b) //false
fmt.Println(a >= b) //true
fmt.Println(a <= b) //false
}
逻辑运算符
多件事情有逻辑相关的问题
例如:18岁和身份证,如果只满足一样是不能去网吧上网的,必须全部满足(已满18岁和拥有身份证)才能去网吧
package main
import "fmt"
/*
逻辑运算符
*/
func main() {
var a bool = false
var b bool = true
//(and)双方都必须为true才为true
fmt.Println(a && b) //false
//(or)双方一方为true就为true
fmt.Println(a || b) //true
//(!)双方都为true才能为false,双方都为false结果才能为true
fmt.Println(!(a && b)) //true
}
位运算
后期再补
赋值运算符
package main
import "fmt"
/*
赋值运输
*/
func main() {
var a int = 10
var b int = 3
a += b //a=a+b
fmt.Println(a) //13
a = 10
a -= b //a=a-b
fmt.Println(a) //1
a = 10
a *= b //a=a*b
fmt.Println(a) //30
a = 10
a /= b //a=a/b
fmt.Println(a) //3
a = 10
a %= b //a=a*b
fmt.Println(a) //1
}
流程控制
程序的流程控制结构一共有三种:顺序结构,选择结果,循环结构
顺序结构:从上到下,逐行执行。默认的逻辑
选择结构:条件满足某些代码才会执行
- if
- switch
- select,后面channel在讲
循环结构:条件满足某些代码会被反复执行0-N次
IF
package main
import "fmt"
/*
IF
*/
func main() {
var a int = 10
var b int = 9
if a > b {
fmt.Println("a比b大")
} else if a == b {
fmt.Println("a和b相等")
} else {
fmt.Println("a比b小")
}
}
结果:a比b大
package main
import "fmt"
/*
IF
*/
func main() {
var score int = 89
/*
大于等于90,A
大于等于80,小于90,B
大于等于70,小于80,C
如果都不满足那么就是,D
*/
if score >= 90 {
fmt.Println("A")
} else if score >= 80 && score < 90 {
fmt.Println("B")
} else if score >= 70 && score < 80 {
fmt.Println("C")
} else {
fmt.Println("D")
}
}
结果:B
Switch
package main
import (
"fmt"
)
/*
switch
*/
func main() {
var score int = 87
//这里参数填写我们需要判断的值
switch score {
//case就是我们匹配上跟我们填写参数的值一样那么就匹配成功就执行:号后面的内容
case 90:
fmt.Println("A")
//这里也可以填写多个参数进行匹配,只要有一个参数匹配成功那么就执行
case 89, 88, 87:
fmt.Println("B")
//如果上面都不满足,那么就执行default里面的代码了
default:
fmt.Println("C")
}
}
结果:B
case 穿透
package main
import (
"fmt"
)
/*
switch
*/
func main() {
var score int = 88
switch score {
case 90:
fmt.Println("A")
case 89, 88, 87:
fmt.Println("B")
//fallthrough 关键字是穿透,如果这里匹配成功执行完成后,那么还会执行下一个匹配(注意只能穿透下面一项)
fallthrough
case 100:
fmt.Println("D")
default:
fmt.Println("C")
}
}
执行结果:B,D
For
package main
import "fmt"
/*
for
*/
func main() {
//for 起始条件;结束条件;控制条件自增自减
for i := 0; i <= 10; i++ {
fmt.Println(i)
}
}
循环加数
package main
import "fmt"
/*
for
*/
func main() {
a := 0
for i := 0; i <= 10; i++ {
a += i
}
fmt.Println(a)
}
五五方正
package main
import "fmt"
/*
for
*/
func main() {
//外面一层for循环是打印一共有5行
for i := 1; i <= 5; i++ {
//里面是打印每一行有多少个
for y := 1; y <= 5; y++ {
fmt.Print("* ")
}
fmt.Println()
}
}
/*
* * * * *
* * * * *
* * * * *
* * * * *
* * * * *
*/
九九乘法表
package main
import "fmt"
/*
for
*/
func main() {
//外面一层打印每一行,第一次打印第一行只有一个,第二次打印第二行这个时候i=2
for i := 1; i <= 9; i++ {
//里面打印每一行的算数,第一次y<=i就是一个,
for y := 1; y <= i; y++ {
fmt.Print(y, "*", i, "=", i*y, " ")
}
fmt.Println()
}
}
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81
退出for循环
break退出整个for循环
package main
import "fmt"
/*
for
*/
func main() {
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Println(i)
}
}
/*
0
1
2
3
4
*/
continue 退出当前循环
package main
import "fmt"
/*
for
*/
func main() {
for i := 0; i < 10; i++ {
if i == 5 {
continue
}
fmt.Println(i)
}
}
/*
0
1
2
3
4
6
7
8
9
*/
string
注意:string里面的下标字符是不允许被修改的
str[1] = ‘A’
fmt.Println(str[1])
package main
import "fmt"
/*
字符串
*/
func main() {
str := "hello,world"
//打印字符串完整
fmt.Println(str)
//打印字符串长度
fmt.Println(len(str))
//根据下标打印字符串的unicode编码
fmt.Println(str[0])
//通过%c可以将unicode编码转换字符串
fmt.Printf("%c", str[0])
fmt.Println()
//for循环遍历取出str的unicode编码
for i := 0; i < len(str); i++ {
fmt.Print(str[i], "转换")
fmt.Printf("%c", str[i])
fmt.Println()
}
//注意:string里面的下标字符是不允许被修改的
//str[1] = 'A'
//fmt.Println(str[1])
}
数组
数组是一种同数据类型的集合
在Go语言中,数组从声明创建的时候就已经确定好大小长度,且可以根据下标更改同类型的内容,长度不可以变化。
基本使用
package main
import "fmt"
func main() {
//创建数组,大小为3。如果不指定内容,那么就是数据类型的默认值
var a [3]int //只声明了大小,没有声明内容。所以默认就是int的默认值0
fmt.Println(a[0])//0
}
数组定义
var 数组名称 [大小] 数据类型
例如 var a [5] int
数组的长度是一共常量 一旦被定义,就不可以改变的。
package main
import "fmt"
func main() {
//创建数组,大小为3。如果不指定内容,那么就是数据类型的默认值
var a [3]int
var b [5]int
//a = b //报错,因为a和b数据类型一样但是大小长度不一样。
var c [3]int
var d [3]int
c = d //正确,因为c和的的数据类型 大小长度一样
fmt.Printf("%T", a)
fmt.Println()
fmt.Printf("%T", b)
fmt.Println()
fmt.Println("-------------")
fmt.Printf("%T", c)
fmt.Println()
fmt.Printf("%T", d)
}
数组可以通过下标访问和修改内容
package main
import "fmt"
func main() {
//创建数组,大小为3。如果不指定内容,那么就是数据类型的默认值
var a [3]int
//通过下标赋值。下标从0开始,一共三个,0,1,2
a[0] = 1
a[1] = 2
a[2] = 3
for i := range a {
fmt.Println(a[i]) //1,2,3
}
fmt.Println("-----------------")
a[1] = 10 //修改下标为1的值为10
for i := range a {
fmt.Println(a[i]) //1,10,3
}
}
数组的初始化
数组初始化的方式有很多
方式一
声明数组的时候就初始化值
package main
import "fmt"
func main() {
var a [3]int //使用数组的数据类型默认值。int默认值0
var b = [3]int{1, 2, 3} //使用指定的初始值,完成初始化赋值
var c = [3]string{"武汉", "北京", "上海"}
fmt.Println(a) //[0 0 0]
fmt.Println(b) //[1 2 3]
fmt.Println(c) //[武汉 北京 上海]
}
方式二
按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度
package main
import "fmt"
func main() {
var a = [...]int{1, 2, 3}
var b = [...]string{"北京", "广州", "深圳"}
fmt.Println(a) //[1 2 3]
fmt.Println(b) //[北京 广州 深圳]
}
方式三
我们还可以通过指定下标的内容来完成初始化赋值
package main
import "fmt"
func main() {
var a = [...]int{1: 1, 3: 2, 5: 10} //这里的长度是根据最后一位的下标决定的
var b = [...]string{2: "北京", 3: "广州", 8: "深圳"} //string 默认值是空
fmt.Println(a) //[0 1 0 2 0 10]
fmt.Println(b) //[0 1 0 2 0 10]
}
数组的遍历
方式一
for循环遍历
package main
import "fmt"
func main() {
var a = [...]int{1, 2, 3, 4, 5} //这里的长度是根据最后一位的下标决定的
for i := 0; i <= len(a); i++ {
fmt.Println(a[i])//1,2,3,4,5
}
}
方式二
使用for range
package main
import "fmt"
func main() {
var a = [...]int{1, 2, 3, 4, 5} //这里的长度是根据最后一位的下标决定的
//下标,值
for index, value := range a {
fmt.Println(index, value)
/**
0 1
1 2
2 3
3 4
4 5
*/
}
}
多维数组
多维数组就是数组里面声明数组,数组里面嵌套数组
二维数组
package main
import "fmt"
func main() {
var a [3][2]int
//赋值
a[0][0] = 1
a[0][1] = 2
a[1][0] = 3
a[1][1] = 4
a[2][0] = 5
a[2][1] = 6
//a[0] 数组里面包含了一个数组,1,2
fmt.Println(a)
/**
[[1 2] [3 4] [5 6]]
*/
}
package main
import "fmt"
func main() {
//声明时赋值二位数组
var a = [3][2]int{{0, 1}, {2, 3}, {4, 5}}
fmt.Println(a)
//[[0 1] [2 3] [4 5]]
}
package main
import "fmt"
func main() {
//声明时赋值二位数组
var a = [3][2]int{{0, 1}, {2, 3}, {4, 5}}
//双层for循环打印,外面一层打印外层数组,第二层打印 第一层里面的数组
for i := 0; i < len(a); i++ {
for y := 0; y < len(a[i]); y++ {
fmt.Println(a[i][y])
}
}
/**
0
1
2
3
4
5
*/
}
注意:多维数组只有第一层可以使用**…**来让编译器推导数组长度。例如:
package main
import "fmt"
func main() {
//支持,第一层使用...,第二层不支持
var a = [...][2]int{{0, 1}, {2, 3}, {4, 5}}
//虽然不会爆红,第一层和第二层都用...
var b = [...][...]int{{0, 1}, {2, 3}, {4, 5}}
fmt.Println(a) //[[0 1] [2 3] [4 5]]
fmt.Println(b) //打印报错,因为多维数组只能第一层使用...
}
数组类型传递
如果指定了数组的长度那么传递的就是副本,如果没有指定长度[]int那么传递的就是当前数组引用本身
传递副本
package main
import "fmt"
func main() {
a := [3]int{0, 1, 2}
max(a)
fmt.Println(a)//0,1,2
}
func max(x [3]int) {
x[0] = 10
}
传递本身
package main
import "fmt"
func main() {
a := []int{0, 1, 2}
max(a)
fmt.Println(a)//[10 1 2]
}
func max(x []int) {
x[0] = 10//修改下标为一的值
}
练习题
定义数组[1,3,5,7,9],求出数组元素总和
package main
import "fmt"
func main() {
a := []int{1, 3, 5, 7, 8}
count := 0
for i := range a {
count += a[i]
}
fmt.Println(count) //24
}
定义一个数组a,在定义一个值b,求出数组a中那两个下标的和等于这个b
package main
import "fmt"
func main() {
a := []int{1, 3, 5, 7, 8}
count := 8
for i := 0; i < len(a); i++ {
for y := i; y < len(a)-i; y++ {
if a[i]+a[y] == count {
fmt.Println(i, y)
}
}
}
}
//0 3
//1 2
函数
- 函数是基本的代码快,用于执行一共任务。
- Go语言中最少有个main()函数,是整个程序的启动函数
- 你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务
- 函数声明告诉了编辑器函数的名称,返回类型,和参数
定义函数
func function_name (parameter type)(return type){}
- 无返回值函数
- 有一个参数的函数
- 有两个参数的函数
- 有一个返回值的函数
- 有两个返回值的函数
实例
package main
import "fmt"
/*
函数
*/
func main() {
//调用函数很简单,直接add(参数1,参数2)继续传递打印就可以了
fmt.Println(add(1, 2))
//单独获取函数返回的内容赋值给变量
a := add(1, 2)
fmt.Println(a)
var b int = add(1, 2)
fmt.Println(b)
}
// func 函数名称(参数名称 参数类型,参数名称 参数类型) 返回值 {}
func add(a int, b int) int {
return a + b
}
无返回值的函数
package main
import "fmt"
/*
函数
*/
func main() {
add()
}
func add() {
fmt.Println(1)
}
有一个参数的函数
package main
import "fmt"
/*
函数
*/
func main() {
add("杨星辰")
}
func add(name string) {
fmt.Println(str)
}
//杨星辰
有二个参数的函数
package main
import "fmt"
/*
函数
*/
func main() {
add("杨星辰", 19)
}
func add(name string, age int) {
fmt.Println(name, age)
}
//杨星辰 19
有一返回值的函数
package main
import "fmt"
/*
函数
*/
func main() {
//接受函数返回的值,并且打印
var name string = add("杨星辰")
fmt.Println(name)
}
func add(name string) string {
return name
}
//杨星辰
有两个返回值的函数
package main
import "fmt"
/*
函数
*/
func main() {
//接受函数返回的值,并且打印,这里自动给我们变量设置类型
var name, age = add("杨星辰", 19)
fmt.Println(name, age)
}
func add(name string, age int) (string, int) {
return name, age
}
//杨星辰 19
形参和实际参数
如果定义返回值了,那么就必须要return出去,否则程序会报错
函数定义参数的个数,类型,调用方都必须按照顺序和对应的类型进行传递,否则程序会报错
package main
/*
函数
*/
func main() {
//这里填写的内容是实际参数,是实际传递给函数的内容
add("杨星辰", 19)
}
//这里的 name,age 是形参,是接收外部传入的参数
func add(name string, age int) (string, int) {
return name, age
}
可变长参数
可变长参数顾名思义,就是参数长度是不固定的,你传递一个也好,传递100个也好都是可以的
注意:
- 如果一共函数里面有可变长参数和其他固定长度参数的时候,可变长参数必须要放在最后,否则回报错
- 一共函数里面只能有一共可变长参数,并且要放在参数的最后一位
使用可变长参数求和
package main
import "fmt"
/*
函数
*/
func main() {
count := add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
fmt.Println(count)
}
// 这里参数后面...就代表可变长
func add(num ...int) int {
//定义临时变量用来获取上一次的和
var temp int
//循环将参数的值加在一起
for i := 0; i < len(num); i++ {
//可变长其实就是一共数组,我们可以通过下标来获取参数的位置
temp += num[i]
}
//最后返回
return temp
}
//55
参数传递
- 值传递是拷贝一份给对方
- 引用传递是将内存地址传递给对方
package main
import "fmt"
/*
值传递
*/
func main() {
arr := [4]int{1, 2, 3, 4}
//这里我们吧arr传递过去,并且修改参数内容
update(arr)
//修改完成后我们打印一下
fmt.Println("main", arr)
//main [1 2 3 4]
//这里会发现我们并没有修改成功arr里面的值。
}
func update(arr [4]int) {
//这里我们修改数组的第0位
arr[0] = 0
//打印一下
fmt.Println("update", arr)
//update [0 2 3 4]
//这里已经修改成功了,但是main的arr没有变
}
/**
总结:我们会发现传递的arr没有变,但是update函数里面接受的arr值却变了
这里其实并没有吧main里面的arr给update函数了,而是将arr变量拷贝了一份传递给update所以会出现main中arr没有被修改,update里面的arr被修改了。
*/
/**
总结:我们会发现传递的arr没有变,但是update函数里面接受的arr值却变了
这里其实并没有吧main里面的arr给update函数了,而是将arr变量拷贝了一份传递给update所以会出现main中arr没有被修改,update里面的arr被修改了。
*/
这里就是将内存地址传递过去了。
package main
import "fmt"
/*
值传递
*/
func main() {
//定义引用类型,那么就不是复制变量给update,而是将当前引用包括内存地址都传递过去,
//然后updtae也可以拿到这个内存地址找到引用进行修改
arr := []int{1, 2, 3, 4}
update(arr)
fmt.Println("main", arr)
//main [0 2 3 4]
}
func update(arr []int) {
arr[0] = 0
fmt.Println("update", arr)
//update [0 2 3 4]
}
延迟函数
假如我有三个函数,那么我不想让这三个函数按照顺序执行,那么我们就可以用defer关键字来延后执行
- 这里要注意延迟函数还是会按照顺序的接受参数的内容,只是最后才执行函数而已
package main
import "fmt"
/*
值传递
*/
func main() {
//这里定义一个变量10
a := 10
//第一次打印变量值
fmt.Println("on1", a) //on1 10
//这里调用函数将10传递进去,
//不过这里我们使用了延迟关键字,会将当前函数和a的变量值放到一共【先进后出的队列中】
defer update(a) //update 10
//这里a+1,但是并没有影响上面延迟函数的a,因为上面的已经赋值给延迟函数了
a += 1
//这里打印最后的a
fmt.Println("on2", a) //on2 11
}
func update(a int) {
//打印传递到的a
fmt.Println("update", a)
}
函数高级
函数类型
研究一下函数是什么类型
package main
import "fmt"
/*
函数高级
*/
func main() {
//打印不同变量类型
fmt.Printf("%T\n", 10)
fmt.Printf("%T\n", "hello")
fmt.Printf("%T\n", 3.144)
//我们打印一下update这个函数的类型,会发现是func类型而且还有参数,【这里直接输入函数名子即可,不要带括号不然就是调用了】
fmt.Printf("%T\n", update)
}
func update(a, b int) {
fmt.Println(a, b)
}
int
string
float64
func(int, int)
得出结论,函数是func类型,如果函数有参数那么类型就是 func(参数一类型,参数二类型)
既然函数也有类型,我们是不是可以创建一共函数类型并且将其他函数赋值给他
package main
import "fmt"
/*
函数高级
*/
func main() {
//定义一个函数类型变量
var a func(int, int)
//然后将update这个函数赋值给a
a = update
//直接a后面加括号进行调用
a(1, 2) //1,2
//打印update函数的内存地址和a的内存地址
fmt.Println(update) //0xece560
fmt.Println(a) //0xece560
}
func update(a, b int) {
fmt.Println(a, b)
}
- 最总结构就是可以创建一个函数类型的变量并且将其他函数赋值给这个变量
- 其次这里是引用赋值,赋值后内存地址都是同一个
匿名函数
package main
import "fmt"
/*
匿名函数
*/
func main() {
//定义匿名函数的一种方式
//no:1
f1 := func(a, b int) {
fmt.Println("no:1", a, b)
}
f1(1, 2)
//no:2,直接在匿名函数后面使用()直接调用
func(a, b int) {
fmt.Println("no:2", a, b)
}(1, 2)
//no:3
f3 := func(a, b int) (int, int) {
return a, b
}
fmt.Println(f3(1, 2))
}
回调函数
这里我们会发现我们将不同函数传递进去,执行的就是传递的函数业务
package main
import "fmt"
/*
回调函数
*/
func main() {
//回调函数就是,吧函数当作参数传递给另外一个函数
/*
这里我们会发现我们将不同函数传递进去,可以做不同的函数业务【很棒的一个设计】
*/
f1(1, 2, add)
f1(1, 2, sub)
}
//这里我们定义一个回调函数,然后根据参数来做不同的业务
func f1(a int, b int, add func(a, b int) int) {
fmt.Println(add(a, b))
}
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
闭包
- 一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量
- 并且该外外层函数的返回值就是这个内层函数
- 这个内层函数和外层函数的局部变量,统称为闭包结构
- 局部变量的生命周期会发生变化,正常的局部变量会随着函数的调用而创建,随着函数的结束而销毁
- 但是闭包结构中的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内存函数还在继续使用
package main
import "fmt"
/*
闭包
*/
func main() {
f1 := add()
fmt.Println(f1()) //9
fmt.Println(f1()) //8
fmt.Println(f1()) //7
fmt.Println(f1()) //6
fmt.Println("==================")
f2 := add() //这里又重新赋值给了一个函数类型的变量,相当于另外开辟了一个空间(闭包特性)
fmt.Println(f2()) //9
fmt.Println(f2()) //8
fmt.Println(f2()) //7
fmt.Println(f2()) //6
//这里我们在调用f1
fmt.Println(f1()) //5
/**
这里会发现我们f1的a还是在我们f1的内存中
*/
}
// 定义一个闭包,返回一个函数并且带上返回函数的返回值
func add() func() int {
a := 10
//定义一个匿名函数,每次调用一次的时候就减一
fadd := func() int {
a -= 1
return a
}
return fadd
}
切片
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。
定义
var name []type
package main
func main() {
//定义一个切片,切片类似于java中的list集合,底层自动扩容数组
var name []int
//可以一直存放元素
name[0] = 1
name[1] = 2
name[2] = 3
//...
name[99] = 98
}
还可以
package main
import "fmt"
func main() {
//定义一个切片,切片类似于java中的list集合,底层自动扩容数组
var name []int
var a = []string{"武汉", "北京"}
var b = []bool{false, true}
fmt.Println(name) //[]
fmt.Println(a) //[武汉 北京]
fmt.Println(b) //[false true]
}
切片的长度和容量
切片拥有自己的长度和容量,我们可以使用内置len方法查看长度。cap()查看切片容量
package main
import "fmt"
func main() {
//定义一个切片,切片类似于java中的list集合,底层自动扩容数组
var name = []int{0, 1, 2, 3, 4, 5}
//查看长度
fmt.Println(len(name)) //6
//查看容量
fmt.Println(cap(name)) //6
}
基于现有数组声明切片
由于切片底层就是用的数组,那么我们也可以基于数组创建一个切片
package main
import "fmt"
func main() {
//首先定义数组
name := [5]int{0, 1, 2, 3, 4}
a := name[1:4]
fmt.Println(a) //[1 2 3]
fmt.Printf("%T", a) //[]int
}
切片定义切片
package main
import "fmt"
func main() {
//首先定义一个切片
name := []int{1, 2, 3, 4, 5, 6, 7}
a := name[2] //定义name下标2的切片
fmt.Println(a) //3
b := name[1:6] //截取 name切片开始下标1,结尾下标6,创建一个切片
//b:=name[1:len(name)] 简写 b:=name[1:]
fmt.Println(b) //[2 3 4 5 6]
fmt.Printf("%T", b) //[]int
}
对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误。
make函数创建切片
我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的**make()**函数,格式:make([]T,len,cap)
package main
import "fmt"
func main() {
//使用make函数定义一个切片,make([]数据类型,初始长度,切片总容量)
name := make([]int, 2, 10)
fmt.Println(name) //[0 0]
name[0] = 1
name[1] = 2
fmt.Println(name) //[1 2]
}
上述代码中,name存储空间被分配了10个,但是只用了2个
切片的本质底层
切片的本质就是对数组进行了封装,切片包含三个信息:指针,长度,容量
举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。
s1的长度内容是0,1,2,3,4 容量是a数组的长度
切片s2 := a[3:6],相应示意图如下:
s2的长度是3[3,4,5],容量是a数组总长度减去3是5
切片不能比较
- 切片直接是不能直接比较的,比如用==操作符来判断两个切片是否包含相等元素
- 切片唯一合法比较是nil比较
- 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0,但是我们不能说一个长度和容量都是0的切片,一直是nil如下图
package main
import "fmt"
func main() {
var a []int
b := []int{}
c := make([]int, 0, 0)
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
}
注意:如果要判断一个切片是否为空,应该用len(a)0来判断,而不是anil
切片是引用传递
package main
import "fmt"
func main() {
name := []int{1, 2, 3, 4, 5}
a := name //引用传递
a[0] = 10
fmt.Println(name) //[10 2 3 4 5]
}
切片遍历
切片遍历和数组遍历都是一样的,都支持下标遍历和range遍历
package main
import "fmt"
func main() {
name := []int{1, 2, 3, 4, 5}
//下标遍历
for i := 0; i < len(name); i++ {
fmt.Println(name[i]) //1,2,3,4,5
}
//range遍历
for index, value := range name {
fmt.Println(index, value)
/*
下标 值
0 1
1 2
2 3
3 4
*/
}
}
切片动态添加元素
package main
import "fmt"
func main() {
name := []int{1, 2, 3, 4, 5}
//
name = append(name, 6)
for n := 0; n < len(name); n++ {
fmt.Println(name[n]) //1,2,3,4,5,6
}
fmt.Println(len(name), cap(name))
for index, value := range name {
fmt.Println(index, value) //1,2,3,4,5,6
}
//注意:这样遍历出切片初始定义的值。无法遍历出append操作的值
for i := range name {
fmt.Println(i) //1,2,3,4,5
}
}
切片扩容策略
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。
切片拷贝
先看下面问题
package main
import "fmt"
func main() {
name := []int{1, 2, 3, 4, 5}
a := name
a[0] = 10
fmt.Println(name)
fmt.Println(a)
//因为切片传递的是自己本身的引用,所以a改变的时候name也会被改变
//[10 2 3 4 5]
//[10 2 3 4 5]
}
如果我们使用内置的copy方法拷贝一个切片出来就不会改变原来切片的值了。
package main
import "fmt"
func main() {
name := []int{1, 2, 3, 4, 5}
//使用make创建一个切片
a := make([]int, len(name), 10)
//将name的元素拷贝到a
copy(a, name)
//修改a的元素.不会影响name的值
a[0] = 10
fmt.Println(name) //[1 2 3 4 5]
fmt.Println(a) //[10 2 3 4 5]
}
从切片中删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
package main
import "fmt"
func main() {
name := []int{1, 2, 3, 4, 5}
//清除下标2的元素
name = append(name[:2], name[3:]...)
fmt.Println(name)//[1 2 4 5]
}
要从切片name中删除索引为index的元素,操作方法是
name=append(name[:下标],name[下标+1:]...)
name = append(name[:2], name[3:]...)
练习
创建一个数组[3, 7, 8, 9, 1] ,进行从小到大排序
package main
import "fmt"
func main() {
var a = []int{3, 7, 8, 9, 1}
//从小到大排序
for i := 0; i < len(a); i++ {
b := 0
for y := i + 1; y < len(a); y++ {
if a[i] > a[y] {
b = a[y]
a[y] = a[i]
a[i] = b
}
}
}
fmt.Println(a)
}
指针
Go语言中的指针
Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。Go语言中的指针操作非常简单,只需要记住两个符号:(取地址)&和***** (根据地址取值)
指针地址和类型
package main
import "fmt"
func main() {
a := 10
b := &a
fmt.Println(&a) //取的是a的内存地址0xc0000180a8
fmt.Println(b) //取的是a的内存地址0xc0000180a8
fmt.Printf("%T", b) //*int
fmt.Println(&b) //取的是b的内存地址0xc00000a028
/**
这里b变量保存的值是a的内存地址,但是b又开拓了一快内存用来存放a的内存地址
*/
}
指针取值
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下
package main
import "fmt"
func main() {
a := 10
b := &a //使用&a可以将a的内存地址赋值给b保存是一个*int类型。注意b保存的内容的a的内存地址,并不是将b的内存地址等于a的内存地址
c := *b //使用*b 将b保存的a的内存地址去查找a这个变量内存,然后将a的变量内存赋值给c
fmt.Println(c)//10
}
总结:
取地址操作符**&和取值操作符*是一对互补操作符,&**取出地址,*****根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
指针传值示例:
package main
import "fmt"
func main() {
a := 10
funA(a) //值传递
fmt.Println(a)
funB(&a) //将内存地址传递过去,然后funB就通过内存地址去修改a的值
fmt.Println(a)
}
func funA(a int) {
a = 100
}
func funB(a *int) {
*a = 200
}
new和make
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["沙河娜扎"] = 100
fmt.Println(b)
}
/*
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0xfce496]
*/
执行上面的代码会引发panic,为什么呢?
在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。
Go语言中new和make是内建的两个函数,主要用来分配内存。
new
new (Type) *Type
Type:表示类型,new函数只接受一个参数,这个参数是一个数据类型 例如int,bool
*Type:表示类型地址,new函数返回的是一个指向该类型的一个内存地址的指针
使用new函数得到的是一个类型的指针,并且该函数对应的值为该类型的默认值,int就是0,bool就是fales
package main
import "fmt"
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T", a)//*int
fmt.Println(*a)//0 //*a是获取内存地址的值
fmt.Printf("%T", b)//*bool
fmt.Println(*b)//false //*a是获取内存地址的值
}
例如 var a int 我们只声明了一个int类型变量a,但是没有初始化,所以a的值是0
但是new函数返回的是一个指针变量,指针作为引用类型,必须先初始化在赋值,因为先初始化才有了内存空间,才能将值写入内存空间中。
对引用指针赋值
package main
import "fmt"
func main() {
//先初始化
a := new(int)
//对应a指针内存赋值
*a = 10
fmt.Println(*a) //*a表示打印a的对应内存地址保存的值
}
make
make也是用于分配内存的,区别与new,make只能对切片,map,和channel的内存创建,而且它的返回值是这三个类型的本身,而不是他们的指针类型,因为这三个数据类型就是引用类型,所以没有必要返回指针了。
make(t Type,size ...IntegerType) Type
make函数是无可替代的,因为我们在后续使用切片,map,channel的时候,都需要使用make来进行初始化,然后才能对他们赋值。
package main
import "fmt"
func main() {
//方式一,先声明一个类型为map的变量a
var a map[string]int
//然后用make函数对a进行初始化
a = make(map[string]int, 10)
//最后对map类型变量a进行赋值
a["杨星辰"] = 100
fmt.Println(a) //map[杨星辰:100]
//方式二 直接用:=进行创建和初始化一个map类型的变量
b := make(map[string]int, 10)
//最后对map类型变量a进行赋值
b["杨贵强"] = 1000
fmt.Println(b) //map[杨贵强:1000]
}
new和make区别
- 二者都是进行创建内存,内存分配的
- make只能用于引用类型,切片,map,channel初始化,返回的是引用类型本身
- 而new只能用于基本类型的初始化内存分配,并且内存对应的值为类型默认值,返回的是类型的内存地址
Map
Go语言中,map是一个无序的key-value集合数据结构,map是引用类型,必须先初始化在赋值
map定义
map[KeyType]ValueType
- keyType:key的数据类型
- valueType:value的数据类型
map类型的变量默认值是nil,需要用make来进行初始化分配内存
make(map[string]int,cap)
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量,以免造成内存浪费
map的使用
package main
import "fmt"
func main() {
student := make(map[string]int, 10)
student["张三"] = 16
student["李四"] = 17
student["杨贵强"] = 18
fmt.Println(student) //map[张三:16 李四:17 杨贵强:18]
fmt.Printf("%T", student) //map[string]int
//通过map的key获取对应key的value
fmt.Println(student["杨贵强"]) //18
}
声明时,赋值
package main
import "fmt"
func main() {
student := map[string]int{"张三": 17, "杨星辰": 18, "杨贵强": 19}
fmt.Println(student)//map[张三:17 杨星辰:18 杨贵强:19]
}
判断某个key是否存在
go语言中有一个特殊的写法来判断map中是否存在这个kye
package main
import "fmt"
func main() {
userInfo := map[string]int{"张三": 18, "李四": 19, "王五": 20}
//这里的ok代表这个有没有这个key,
//如果有那么ok就是true,v就是对应的value
//如果没有ok就是false,v就是value数据类型默认值
v, ok := userInfo["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("没有这个元素")
}
//18
c, yes := userInfo["杨贵强"]
if yes {
fmt.Println(c)
} else {
fmt.Println("没有这个元素", c)
}
//没有这个元素 0
}
map遍历
遍历map有两种方式
package main
import "fmt"
func main() {
userInfo := map[string]int{"张三": 18, "李四": 19, "王五": 20}
//遍历出所有的key和对应的value
for k, v := range userInfo {
fmt.Println(k, v) //张三 18 李四 19 王五 20
}
//遍历所有的key
for s := range userInfo {
fmt.Println(s) //张三 李四 王五
}
}
移除map元素
package main
import "fmt"
func main() {
userInfo := map[string]int{"张三": 18, "李四": 19, "王五": 20}
//使用内置的delete函数可以移除map中的元素
//delete(map变量名称,key)
delete(userInfo, "王五")
for k, v := range userInfo {
fmt.Println(k, v)
/*
张三 18
李四 19
*/
}
}
按照指定顺序变量map
package main
import (
"fmt"
"sort"
)
func main() {
userInfo := make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("user:", i) //使用Sprintf进行拼接成string类型变量
value := i
userInfo[key] = value
}
//取出所有map的key存入切片
user := make([]string, 0, 200)
for k, _ := range userInfo {
user = append(user, k)
}
//对切片进行排序
sort.Strings(user)
//按照排序后的key遍历map
for i := range user {
fmt.Println(user[i], userInfo[user[i]])
}
}
元素为map类型的切片
package main
import "fmt"
func main() {
userInfo := make([]map[string]int, 0, 200)
userInfo = append(userInfo, map[string]int{"杨星辰": 18, "杨贵强": 19})
userInfo = append(userInfo, map[string]int{"武汉": 20, "美术": 22})
fmt.Println(userInfo[0]) //map[杨星辰:18 杨贵强:19]
userInfo[1]["张三"] = 20
fmt.Println(userInfo[1]) //map[张三:20 武汉:20 美术:22]
for k, v := range userInfo {
fmt.Println(k, v)
}
/*
0 map[杨星辰:18 杨贵强:19]
1 map[张三:20 武汉:20 美术:22]
*/
}
值为切片类型的map
package main
import "fmt"
func main() {
userinfo := make(map[string][]int, 200)
userinfo["张三"] = append(userinfo["张三"], 1)
userinfo["张三"] = append(userinfo["张三"], 2)
fmt.Println(userinfo["张三"]) //[1 2]
}
习题
统计出一段英文句子中每个单词出现的频率
package main
import (
"fmt"
"strings"
)
func main() {
a := "hello world hello nike what"
b := strings.Split(a, " ")
//如果我们使用双层for循环就会出线打印重复的hello
// 那么我们就使用map,因为map是不允许重复的
c := make(map[string]int)
for i := 0; i < len(b); i++ {
d := 0
for y := 0; y < len(b); y++ {
if b[i] == b[y] {
d += 1
}
}
c[b[i]] = d
}
fmt.Println(c)
//map[hello:2 nike:1 what:1 world:1]
}
结构体
go语言中有一些基本数据类型,如string,int,bool等数据类型
go语言中可以使用type关键字来定义自定义类型
go语言自定义类型是定义一个全新的类型,我们可以基于现有的基本数据类型定义,也可以通过struct定义
package main
func main() {
//将myInt定义为int类型
type myInt int
}
可以通过type关键字定义,Myint就是一种全新的类型,它具有int的特性
类型别名
类型别名是在go1.9版本后新添加的功能
类型别名规定:newInt只是Type的一个别名,本质上newInt与type是同一个类型,就想孩子小时候有小名,乳名,上学有学名,出国后有英文名字,但本质上这些名字都代表是他本人
package main
func main() {
//这里的type是数据类型例如int,string
type newInt = Type
}
我们的rune和byte底层就是用的自定义类型,但是本质还是int,只是长度不一样
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
数据类型的定义和别名区别
自定义类型定义和类型别名看上去就是一个“=”的区别,但是还是有差别的。
package main
import "fmt"
// 定义一个自定义类型
type myInt int
// 定义一个类型别名
type newInt = int
func main() {
var a myInt
var b newInt
fmt.Printf("%T", a) //main.myInt
fmt.Printf("%T", b) //int
}
结果显示自定义类型定义是main包下面的myint类型,但是类型别名newint的类型是int,
newint只是在代码中存在,但是在编译的时候会自动转换为原始类型。
结构体定义
go语言中的结构体,就类似于Java中的实体类
使用type和struct关键字来定义结构体
package main
import "fmt"
func main() {
//定义一个结构体,user对象,里面有三个属性
type user struct {
name string
age int
address string
}
//然后创建一个对象
zhangsan := user{"张三", 18, "武汉"}
//打印对象
fmt.Println(zhangsan) //{张三 18 武汉}
}
这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型
结构体实例化
package main
import "fmt"
func main() {
//定义一个结构体,user对象,里面有三个属性
type user struct {
name string
age int
address string
}
//我们可以先实例化,然后在使用面向对象.属性进行赋值
p1 := user{}
p1.name = "杨星辰"
p1.age = 19
p1.address = "武汉"
fmt.Printf("%T", p1) //main.user
fmt.Println(p1) //{杨星辰 19 武汉}
}
匿名结构体
匿名结构体主要是在临时数据结构等场景
package main
func main() {
//匿名结构体
var user struct {
name string
age int
address string
}
user.name = "zhangsan"
user.age = 18
user.address = "武汉"
}
指针结构体类型
package main
import "fmt"
func main() {
type user struct {
name string
age int
address string
}
userinfo := new(user)
fmt.Printf("%T", userinfo) //*main.user
//在go语言中,指针创建的结构体是可以直接.成员属性进行赋值
userinfo.name = "zhangsan"
userinfo.age = 19
userinfo.address = "wuhan"
fmt.Println(userinfo) //&{zhangsan 19 wuhan}
}
取结构体地址实例化
使用**&对结构体进行取地址操作相当于对该结构体类型进行了一次new**实例化操作。
package main
import "fmt"
func main() {
type user struct {
name string
age int
address string
}
//使用&取结构体地址值,就相当于new实例化操作
p1 := &user{}
fmt.Println(p1) //&{ 0 }
p1.name = "张三"
p1.age = 19
p1.address = "武汉"
fmt.Printf("%T", p1) //*main.user
fmt.Println(p1) //&{张三 19 武汉}
}
结构体初始化
进行初始化,但是还没有赋值,那么属性值就是数据类型的默认值
package main
import "fmt"
func main() {
type user struct {
name string
age int
address string
}
//进行初始化,但是还没有赋值,那么属性值就是数据类型的默认值
p1 := &user{}
fmt.Println(p1) //&{ 0 }
}
结构体初始化赋值
package main
import "fmt"
func main() {
type user struct {
name string
age int
address string
}
//结构体初始化的时候就赋值
p1 := user{
name: "杨贵强",
age: 18,
address: "武汉",
}
fmt.Println(p1)//&{杨贵强 18 武汉}
}
使用指针进行键值对初始化
package main
func main() {
type user struct {
name string
age int
address string
}
//使用指针初始化
p2 := &user{
name: "杨贵强",
age: 18,
address: "武汉",
}
}
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
package main
import "fmt"
func main() {
type user struct {
a int8
b int8
c int8
d int8
}
//可以指定某些属性进行初始化
p1 := user{
a: 1,
b: 2,
}
fmt.Println(p1) //{1 2 0 0}
}
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
package main
func main() {
type user struct {
name string
age int
address string
}
//使用指针初始化
p2 := &user{"杨贵强",18,"武汉"}
fmt.Println(p2)//*{"杨贵强",18,"武汉"}
}
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
结构体中属性内存布局
结构体中属性的内存布局其实都是连续的,都在一起
package main
import "fmt"
func main() {
type user struct {
a int8
b int8
c int8
d int8
}
//结构体初始化的时候就赋值
p1 := &user{1, 2, 3, 4}
fmt.Println(&p1.a) //0xc0000180b8
fmt.Println(&p1.b) //0xc0000180b9
fmt.Println(&p1.c) //0xc0000180ba
fmt.Println(&p1.d) //0xc0000180bb
}
构造函数
go语言中没有构造函数,我们可以自己实现一个
这里我们定义一个user类型,然后写一个函数返回user对象
package main
import "fmt"
func main() {
p1 := userInfo("杨贵强", 19, "武汉")
fmt.Println(p1) //&{杨贵强 19 武汉}
}
type user struct {
name string
age int
address string
}
// 定义一个构造函数,因为struct是值拷贝,如果属性比较多,可能会对性能照成影响,所以我们返回指针地址
func userInfo(name string, age int, address string) *user {
return &user{name: name, age: age, address: address}
}
结构体方法
这里就像是实体类中的方法,java中有tostring方法等等
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于java语言中的this
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
其中,
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
- 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
package main
import "fmt"
func main() {
p1 := userInfo("杨贵强", 19, "武汉")
//实例化对象直接.方法名称即可调用
p1.userMethod()
}
type user struct {
name string
age int
address string
}
// 定义一个构造函数,因为struct是值拷贝,如果属性比较多,可能会对性能照成影响,所以我们返回指针地址
func userInfo(name string, age int, address string) *user {
return &user{name: name, age: age, address: address}
}
// 这样定义就像是java中实体类里面的定义的方法,例如tostring方法,
func (user user) userMethod() {
fmt.Println("名字", user.name, ",年龄", user.age, ",地址", user.address)
}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
结构体指针方法
如果方法传入的参数是(user *user) 那么就是引用传递,传递的是(user user)那么就是值传递
package main
import "fmt"
func main() {
p1 := userInfo("杨贵强", 19, "武汉")
fmt.Println(p1) //&{杨贵强 19 武汉}
//实例化对象直接.方法名称即可调用
p1.updateAge(10)
fmt.Println(p1) //&{杨贵强 10 武汉}
}
type user struct {
name string
age int
address string
}
func userInfo(name string, age int, address string) *user {
return &user{name: name, age: age, address: address}
}
// 这里如果是user user那么就是值传递,如果这里是user *user 就是指针的引用传递,会修改这个引用
func (user *user) updateAge(age int) {
user.age = age
}
什么时候用指针什么时候用值传递
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
自定义类型添加方法
在Go语言中,接收者的类型可以是任何方法,不仅仅是结构体(实体类),任何类型都可以
自定义一个类型,然后函数传入
package main
import "fmt"
type myInt int
func main() {
var a myInt
a.save()
fmt.Println(a) //10
}
//函数传递
func (myint *myInt) save() {
*myint = 10
}
注意事项:
非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
结构体匿名属性
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个
package main
import "fmt"
// 匿名结构体属性,数据类型不可以重复
type user struct {
string
int
bool
}
func main() {
p := &user{"ygq", 18, true}
fmt.Println(p) //&{ygq 18 true}
}
嵌套结构体
嵌套结构体,顾名思义就是结构体里面包含结构体
package main
import "fmt"
// 匿名结构体属性,数据类型不可以重复
type student struct {
name string
//结构体
userinfo user
}
type user struct {
name string
age int
}
func main() {
p := &student{}
//直接可以赋值一个结构体
p.userinfo = user{"张三", 18}
p.name = "杨贵强"
fmt.Println(p) //&{杨贵强 {张三 18}}
//可以直接使用面向对象的.嵌套的结构体.属性
fmt.Println(p.userinfo.name) //张三
fmt.Println(p.userinfo.age) //18
}
基础
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
package main
import "fmt"
type Animal struct {
name string
}
func (animal *Animal) move() {
fmt.Println(animal.name, "会跑")
}
func (Animal *Animal) wang() {
fmt.Println(Animal.name, "会叫")
}
type dog struct {
Feet int8
Animal *Animal
}
func main() {
d := dog{}
d.Feet = 4
d.Animal = &Animal{name: "小黑"}
d.Animal.move() //小黑 会跑
d.Animal.wang() //小黑 会叫
}
结构体的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号**""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,**分隔。
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
json结构体属性别名
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1"; key2:"value2"`
结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。
注意事项:
为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student结构体的每个字段定义json序列化时使用的Tag:
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "沙河娜扎",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}
包
我们还可以根据自己的需要创建自己的包。一个包可以简单理解为一个存放**.go**文件的文件夹。
该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。
注意事项:
- 一个文件夹下面只能有一个包,同样一个包的文件不能在多个文件夹下。
- 包名可以不和文件夹的名字一样,包名不能包含**-**符号。
- 包名为main的包为应用程序的入口包,编译时不包含main包的源代码时不会得到可执行文件。
包的可见性
如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。
package student
// 包变量可见性
var a = 10 //变量名首字母小写,外部看不见,是私有的,private
var Address = "武汉" //首字母大写, 外部包都可以获取到,是公共的,public
// 可见,
type User struct {
name string
age int
}
func Add(x int, y int) int {
return x + y
}
type Student struct {
Name string //外部包可以访问
class string //私有,当前包可以访问
}
type pay interface {
wxpay() //首字母小写,进行本包访问的方法
Alipay() //首字母大小,外部包可以访问
}
包的导入
user.go
package student
// 包变量可见性
var a = 10 //变量名首字母小写,外部看不见,是私有的,private
var Address = "武汉" //首字母大写, 外部包都可以获取到,是公共的,public
// 可见,
type User struct {
//如果自定义结构体名称是公开的,但是属性还是私有的就不能进行赋值
Name string
Age int
}
func Add(x int, y int) int {
return x + y
}
type Student struct {
Name string //外部包可以访问
class string //私有,当前包可以访问
}
type pay interface {
wxpay() //首字母小写,进行本包访问的方法
Alipay() //首字母大小,外部包可以访问
}
app.go
package main
import "fmt"
//最外层目录/子目录
//起别名,import 别名名称 包路径
import user "test/student"
func main() {
//直接包名.方法即可调用 引入的包中公开的方法和属性
m := user.Add(1, 2)
fmt.Println(m) //3
p := user.User{"张三", 18}
fmt.Println(p) //{张三 18}
fmt.Println(user.Address) //武汉
}
匿名导入包
如果你只希望导入包,但是不想使用
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。
import _ "test/student"
//这样就不会报错了,也吧这个包引入进来了
init初始化函数
在Go语言程序执行时导入包语句会自动触发包内部**init()**函数的调用。需要注意的是:
**init()**函数没有参数也没有返回值。
**init()**函数在程序运行时自动被调用执行,不能在代码中主动调用它。
package main
import "fmt"
var a = 10
func main() {
fmt.Println("我是main")
}
func init() {
fmt.Println(a)
fmt.Println(name)
}
const name = 200
10
200
我是main
从而得知,执行顺序是
init()函数执行顺序
Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
在运行时,被最后导入的包会最先初始化并调用其**init()**函数, 如下图示:
试验一下
boss包
package boss
import "fmt"
func init() {
fmt.Println("我是boss包")
}
student包
package student
import "fmt"
//引入了boss包
import _ "test/boss"
func init() {
fmt.Println("我是student包")
}
main包
package main
import "fmt"
//引入student包
import _ "test/student"
var a = 10
func main() {
fmt.Println("我是main")
}
func init() {
fmt.Println(a)
fmt.Println(name)
}
const name = 200
我是boss包
我是student包
10
200
我是main
接口
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。
package main
import "fmt"
type dog struct {
name string
}
type cat struct {
name string
}
//定义方法
type Student interface {
eat()
}
func main() {
d := dog{"小黑"}
c := cat{"小MI"}
d.eat()
c.eat()
}
// 接口使用和结构体的方法一样
func (d dog) eat() {
fmt.Println(d, "爱吃饭")
}
func (c cat) eat() {
fmt.Println(c, "爱吃饭")
}
接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。
接口类型变量
接口类型变量就是将这个接口的实现赋值给另外一个变量
package main
import "fmt"
type dog struct {
name string
}
type cat struct {
name string
}
type Student interface {
eat()
}
func main() {
d := dog{"小黑"}
c := cat{"小MI"}
d.eat()
c.eat()
//声明一个接口类型的变量
var x Student
//然后将实现接口的对象赋值给它
x = d
//最后也可以直接调用
x.eat()
}
// 接口使用和结构体的方法一样
func (d dog) eat() {
fmt.Println(d, "爱吃饭")
}
func (c cat) eat() {
fmt.Println(c, "爱吃饭")
}
值接收者和指针接收者区别
package main
import "fmt"
type dog struct {
name string
}
type cat struct {
name string
}
type Student interface {
eat()
}
func main() {
var x Student
d := dog{"小黑"}
d.eat()
//赋值指针,会自动根据指针的地址找到对应的cat结构体赋值
c := &cat{"小MI"}
c.eat()
x = c
x.eat()
}
// 接口使用和结构体的方法一样
func (d dog) eat() {
fmt.Println(d, "爱吃饭")
}
func (c cat) eat() {
fmt.Println(c, "爱吃饭")
}
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖
指针接收者实现接口
package main
import "fmt"
type dog struct {
name string
}
type Student interface {
eat()
}
func main() {
//x是里面接口eat,是指针传道者,赋值给x只能是指针赋值
var x Student
d := dog{"小黑"}
d.eat()//&{小黑} 爱吃饭
//x = d //因为d是值类型,不是指针类型所以不能赋值给x。
c := &dog{"小王"}
x = c
x.eat() //&{小王} 爱吃饭
}
// 接口使用和结构体的方法一样
func (d *dog) eat() {
fmt.Println(d, "爱吃饭")
}
空接口的应用
使用空接口实现可以接收任意类型的函数参数。
func show(a interface{}) {
fmt.Println(a)
}
空接口作为map的value
使用空接口实现可以保存任意值的字典。
//这个时候我们的value就可以是任何类型的
package main
func main() {
//我们使用接口创建一个map,key为string,value为接口类型
user := make(map[string]interface{})
//这个时候我们的value就可以是任何类型的
user["张三"] = "1"
user["李四"] = 2
user["王五"] = true
}
类型断言
空接口可以保存任意类型的值,那我们应该如何获取存储的具体数据呢
其中:
- x:表示类型为**interface{}**的变量
- T:表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。
package main
import "fmt"
func main() {
var a interface{}
a = 1
//使用a,(数据类型),判断是否断言成功
value, ok := a.(string)
if ok {
fmt.Println(value)
} else {
fmt.Println("断言失败") //执行
}
}
如果判断多个类型可以使用switch循环来实现
package main
import "fmt"
func main() {
//定义一个接口
var a interface{}
//给接口赋值
a = 1
duanYan(a)//是int类型 1
}
func duanYan(a interface{}) {
//a.(type)就是获取当a的类型
switch v := a.(type) {
case string:
fmt.Println("是string类型", v)
case int:
fmt.Println("是int类型", v)
case bool:
fmt.Println("是bool类型", v)
default:
fmt.Println("未知类型", v)
}
}
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
并发
goroutine
简单goroutine
启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go
关键字。启动一个轻量级线程
package main
import (
"fmt"
)
func hello() {
fmt.Println("hello")
}
func main() {
hello()
fmt.Println("main")//hello main
//这里是串行,执行完成hello在执行main
}
这个示例中没有使用goroutine,那么程序就是从上到下执行,属于串行,先执行main方法里面的hello方法在执行fmt.println
package main
import (
"fmt"
)
func hello() {
fmt.Println("hello")
}
func main() {
go hello()
fmt.Println("main")//main
}
我们执行后就只打印了main字符串,线程hello方法里面的没有执行打印 那是为什么呢?
- 在goroutine的时候go会给main默认的线程
- 当main函数执行的时候main线程就结束了,所以在main函数中启动的线程会一同结束,main函数的线程就相当于是游戏中的母体,那么其他线程就是小怪,如果母体死亡后,那么其他小怪都会一起死亡。
- 所以我们如果想让main函数和hello函数都执行,那么我们可以在main函数运行完成的时候等待一会,让hello也执行完成
- 让main函数等待我们可以使用time.Sleep(时间毫秒单位)
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("hello")
}
func main() {
go hello()
fmt.Println("main")
time.Sleep(500)
}
/*
main
hello
*/
执行上面的代码你会发现,这一次先打印main goroutine done!
,然后紧接着打印Hello Goroutine!
。
首先为什么会先打印main
是因为我们在创建新的线程的时候需要花费一些时间,而此时main函数所在的线程
是继续执行的。
启动多个goroutine
在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine
。让我们再来一个例子:
(这里使用了sync.WaitGroup
来实现goroutine的同步)
package main
import (
"fmt"
"sync"
)
// 引入一个计数器,
var sync1 sync.WaitGroup
func hello(x int) {
//将计数器放进延迟队列中,先进后出
defer sync1.Done()
fmt.Println(x)
}
func main() {
for i := 0; i < 10; i++ {
//计数器+1
sync1.Add(1)
hello(i)
}
//执行延迟队列,为0的时候执行wait
sync1.Wait()
}
/*
9
4
2
3
6
5
7
0
8
*/
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine
是并发执行的,而goroutine
的调度是随机的。
goroutine与线程
可增长栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine
的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine
的栈不是固定的,他可以按需增大和缩小,goroutine
的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine
也是可以的。
gorunine调度
Go运行时的调度器使用GOMAXPROCS
参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()
函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("b", i)
}
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}
两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。
将逻辑核心数设为2,此时两个任务并行执行,代码如下。
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("b", i)
}
}
func main() {
//设置执行核心数
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
Go语言中的操作系统线程和goroutine的关系:
- 一个操作系统线程对应用户态多个goroutine。
- go程序可以同时使用多个操作系统线程。
- goroutine和OS线程是多对多的关系,即m:n。
channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine
中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes)
,提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine
是Go程序并发的执行体,channel
就是它们之间的连接。channel
是可以让一个goroutine
发送特定值到另一个goroutine
的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel创建
channel是引用类型,所以默认值是nil
var name chan Type
package main
import "fmt"
func main() {
//创建一个int类型的channel
var ch chan int
fmt.Println(ch) //nil
}
声明的通道后需要使用make
函数初始化之后才能使用。
创建channel的格式如下:
make(chan 元素类型, [缓冲大小])
package main
func main() {
//第一种
var ch chan int
ch = make(chan int)
//第二种
a := make(chan string)
//第三种
b := make(chan bool)
}
channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-
符号。
现在我们先使用以下语句定义一个通道:
创建channel
ch := make(chan int)
发送
想通道里面发送一个值
ch <- 10
接收
从一个通道中接收值
package main
func main() {
ch := make(chan int)
//想ch通道中添加一个值
ch <- 10
//从ch通道中取一个值赋值给a
a := <-ch
//从ch通道中取一个值,但是不用
<-ch
//关闭通道
close(ch)
}
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
无缓冲通道
无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:
package main
import "fmt"
func main() {
ch := make(chan int)
//想ch通道中添加一个值
ch <- 10
fmt.Println(ch)
}
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
F:/学习笔记/GoLang/test/app.go:8 +0x36
为什么会出现deadlock
错误呢?
因为我们使用ch := make(chan int)
创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
package main
import (
"fmt"
"time"
)
func hello(ch chan int) {
//从通道中取值
a := <-ch
fmt.Println("取值成功", a)
}
func main() {
ch := make(chan int)
//向ch通道中添加一个值,这里要用线程去取
//启动goroutine从通道中取值
go hello(ch)
ch <- 10
time.Sleep(500)
}
无缓冲通道的发送操作会阻塞,直到另外一个goroutine在该通道上执行接受操作,这时值才能发送成功,两个goroutine将继续执行,相反,如果接收方先执行,那么接收方的goroutine将阻塞,直到另外一个发送方在通道上发送一个值。
使用无缓冲通道进行通信将导致发送方和接收方同步化,因此,无缓冲通道又叫同步通道。
有缓存通道
解决无缓冲通道的阻塞问题就是使用有缓存通道。我们可以使用make函数初始化通道,并且指定一个通道容量。、
package main
import (
"fmt"
)
func main() {
//使用make初始化通道,make(chan Type,通道缓存区容量)
ch := make(chan int, 1) //这里的1就表示缓冲区容量为1,一次只能存一个,取出后即可继续存
ch <- 10
//取出值
a := <-ch
fmt.Println("取出成功", a) //取出成功 10
//继续存
ch <- 10
fmt.Println("发送值成功") //取出成功 10
}
只要通道大于0,那么通道就是有缓冲区的通,通道容量就是通道中可以存放多少个元素数量,就想小区快递柜一样,只能固定数量的格子,多了就装不下了,只有取出一个,才能再往里面塞一个。
们可以使用内置的len
函数获取通道内元素的数量,使用cap
函数获取通道的容量
如何优雅的从通道循环取值
当通过通道发送有限的数据时,我们可以通过close
函数关闭通道来告知从该通道接收值的goroutine
停止等待。当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
//开启一个goroutine将0~100的值发送给ch1
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
//开启一个goroutine从ch1中接收值,并将该值²发送到ch2中
go func() {
for {
k, v := <-ch1 //通道关闭后在取值就是v=false
if v {
ch2 <- k * k
}
}
close(ch2)
}()
//在主goroutine中从ch2中取值打印
for i := range ch2 { //通道关闭后会停止循环
fmt.Println(i)
}
}
从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是for range
的方式。
单向通道
package main
import "fmt"
// <-chan 只能发送值,chan<- 只能接收值
func a(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
// <-chan 只能发送值,chan<- 只能接收值
func b(read <-chan int, out chan<- int) {
//接收read,发送给out
for i := range read {
out <- i * i
}
close(out)
}
// 接收值
func c(read <-chan int) {
for i := range read {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
//启动两个goroutine发送和接收值
go a(ch1)
go b(ch1, ch2)
//主函数去打印,不用goroutine
c(ch2)
}
在形参中
-
chan<- int
是一个只能发送的通道,可以发送但是不能接收;
-
<-chan int
是一个只能接收的通道,可以接收但是不能发送。
在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。
通道总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-akYTUHLU-1672802020537)(Go学习笔记.assets/image-20230104104035491.png)]
关闭已经关闭的channel
也会引发panic
。
goroutine(池)
package main
import (
"fmt"
"time"
)
func worker(i int, read <-chan int, out chan<- int) {
for i2 := range read {
fmt.Println("i:", i, "read:", i2)
time.Sleep(time.Second)
fmt.Println("i:", i, "read:", i2)
out <- i2 * 2
}
}
func main() {
ch1 := make(chan int, 100)
ch2 := make(chan int, 100)
//开启三个goroutine
for i := 1; i <= 3; i++ {
go worker(i, ch1, ch2)
}
//五个任务
for i := 1; i < 5; i++ {
ch1 <- i
}
close(ch1)
for i := 1; i < 5; i++ {
<-ch2
}
}
select多路复用
GoWeb
Gin
创建服务
package main
//引入依赖
import "github.com/gin-gonic/gin"
func main() {
//创建一个服务
server := gin.Default()
//定义一个请求
server.GET("/hell", func(context *gin.Context) {
//context就是上下文可以接受参数和返回数据
context.JSON(200, gin.H{"msg": "你好"})
})
//启动服务并且设置端口
server.Run(":8082")
}
Restful API
Gin框架也是支持restful开发的
get /user
post /user
put /user
delete /user
package main
//引入依赖
import "github.com/gin-gonic/gin"
func main() {
//创建一个服务
server := gin.Default()
//定义Restful请求
server.GET("/hell", func(context *gin.Context) {
context.JSON(200, gin.H{"msg": "GET"})
})
server.POST("/hell", func(context *gin.Context) {
context.JSON(200, gin.H{"msg": "POST"})
})
server.PUT("/hell", func(context *gin.Context) {
context.JSON(200, gin.H{"msg": "PUT"})
})
server.DELETE("/hell", func(context *gin.Context) {
context.JSON(200, gin.H{"msg": "DELETE"})
})
//启动服务并且设置端口
server.Run(":8082")
}
整合前端页面
目录
Go代码
package main
//引入依赖
import "github.com/gin-gonic/gin"
func main() {
//创建一个服务
server := gin.Default()
//整合前端
//配置前端模板目录
server.LoadHTMLGlob("templates/*")
//配置静态文件
server.Static("/static", "./static")
server.GET("/hell", func(context *gin.Context) {
context.HTML(200, "index.html", gin.H{"msg": "GoLang"})
})
//启动服务并且设置端口
server.Run(":8082")
}
css
h1{
color: red;
}