go的学习笔记
中文标准库文档:https://studygolang.com/pkgdoc
第一段代码
所有代码的主文件都是main.go,下面的代码直接在项目里面创建main.go运行
package main // 声明文件所在的包,每个go文件必须有归属的包
import "fmt" // 引入程序需要的包,为了使用包下的函数,比如Println
func main() { // 程序的主函数,程序的运行入口
fmt.Println("Hello World") // 在控制台打印输出一句话
}
如果是vscode等工具需要执行go run main.go
来运行,我这里使用的是goLand
右键直接运行了
注意事项:
- 源文件以
.go
为扩展名 - 程序的执行入口是main()函数
- 严格区分大小写
- 方法由一条条语句构成,每个语句后面不需要加分号,这也体现出go的简洁性
- Go编译器是一行行进行编译的,因此一行就写一条语句,不能把多个语句写在同一行,否则报错
- 定义的变量或者import的包如果没有使用到,代码不能编译通过
- 大括号都是成对出现的,缺一不可
变量的定义和使用
package main
import "fmt"
// 全局变量: 定义在函数外的变量
var n9 = 100
var n10 = 9.7
// 一次性定义多个全局变量
var (
n11 = 12
n12 = "你好"
)
func main() {
// 定义在{}中的变量叫: 局部变量
// 变量重复定义会报错
var name string // 声明变量和类型
name = "小明" // 赋值变量
fmt.Println(name) // 打印值
var age int = 18 // 声明变量和类型并赋值
fmt.Println(age)
var address = "北京" // 声明没指定类型时,根据后面的值的类型自动推断类型
//address = 12 // 报错,因为上面类型推断为string
fmt.Println(address)
stuName := "萧寂" // 声明和赋值
fmt.Println("学生姓名为: ", stuName) // 打印值
//count = 1 // 不能直接写等于号,除非:=或者var count = 1 必须要声明
fmt.Println("--------------------------------------------")
// 声明多个变量不赋值
var n1, n2, n3 int
fmt.Println(n1, n2, n3) // 不赋值情况下会打印每个类型的原始值
// 声明多个变量并赋值
var n4, n5, n6 = 14, "jack", 12.7
fmt.Println(n4, n5, n6) // 声明多个变量并赋值
n7, n8 := 12, "hahaha"
fmt.Println(n7, n8)
// 打印全局变量
fmt.Println(n9, n10, n11, n12)
}
数据类型介绍
- 基本数据类型
- 数值型
- 整数类型(
int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,byte
) - 浮点类型(
float32,float64
)
- 整数类型(
- 字符型(
没有单独的字符型,使用byte来保存单个字母字符
) - 布尔型(
bool
) - 字符串(
string
)
- 数值型
- 派生数据类型(也是复杂数据类型)
- 指针
- 数组
- 结构体
- 管道
- 函数
- 切片
- 接口
- map
整数类型
package main
import (
"fmt"
"unsafe"
)
func main() {
// 整数类型
// 定义一个整数类型
//var num1 int8 = 230 // 报错(230超出int8范围)
//fmt.Println(num1)
// 定义整数类型
var num2 = -20
fmt.Println(num2)
// 打印类型
// 其他占位符的使用: https://www.jianshu.com/p/66aaf908045e
fmt.Printf("num2的类型是: %T", num2)
fmt.Println() // 换行作用
fmt.Println(unsafe.Sizeof(num2)) // 打印字节数
// 定义byte类型
var num3 byte = 20
fmt.Println(num3)
}
浮点类型
package main
import "fmt"
func main() {
// 定义浮点类型的数据
var num1 float32 = 3.14
var num2 float64 = 3.15
fmt.Println(num1, num2)
}
字符类型
package main
import "fmt"
func main() {
// 定义字符类型的数据
var c1 byte = 'a'
var c2 byte = 'b'
var c3 byte = '('
var c4 byte = '6'
var c5 int = '中' // 底层是Unicode编码
fmt.Println(c1, c2, c3, c4, c5)
}
布尔类型
package main
import "fmt"
func main() {
// 测试布尔类型的数值
var flag1 bool = true
var flag2 bool = false
var flag3 = 5 < 9
fmt.Println(flag1, flag2, flag3)
}
字符串类型
package main
import "fmt"
func main() {
// 测试布尔类型的数值
var str string = "你好"
var str2 string = "世界"
fmt.Println(str)
fmt.Println(str+str2)
}
基本数据类型之间的转换
package main
import "fmt"
func main(){
//进行类型转换:
var n1 int = 100
//var n2 float32 = n1 在这里自动转换不好使,比如显式转换
fmt.Println(n1)
//fmt.Println(n2)
var n2 float32 = float32(n1)
fmt.Println(n2)
//注意:n1的类型其实还是int类型,只是将n1的值100转为了float32而已,n1还是int的类型
fmt.Printf("%T",n1) //int
fmt.Println()
//将int64转为int8的时候,编译不会出错的,但是会数据的溢出
var n3 int64 = 888888
var n4 int8 = int8(n3)
fmt.Println(n4)//56
var n5 int32 = 12
var n6 int64 = int64(n5) + 30 //一定要匹配=左右的数据类型
fmt.Println(n5)
fmt.Println(n6)
var n7 int64 = 12
var n8 int8 = int8(n7) + 127 //编译通过,但是结果可能会溢出
//var n9 int8 = int8(n7) + 128 //编译不会通过
fmt.Println(n8)
//fmt.Println(n9)
}
基本数据类型转string
package main
import "fmt"
func main(){
var n1 int = 19
var n2 float32 = 4.78
var n3 bool = false
var n4 byte = 'a'
var s1 string = fmt.Sprintf("%d",n1)
fmt.Printf("s1对应的类型是:%T ,s1 = %q \n",s1, s1)
var s2 string = fmt.Sprintf("%f",n2)
fmt.Printf("s2对应的类型是:%T ,s2 = %q \n",s2, s2)
var s3 string = fmt.Sprintf("%t",n3)
fmt.Printf("s3对应的类型是:%T ,s3 = %q \n",s3, s3)
var s4 string = fmt.Sprintf("%c",n4)
fmt.Printf("s4对应的类型是:%T ,s4 = %q \n",s4, s4)
}
string转基本数据类型
package main
import (
"fmt"
"strconv"
)
func main() {
//string-->bool
var s1 string = "true"
var b bool
//ParseBool这个函数的返回值有两个:(value bool, err error)
//value就是我们得到的布尔类型的数据,err出现的错误
//我们只关注得到的布尔类型的数据,err可以用_直接忽略
b, _ = strconv.ParseBool(s1)
fmt.Printf("b的类型是:%T,b=%v \n", b, b)
//string---》int64
var s2 string = "19"
var num1 int64
num1, _ = strconv.ParseInt(s2, 10, 64)
fmt.Printf("num1的类型是:%T,num1=%v \n", num1, num1)
//string-->float32/float64
var s3 string = "3.14"
var f1 float64
f1, _ = strconv.ParseFloat(s3, 64)
fmt.Printf("f1的类型是:%T,f1=%v \n", f1, f1)
//注意:string向基本数据类型转换的时候,一定要确保string类型能够转成有效的数据类型,否则最后得到的结果就是按照对应类型的默认值输出
var s4 string = "golang"
var b1 bool
b1, _ = strconv.ParseBool(s4)
fmt.Printf("b1的类型是:%T,b1=%v \n", b1, b1)
var s5 string = "golang"
var num2 int64
num2, _ = strconv.ParseInt(s5, 10, 64)
fmt.Printf("num2的类型是:%T,num2=%v \n", num2, num2)
}
指针
package main
import (
"fmt"
)
func main() {
var age int = 18
//&符号+变量 就可以获取这个变量内存的地址
fmt.Println(&age) //0xc0000a2058
//定义一个指针变量:
//var代表要声明一个变量
//ptr 指针变量的名字
//ptr对应的类型是:*int 是一个指针类型 (可以理解为 指向int类型的指针)
//&age就是一个地址,是ptr变量的具体的值
var ptr *int = &age
fmt.Println(ptr)
fmt.Println("ptr本身这个存储空间的地址为:", &ptr)
//想获取ptr这个指针或者这个地址指向的那个数据:
fmt.Printf("ptr指向的数值为:%v", *ptr) //ptr指向的数值为:18
}
运算符
算术运算符
package main
import "fmt"
func main() {
//+加号:
//1.正数 2.相加操作 3.字符串拼接
var n1 int = +10
fmt.Println(n1)
var n2 int = 4 + 7
fmt.Println(n2)
var s1 string = "abc" + "def"
fmt.Println(s1)
// /除号:
fmt.Println(10 / 3) //两个int类型数据运算,结果一定为整数类型
fmt.Println(10.0 / 3) //浮点类型参与运算,结果为浮点类型
// % 取模 等价公式: a%b=a-a/b*b
fmt.Println(10 % 3) // 10%3= 10-10/3*3 = 1
fmt.Println(-10 % 3)
fmt.Println(10 % -3)
fmt.Println(-10 % -3)
//++自增操作:
var a int = 10
a++
fmt.Println(a)
a--
fmt.Println(a)
//++ 自增 加1操作,--自减,减1操作
//go语言里,++,--操作非常简单,只能单独使用,不能参与到运算中去
//go语言里,++,--只能在变量的后面,不能写在变量的前面 --a ++a 错误写法
}
赋值运算符
package main
import "fmt"
func main() {
var num1 int = 10
fmt.Println(num1)
var num2 int = (10+20)%3 + 3 - 7 //=右侧的值运算清楚后,再赋值给=的左侧
fmt.Println(num2)
var num3 int = 10
num3 += 20 //等价num3 = num3 + 20;
fmt.Println(num3)
//练习:交换两个数的值并输出结果:
var a int = 8
var b int = 4
fmt.Printf("a = %v,b = %v", a, b)
//交换:
//引入一个中间变量:
var t int
t = a
a = b
b = t
fmt.Printf("a = %v,b = %v", a, b)
}
关系运算符
package main
import "fmt"
func main() {
fmt.Println(5 == 9) //判断左右两侧的值是否相等,相等返回true,不相等返回的是false, ==不是=
fmt.Println(5 != 9) //判断不等于
fmt.Println(5 > 9)
fmt.Println(5 < 9)
fmt.Println(5 >= 9)
fmt.Println(5 <= 9)
}
逻辑运算符
package main
import "fmt"
func main() {
//与逻辑:&& :两个数值/表达式只要有一侧是false,结果一定为false
//也叫短路与:只要第一个数值/表达式的结果是false,那么后面的表达式等就不用运算了,直接结果就是false -->提高运算效率
fmt.Println(true && true)
fmt.Println(true && false)
fmt.Println(false && true)
fmt.Println(false && false)
//或逻辑:||:两个数值/表达式只要有一侧是true,结果一定为true
//也叫短路或:只要第一个数值/表达式的结果是true,那么后面的表达式等就不用运算了,直接结果就是true -->提高运算效率
fmt.Println(true || true)
fmt.Println(true || false)
fmt.Println(false || true)
fmt.Println(false || false)
//非逻辑:取相反的结果:
fmt.Println(!true)
fmt.Println(!false)
}
获取用户终端输入数据
package main
import "fmt"
func main() {
//实现功能:键盘录入学生的年龄,姓名,成绩,是否是VIP
//方式1:Scanln
var age int
// fmt.Println("请录入学生的年龄:")
//传入age的地址的目的:在Scanln函数中,对地址中的值进行改变的时候,实际外面的age被影响了
//fmt.Scanln(&age)//录入数据的时候,类型一定要匹配,因为底层会自动判定类型的
var name string
// fmt.Println("请录入学生的姓名:")
// fmt.Scanln(&name)
var score float32
// fmt.Println("请录入学生的成绩:")
// fmt.Scanln(&score)
var isVIP bool
// fmt.Println("请录入学生是否为VIP:")
// fmt.Scanln(&isVIP)
//将上述数据在控制台打印输出:
//fmt.Printf("学生的年龄为:%v,姓名为:%v,成绩为:%v,是否为VIP:%v",age,name,score,isVIP)
//方式2:Scanf
fmt.Println("请录入学生的年龄,姓名,成绩,是否是VIP,使用空格进行分隔")
fmt.Scanf("%d %s %f %t", &age, &name, &score, &isVIP)
//将上述数据在控制台打印输出:
fmt.Printf("学生的年龄为:%v,姓名为:%v,成绩为:%v,是否为VIP:%v", age, name, score, isVIP)
}
流程控制语句
if语句
单分支
package main
import "fmt"
func main(){
//实现功能:如果口罩的库存小于30个,提示:库存不足:
//var count int = 100
//单分支:
// if count < 30 {
// fmt.Println("对不起,口罩存量不足")
// }
//if后面表达式,返回结果一定是true或者false,
//如果返回结果为true的话,那么{}中的代码就会执行
//如果返回结果为false的话,那么{}中的代码就不会执行
//if后面一定要有空格,和条件表达式分隔开来
//{}一定不能省略
//条件表达式左右的()是建议省略的
//在golang里,if后面可以并列的加入变量的定义:
if count := 20;count < 30 {
fmt.Println("对不起,口罩存量不足")
}
}
双分支
package main
import "fmt"
func main() {
//实现功能:如果口罩的库存小于30个,提示:库存不足,否则提示:库存充足
//定义口罩的数量:
var count int = 70
if count < 30 { //这个条件表达式返回的是true的话,后面{}执行了
fmt.Println("库存不足")
} else { //count >= 30
fmt.Println("库存充足")
}
//双分支一定会二选一走其中一个分支。
}
多分支
package main
import "fmt"
func main() {
//实现功能:根据给出的学生分数,判断学生的等级:
// >=90 -----A
// >=80 -----B
// >=70 -----C
// >=60 -----D
// <60 -----E
//方式1:利用if单分支实现:
//定义一个学生的成绩:
var score int = 18
//对学生的成绩进行判定:
// if score >= 90 {
// fmt.Println("您的成绩为A级别")
// }
// if score >= 80 && score < 90 {
// fmt.Println("您的成绩为B级别")
// }
// if score >= 70 && score < 80 {
// fmt.Println("您的成绩为C级别")
// }
// if score >= 60 && score < 70 {
// fmt.Println("您的成绩为D级别")
// }
// if score < 60 {
// fmt.Println("您的成绩为E级别")
// }
//上面方式1利用多个单分支拼凑出多个选择,多个选择是并列的,依次从上而下顺序执行,即使走了第一个分支,那么其它分支也是需要判断
//方式2:多分支:优点:如果已经走了一个分支了,那么下面的分支就不会再去判断执行了
// if score >= 90 {
// fmt.Println("您的成绩为A级别")
// } else if score >= 80 {//else隐藏:score < 90
// fmt.Println("您的成绩为B级别")
// } else if score >= 70 {//score < 80
// fmt.Println("您的成绩为C级别")
// } else if score >= 60 {//score < 70
// fmt.Println("您的成绩为D级别")
// } else {//score < 60
// fmt.Println("您的成绩为E级别")
// } //建议你保证else的存在,只有有了else才会真正 起到多选一 的效果
if score > 10 {
fmt.Println("aaa")
} else if score > 6 {
fmt.Println("bbb")
}
}
switch语句
package main
import "fmt"
func main() {
//实现功能:根据给出的学生分数,判断学生的等级:
// >=90 -----A
// >=80 -----B
// >=70 -----C
// >=60 -----D
// <60 -----E
//给出一个学生分数:
var score int = 187
//根据分数判断等级:
//switch后面是一个表达式,这个表达式的结果依次跟case进行比较,满足结果的话就执行冒号后面的代码。
//default是用来“兜底”的一个分支,其它case分支都不走的情况下就会走default分支
//default分支可以放在任意位置上,不一定非要放在最后。
switch score / 10 {
case 10:
fmt.Println("您的等级为A级")
case 9:
fmt.Println("您的等级为A级")
case 8:
fmt.Println("您的等级为B级")
case 7:
fmt.Println("您的等级为C级")
case 6:
fmt.Println("您的等级为D级")
case 5:
fmt.Println("您的等级为E级")
case 4:
fmt.Println("您的等级为E级")
case 3:
fmt.Println("您的等级为E级")
case 2:
fmt.Println("您的等级为E级")
case 1:
fmt.Println("您的等级为E级")
case 0:
fmt.Println("您的等级为E级")
default:
fmt.Println("您的成绩有误")
}
}
注意事项:
- switch后是一个表达式(即:常量值、变量、一个有返回值的函数等都可以)
- case后面的值如果是常量值(字面量),则要求不能重复
- case后的各个值的数据类型,必须和 switch 的表达式数据类型一致
- case后面可以带多个值,使用逗号间隔。比如 case 值1,值2...
- case后面不需要带break
- default语句不是必须的,位置也是随意的。
- switch后也可以不带表达式,当做if分支来使用
- switch后也可以直接声明/定义一个变量,分号结束,不推荐
- switch穿透,利用fallthrough关键字,如果在case语句块后增加fallthrough ,则会继续执行下一个case,也叫switch穿透。
循环
for循环
go语言只有for循环
package main
import "fmt"
func main() {
//实现一个功能:求和: 1+2+3+4+5:
//求和:
//利用for循环来解决问题:
var sum int = 0
for i := 1; i <= 5; i++ {
sum += i
}
//输出结果:
fmt.Println(sum)
// for循环的语法格式:
// for 初始表达式; 布尔表达式(条件判断); 迭代因子 {
// 循环体;--》反复重复执行的内容
// }
// 注意:for的初始表达式 不能用var定义变量的形式,要用:=
// 注意:for循环实际就是让程序员写代码的效率高了,但是底层该怎么执行还是怎么执行的,底层效率没有提高,只是程序员写代码简洁了而已
}
细节1:格式灵活
package main
import "fmt"
func main() {
i := 1 //变量的初始化
for i <= 5 { //条件表达式。判断条件
fmt.Println("你好 Golang") //循环体
i++ //迭代
}
}
细节2:死循环
package main
import "fmt"
func main() {
//死循环:
// for {
// fmt.Println("你好 Golang")
// }
for {
fmt.Println("你好 Golang")
}
}
for renge键值对循环
(键值循环) for range结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道,for range 语法上类似于其它语言中的 foreach 语句
package main
import "fmt"
func main() {
//定义一个字符串:
var str string = "hello golang你好"
//方式1:普通for循环:按照字节进行遍历输出的 (暂时先不使用中文)
// for i := 0;i < len(str);i++ {//i:理解为字符串的下标
// fmt.Printf("%c \n",str[i])
// }
//方式2:for range
for i, value := range str {
fmt.Printf("索引为:%d,具体的值为:%c \n", i, value)
}
//对str进行遍历,遍历的每个结果的索引值被i接收,每个结果的具体数值被value接收
//遍历对字符进行遍历的
}
关键字
break
- switch分支中,每个case分支后都用break结束当前分支,但是在go语言中break可以省略不写。
- break可以结束正在执行的循环
- break的作用结束离它最近的循环
package main
import "fmt"
func main(){
//功能:求1-100的和,当和第一次超过300的时候,停止程序
var sum int = 0
for i := 1 ; i <= 100 ; i++ {
sum += i
fmt.Println(sum)
if sum >= 300 {
//停止正在执行的这个循环:
break
}
}
fmt.Println("-----ok")
}
深入理解
package main
import "fmt"
func main(){
//双重循环:
for i := 1; i <= 5; i++ {
for j := 2; j <= 4; j++ {
fmt.Printf("i: %v, j: %v \n",i,j)
if i == 2 && j == 2 {
break
}
}
}
}
标签的使用展示
package main
import "fmt"
func main() {
//双重循环:
label2:
for i := 1; i <= 5; i++ {
for j := 2; j <= 4; j++ {
fmt.Printf("i: %v, j: %v \n", i, j)
if i == 2 && j == 2 {
break label2 //结束指定标签对应的循环
}
}
}
fmt.Println("-----ok")
}
continue
- continue的作用是结束离它近的那个循环,继续离它近的那个循环
package main
import "fmt"
func main(){
//功能:输出1-100中被6整除的数:
//方式1:
// for i := 1; i <= 100; i++ {
// if i % 6 == 0 {
// fmt.Println(i)
// }
// }
//方式2:
for i := 1; i <= 100; i++ {
if i % 6 != 0 {
continue //结束本次循环,继续下一次循环
}
fmt.Println(i)
}
}
深入理解
package main
import "fmt"
func main(){
//双重循环:
for i := 1; i <= 5; i++ {
for j := 2; j <= 4; j++ {
if i == 2 && j == 2 {
continue
}
fmt.Printf("i: %v, j: %v \n",i,j)
}
}
fmt.Println("-----ok")
}
标签的使用展示
package main
import "fmt"
func main() {
//双重循环:
label:
for i := 1; i <= 5; i++ {
for j := 2; j <= 4; j++ {
if i == 2 && j == 2 {
continue label
}
fmt.Printf("i: %v, j: %v \n", i, j)
}
}
fmt.Println("-----ok")
}
goto
- Golang的 goto 语句可以无条件地转移到程序中指定的行。
- goto语句通常与条件语句配合使用。可用来实现条件转移.
- 在Go程序设计中一般不建议使用goto语句,以免造成程序流程的混乱。
package main
import "fmt"
func main() {
fmt.Println("hello golang1")
fmt.Println("hello golang2")
if 1 == 1 {
goto label1 //goto一般配合条件结构一起使用
}
fmt.Println("hello golang3")
fmt.Println("hello golang4")
fmt.Println("hello golang5")
fmt.Println("hello golang6")
label1:
fmt.Println("hello golang7")
fmt.Println("hello golang8")
fmt.Println("hello golang9")
}
return
package main
import "fmt"
func main() {
for i := 1; i <= 100; i++ {
fmt.Println(i)
if i == 14 {
return //结束当前的函数
}
}
fmt.Println("hello golang")
}
函数
普通函数
- 遵循标识符命名规范:见名知意 addNum,驼峰命名addNum
- 首字母不能是数字
- 首字母大写该函数可以被本包文件和其它包文件使用(类似public)
- 首学母小写只能被本包文件使用,其它包文件不能使用(类似private)
- Golang中函数不支持重载
- Golang中支持可变参数 (如果你希望函数带有可变数量的参数)
- 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值
- 以值传递方式的数据类型,如果希望在函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果来看类似引用传递。
- 在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
- 函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用(把函数本身当做一种数据类型)
- 为了简化数据类型定义,Go支持自定义数据类型 基本语法: type 自定义数据类型名 数据类型
可以理解为 : 相当于起了一个别名 例如:type mylnt int -----》这时mylnt就等价int来使用了. - 支持对函数返回值命名传统写法要求:返回值和返回值的类型对应,顺序不能差
package main
import "fmt"
// func 函数名(形参列表)(返回值类型列表){
// 执行语句..
// return + 返回值列表
// }
//
// 自定义函数:功能:两个数相加:
func cal(num1 int, num2 int) int { //如果返回值类型就一个的话,那么()是可以省略不写的
var sum int = 0
sum += num1
sum += num2
return sum
}
func main() {
//功能:10 + 20
//调用函数:
sum := cal(10, 20)
fmt.Println(sum)
// var num1 int = 10
// var num2 int = 20
//求和:
// var sum int = 0
// sum += num1
// sum += num2
// fmt.Println(sum)
//功能:30 + 50
var num3 int = 30
var num4 int = 50
//调用函数:
sum1 := cal(num3, num4)
fmt.Println(sum1)
//求和:
// var sum1 int = 0
// sum1 += num3
// sum1 += num4
// fmt.Println(sum1)
}
可变参数案例
package main
import "fmt"
// 定义一个函数,函数的参数为:可变参数 ... 参数的数量可变
// args...int 可以传入任意多个数量的int类型的数据 传入0个,1个,,,,n个
func test(args ...int) {
//函数内部处理可变参数的时候,将可变参数当做切片来处理
//遍历可变参数:
for i := 0; i < len(args); i++ {
fmt.Println(args[i])
}
}
func main() {
test()
fmt.Println("--------------------")
test(3)
fmt.Println("--------------------")
test(37, 58, 39, 59, 47)
}
值传递改变数据案例
package main
import "fmt"
// 参数的类型为指针
func test(num *int) {
// 对地址对应的变量进行改变数值
*num = 30
}
func main() {
var num int = 10
fmt.Println(&num)
test(&num) // 调用test函数的时候,传入的是num的地址
fmt.Println(num)
}
函数作为参数的案例
package main
import "fmt"
// 定义一个函数:
func test(num int) {
fmt.Println(num)
}
// 定义一个函数,把另一个函数作为形参:
func test02(num1 int, num2 float32, testFunc func(int)) {
fmt.Println("-----test02")
}
func main() {
//函数也是一种数据类型,可以赋值给一个变量
a := test //变量就是一个函数类型的变量
fmt.Printf("a的类型是:%T,test函数的类型是:%T \n", a, test) //a的类型是:func(int),test函数的类型是:func(int)
//通过该变量可以对函数调用
a(10) //等价于 test(10)
//调用test02函数:
test02(10, 3.19, test)
test02(10, 3.19, a)
}
init函数
- init函数:初始化函数,可以用来进行一些初始化的操作
- 每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用。
- 全局变量定义->init函数->main函数的执行流程
示例代码
package main
import "fmt"
// 全局变量
var num int = test()
func test() int {
fmt.Printf("test函数被执行")
fmt.Println() // 换行作用
return 10
}
func init() {
fmt.Println("init函数被执行")
}
func main() {
fmt.Println("Hello Go")
}
匿名函数
Go支持匿名函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数
匿名函数使用方式:
- 在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次(用的多)
- 将匿名函数赋给一个变量(该变量就是函数变量了),再通过该变量来调用匿名函数(用的少)
- 如何让一个匿名函数,可以在整个程序中有效呢?将匿名函数给一个全局变量就可以了
示例代码如下
package main
import "fmt"
var Func01 = func(num1 int, num2 int) int {
return num1 * num2
}
func main() {
//定义匿名函数:定义的同时调用
result := func(num1 int, num2 int) int {
return num1 + num2
}(10, 20)
fmt.Println(result)
//将匿名函数赋给一个变量,这个变量实际就是函数类型的变量
//sub等价于匿名函数
sub := func(num1 int, num2 int) int {
return num1 - num2
}
//直接调用sub就是调用这个匿名函数了
result01 := sub(30, 70)
fmt.Println(result01)
result02 := sub(30, 70)
fmt.Println(result02)
result03 := Func01(3, 4)
fmt.Println(result03)
}
闭包
- 闭包就是一个函数和与其相关的引用环境组合的一个整体
闭包的本质:
- 闭包本质依旧是一个匿名函数,只是这个函数引入外界的变量/参数
- 匿名函数+引用的变量/参数 = 闭包
特点:
- 返回的是一个匿名函数,但是这个匿名函数引用到函数外的变量/参数 ,因此这个匿名函数就和变量/参数形成一个整体,构成闭包。
- 闭包中使用的变量/参数会一直保存在内存中,所以会一直使用---》意味着闭包不可滥用(对内存消耗大) 示例代码
package main
import "fmt"
// 函数功能:求和
// 函数的名字:getSum 参数为空
// getSum函数返回值为一个函数,这个函数的参数是一个int类型的参数,返回值也是int类型
func getSum() func(int) int {
var sum int = 0
return func(num int) int {
sum = sum + num
return sum
}
}
// 闭包:返回的匿名函数+匿名函数以外的变量num
func main() {
f := getSum()
fmt.Println(f(1)) //1
fmt.Println(f(2)) //3
fmt.Println(f(3)) //6
fmt.Println(f(4)) //10
}
defer关键字
- 在函数中,程序员经常需要创建资源,为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer关键字
- 遇到defer关键字,会将后面的代码语句压入栈中,也会将相关的值同时拷贝入栈中,不会随着函数后面的变化而变化。
- 比如你想关闭某个使用的资源,在使用的时候直接随手defer,因为defer有延迟执行机制(函数执行完毕再执行defer压入栈的语句),所以你用完随手写了关闭,比较省心,省事
package main
import "fmt"
func main() {
fmt.Println(add(30, 60))
}
func add(num1 int, num2 int) int {
//在Golang中,程序遇到defer关键字,不会立即执行defer后的语句,而是将defer后的语句压入一个栈中,然后继续执行函数后面的语句defer
defer fmt.Println("num1= ", num1)
defer fmt.Println("num2= ", num2)
//栈的特点是:先进后出
//在函数执行完毕以后,从栈中取出语句开始执行,按照先进后出的规则执行语句
var sum int = num1 + num2
fmt.Println(sum)
return sum
}
字符串函数
【1】统计字符串的长度,按字节进行统计 len(str)使用内置函数也不用导包的,直接用就行 【2】字符串遍历: r:=[]rune("hello")
【3】字符串转整数: n, err := strconv.Atoi("66")
【4】整数转字符串: str = strconv.ltoa(6887)
【5】查找子串是否在指定的字符串中: strings.Contains("javaandgolang", "go")
【6】统计一个字符串有几个指定的子串: strings.Count("javaandgolang","a")
【7】不区分大小写的字符串比较: fmt.Println(strings.EqualFold("go" ,“Go"))
【8】返回子串在字符串第一次出现的索引值,如果没有回-1: strings.llndex("javaandgolang","a")
【9】字符串的替换:strings.Replace("goandjavagogo", "go", "golang", n)
n可以指定你希望替换几个,如果n=-1表示全部替换,替换两个n就是2 【10】按照指定的某个字符,为分割标识,将一个学符串拆分成字符串数组:strings.Split("go-python-java", "-")
【11】将字符串的字母进行大小写的转换:strings.ToLower("Go")
// go strings.ToUpper"go")
//Go 【12】将字符串左右两边的空格去掉:strings.TrimSpace(" go and java ")
【13】将字符串左右两边指定的字符去掉:strings.Trim("~golang~ ", " ~")
【14】将字符串左边指定的字符去掉:strings.TrimLeft("~golang~", "~")
【15】将字符串右边指定的字符去掉:strings.TrimRight("~golang~", "~")
【16】判断字符串是否以指定的字符串开头: strings.HasPrefix("http://java.sun.com/jsp/jstl/fmt", "http")
【17】判断字符串是否以指定的字符串结束: strings.HasSuffix("demo.png", ".png")
日期和时间的函数
【1】时间和日期的函数,需要到入time包,所以你获取当前时间,就要调用函数Now函数:
package main
import (
"fmt"
"time"
)
func main() {
//时间和日期的函数,需要到入time包,所以你获取当前时间,就要调用函数Now函数:
now := time.Now()
//Now()返回值是一个结构体,类型是:time.Time
fmt.Printf("%v ~~~ 对应的类型为:%T\n", now, now)
//2021-02-08 17:47:21.7600788 +0800 CST m=+0.005983901 ~~~ 对应的类型为:time.Time
//调用结构体中的方法:
fmt.Printf("年:%v \n", now.Year())
fmt.Printf("月:%v \n", now.Month()) //月:February
fmt.Printf("月:%v \n", int(now.Month())) //月:2
fmt.Printf("日:%v \n", now.Day())
fmt.Printf("时:%v \n", now.Hour())
fmt.Printf("分:%v \n", now.Minute())
fmt.Printf("秒:%v \n", now.Second())
}
【2】日期的格式化: (1)将日期以年月日时分秒按照格式输出为字符串:
//Printf将字符串直接输出:
fmt.Printf("当前年月日: %d-%d-%d 时分秒:%d:%d:%d \n",now.Year(),now.Month(),
now.Day(),now.Hour(),now.Minute(),now.Second())
//Sprintf可以得到这个字符串,以便后续使用:
datestr := fmt.Sprintf("当前年月日: %d-%d-%d 时分秒:%d:%d:%d \n",now.Year(),now.Month(),
now.Day(),now.Hour(),now.Minute(),now.Second())
fmt.Println(datestr)
(2)按照指定格式:
//这个参数字符串的各个数字必须是固定的,必须这样写
datestr2 := now.Format("2006/01/02 15/04/05")
fmt.Println(datestr2)
//选择任意的组合都是可以的,根据需求自己选择就可以(自己任意组合)。
datestr3 := now.Format("2006 15:04")
fmt.Println(datestr3)
错误处理
报错代码演示
package main
import "fmt"
func main() {
test()
fmt.Println("上述函数执行成功,正常执行下面逻辑")
}
func test() {
num1 := 10
num2 := 0
result := num1 / num2 // 0不能被除,会报错
fmt.Println(result)
}
defer+recover机制处理错误
错误处理/捕获机制: go中追求代码优雅,引入机制:defer+recover机制处理错误
**上面代码改造如下: **
package main
import "fmt"
func main() {
test()
fmt.Println("上述函数执行成功,正常执行下面逻辑")
}
func test() {
// 利用defer+recover来捕获错误
defer func() {
err := recover()
if err != nil {
fmt.Println("错误已经被捕获", err)
}
}()
// 通过上面匿名函数捕获,下面报错不会停止程序执行了,可以正常走逻辑
num1 := 10
num2 := 0
result := num1 / num2 // 0不能被除,会报错
fmt.Println(result)
}
自定义错误
自定义错误:需要调用errors包下的New函数:函数返回error类型
package main
import (
"errors"
"fmt"
)
func main() {
err := test()
if err != nil {
fmt.Println("程序发生了错误", err)
}
fmt.Println("上述函数执行成功,正常执行下面逻辑")
}
func test() (err error) {
num1 := 10
num2 := 0
if num2 == 0 {
// 抛出自定义错误
return errors.New("除数不能为0")
} else {
// 如果除数不为0,正常走逻辑
result := num1 / num2 // 0不能被除,会报错
fmt.Println(result)
return nil
}
}
有一种情况:程序出现错误以后,后续代码就没有必要执行,想让程序中断,退出程序: 借助:builtin包下内置函数:panic
上面代码加个方法
package main
import (
"errors"
"fmt"
)
func main() {
err := test()
if err != nil {
fmt.Println("程序发生了错误", err)
panic(err) // 程序停止往下执行
}
fmt.Println("上述函数执行成功,正常执行下面逻辑")
}
func test() (err error) {
num1 := 10
num2 := 0
if num2 == 0 {
// 抛出自定义错误
return errors.New("除数不能为0")
} else {
// 如果除数不为0,正常走逻辑
result := num1 / num2 // 0不能被除,会报错
fmt.Println(result)
return nil
}
}
数组
数组引入
案例
package main
import "fmt"
func main() {
//实现的功能:给出五个学生的成绩,求出成绩的总和,平均数:
//给出五个学生的成绩:--->数组存储:
//定义一个数组:
var scores [5]int
//将成绩存入数组:
scores[0] = 95
scores[1] = 91
scores[2] = 39
scores[3] = 60
scores[4] = 21
//求和:
//定义一个变量专门接收成绩的和:
sum := 0
for i := 0; i < len(scores); i++ { //i: 0,1,2,3,4
sum += scores[i]
}
//平均数:
avg := sum / len(scores)
//输出
fmt.Printf("成绩的总和为:%v,成绩的平均数为:%v", sum, avg)
}
数组的遍历方式
【1】普通for循环 【2】键值循环 (键值循环) for range结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道,for range 语法上类似于其它语言中的 foreach 语句,一般形式为:
for key, val := range coll {
...
}
注意: (1)coll就是你要的数组 (2)每次遍历得到的索引用key接收,每次遍历得到的索引位置上的值用val (3)key、value的名字随便起名 k、v key、value
(4)key、value属于在这个循环中的局部变量 (5)你想忽略某个值:用_就可以了:
package main
import "fmt"
func main() {
//实现的功能:给出五个学生的成绩,求出成绩的总和,平均数:
//给出五个学生的成绩:--->数组存储:
//定义一个数组:
var scores [5]int
//将成绩存入数组:(循环 + 终端输入)
for i := 0; i < len(scores); i++ { //i:数组的下标
fmt.Printf("请录入第个%d学生的成绩", i+1)
fmt.Scanln(&scores[i])
}
//展示一下班级的每个学生的成绩:(数组进行遍历)
//方式1:普通for循环:
for i := 0; i < len(scores); i++ {
fmt.Printf("第%d个学生的成绩为:%d\n", i+1, scores[i])
}
fmt.Println("-------------------------------")
//方式2:for-range循环
for key, value := range scores {
fmt.Printf("第%d个学生的成绩为:%d\n", key+1, value)
}
}
数组的初始化方式
package main
import "fmt"
func main() {
//第一种(指定长度):
var arr1 [3]int = [3]int{3, 6, 9}
fmt.Println(arr1)
//第二种(指定长度):
var arr2 = [3]int{1, 4, 7}
fmt.Println(arr2)
//第三种(不指定长度):
var arr3 = [...]int{4, 5, 6, 7}
fmt.Println(arr3)
//第四种(不指定长度):
var arr4 = [...]int{2: 66, 0: 33, 1: 99, 3: 88}
fmt.Println(arr4)
}
二维数组的定义和遍历
package main
import "fmt"
func main(){
//定义二维数组:
var arr [3][3]int = [3][3]int{{1,4,7},{2,5,8},{3,6,9}}
fmt.Println(arr)
fmt.Println("------------------------")
//方式1:普通for循环:
for i := 0;i < len(arr);i++{
for j := 0;j < len(arr[i]);j++ {
fmt.Print(arr[i][j],"\t")
}
fmt.Println()
}
fmt.Println("------------------------")
//方式2:for range循环:
for key,value := range arr {
for k,v := range value {
fmt.Printf("arr[%v][%v]=%v\t",key,k,v)
}
fmt.Println()
}
}
切片
切片的引入
【1】切片(slice)是golang中一种特有的数据类型 【2】数组有特定的用处,但是却有一些呆板(数组长度固定不可变),所以在 Go 语言的代码里并不是特别常见。相对的切片却是随处可见的,切片是一种建立在数组类型之上的抽象,它构建在数组之上并且提供更强大的能力和便捷。 【3】切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。 【4】切片的语法:var 切片名 []类型 = 数组的一个片段引用
示例代码
package main
import "fmt"
func main() {
//定义数组
var intarr [6]int = [6]int{1, 2, 3, 4, 5, 6}
// 切片构建在数组之上
// 定义一个切片名字为slice,[]动态变化的数组,长度不写,int类型,intarr是原数组
// [1:3]切片 - 切出的一段片段 - 索引:从1开始,到3结束(不包含3) - [1,3)
// var slice []int = intarr[1:3]
slice := intarr[1:3]
// 简写方式
// 1) var slice = arr[0:end] ----> var slice = arr[:end]
// 2) var slice = arr[start:len(arr)] ----> var slice = arr[start:]
// 3) var slice = arr[0:len(arr)] ----> var slice = arr[:]
// 输出数组
fmt.Println("intarr", intarr)
// 输出切片
fmt.Println("slice", slice)
//切片元素个数
fmt.Println("slice len:", len(slice))
// 获取切片容量,容量可以动态变化
fmt.Println("slice cap:", cap(slice))
}
切片的定义
【1】方式1:定义一个切片,然后让切片去引用一个已经创建好的数组。
package main
import "fmt"
func main() {
//定义数组
var intarr [6]int = [6]int{1, 2, 3, 4, 5, 6}
// 定义切片
slice := intarr[1:3]
fmt.Println("slice", slice)
}
【2】方式2:通过make内置函数来创建切片。基本语法: var切片名[type = make([], len,[cap])
package main
import "fmt"
func main() {
// 使用make内置函数创建切片
// make函数的三个参数:1.切片类型2.切片长度3.切片的容量
slice := make([]int, 4, 20)
fmt.Println(slice) // [0,0,0,0]
fmt.Println("切片的长度", len(slice))
fmt.Println("切片的容量", cap(slice))
slice[0] = 66
slice[1] = 88
fmt.Println(slice)
}
PS : make底层创建一个数组,对外不可见,所以不可以直接操作这个数组,要通过slice去间接的访问各个元素,不可以直接对数组进行维护/操作
【3】方式3:定一个切片,直接就指定具体数组,使用原理类似make的方式。
package main
import "fmt"
func main() {
// 直接定义切片
slice := []int{1, 4, 7}
fmt.Println(slice) // [1,4,7]
fmt.Println("切片的长度", len(slice))
fmt.Println("切片的容量", cap(slice))
}
切片的遍历
package main
import "fmt"
func main() {
//定义切片:
slice := make([]int, 4, 20)
slice[0] = 66
slice[1] = 88
slice[2] = 99
slice[3] = 100
//方式1:普通for循环
for i := 0; i < len(slice); i++ {
fmt.Printf("slice[%v] = %v \t", i, slice[i])
}
fmt.Println("\n------------------------------")
//方式2:for-range循环:
for i, v := range slice {
fmt.Printf("下标:%v ,元素:%v\n", i, v)
}
}
切片可以继续切片
切片的新增
package main
import "fmt"
func main(){
//定义数组:
var intarr [6]int = [6]int{1,4,7,3,6,9}
//定义切片:
var slice []int = intarr[1:4] //4,7,3
fmt.Println(len(slice))
slice2 := append(slice,88,50)
fmt.Println(slice2) //[4 7 3 88 50]
fmt.Println(slice)
//底层原理:
//1.底层追加元素的时候对数组进行扩容,老数组扩容为新数组:
//2.创建一个新数组,将老数组中的4,7,3复制到新数组中,在新数组中追加88,50
//3.slice2 底层数组的指向 指向的是新数组
//4.往往我们在使用追加的时候其实想要做的效果给slice追加:
slice = append(slice,88,50)
fmt.Println(slice)
//5.底层的新数组 不能直接维护,需要通过切片间接维护操作。
}
切片的拷贝
package main
import "fmt"
func main(){
//定义切片:
var a []int = []int{1,4,7,3,6,9}
//再定义一个切片:
var b []int = make([]int,10)
//拷贝:
copy(b,a) //将a中对应数组中元素内容复制到b中对应的数组中
fmt.Println(b)
}
map类型
初识map
【1】映射(map), Go语言中内置的一种类型,它将键值对相关联,我们可以通过键 key来获取对应的值 value。 类似其它语言的集合
【2】基本语法 var map变量名 map[keytype]valuetype
注意:
- key、value的类型:bool、数字、string、指针、channel 、还可以是只包含前面几个类型的接口、结构体、数组
- key通常为int 、string类型,value通常为数字(整数、浮点数)、string、map、结构体
- key:slice、map、function不可以
【3】map的特点:
- map集合在使用前一定要make
- map的key-value是无序的
- key是不可以重复的,如果遇到重复,后一个value会替换前一个value
- value可以重复的
package main
import "fmt"
func main() {
//定义map变量:
var a map[int]string
//只声明map内存是没有分配空间
//必须通过make函数进行初始化,才会分配空间:
a = make(map[int]string, 10) //map可以存放10个键值对
//将键值对存入map中:
a[20095452] = "张三"
a[20095387] = "李四"
a[20097291] = "王五"
a[20095387] = "朱六"
a[20096699] = "张三"
//输出集合
fmt.Println(a)
}
map的三种创建方式
package main
import "fmt"
func main() {
//方式1:
//定义map变量:
var a map[int]string
//只声明map内存是没有分配空间
//必须通过make函数进行初始化,才会分配空间:
a = make(map[int]string, 10) //map可以存放10个键值对
//将键值对存入map中:
a[20095452] = "张三"
a[20095387] = "李四"
//输出集合
fmt.Println(a)
//方式2:
b := make(map[int]string)
b[20095452] = "张三"
b[20095387] = "李四"
fmt.Println(b)
//方式3:
c := map[int]string{
20095452: "张三",
20098765: "李四",
}
c[20095387] = "王五"
fmt.Println(c)
}
map的操作
【1】增加和更新操作: map["key"]= value ——》 如果key还没有,就是增加,如果key存在就是修改。 【2】删除操作: delete(map,"key") , delete是一个内置函数,如果key存在,就删除该key-value,如果k的y不存在,不操作,但是也不会报错 【3】清空操作: (1)如果我们要删除map的所有key ,没有一个专门的方法一次删除,可以遍历一下key,逐个删除 (2)或者map = make(...),make一个新的,让原来的成为垃圾,被gc回收 【4】查找操作: value ,bool = map[key] value为返回的value,bool为是否返回 ,要么true 要么false
package main
import "fmt"
func main() {
//定义map
b := make(map[int]string)
//增加:
b[20095452] = "张三"
b[20095387] = "李四"
//修改:
b[20095452] = "王五"
//删除:
delete(b, 20095387)
delete(b, 20089546)
fmt.Println(b)
//查找:
value, flag := b[200]
fmt.Println(value)
fmt.Println(flag)
}
【5】获取长度:len函数 【6】遍历:for-range
package main
import "fmt"
func main() {
//定义map
b := make(map[int]string)
//增加:
b[20095452] = "张三"
b[20095387] = "李四"
b[20098833] = "王五"
//获取长度:
fmt.Println(len(b))
//遍历:
for k, v := range b {
fmt.Printf("key为:%v value为%v \t", k, v)
}
fmt.Println("---------------------------")
//加深难度:
a := make(map[string]map[int]string)
//赋值:
a["班级1"] = make(map[int]string, 3)
a["班级1"][20096677] = "露露"
a["班级1"][20098833] = "丽丽"
a["班级1"][20097722] = "菲菲"
a["班级2"] = make(map[int]string, 3)
a["班级2"][20089911] = "小明"
a["班级2"][20085533] = "小龙"
a["班级2"][20087244] = "小飞"
for k1, v1 := range a {
fmt.Println(k1)
for k2, v2 := range v1 {
fmt.Printf("学生学号为:%v 学生姓名为%v \t", k2, v2)
}
fmt.Println()
}
}
面向对象
引入
【1】Golang语言面向对象编程说明: (1)Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。 (2)Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解Gelang是基于struct来实现OOP特性的。 (3)Golang面向对象编程非常简洁,去掉了传统OOP语言的方法重载、构造函数和析构函数、隐藏的this指针等等 (4)Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承:Golang没有extends 关键字,继承是通过匿名字段来实现。
【2】结构体的引入:
具体的对象: 一位老师:珊珊老师: 姓名:赵珊珊 年龄:31岁 性别 :女 ......
可以使用变量来处理:
package main
import "fmt"
func main() {
//珊珊老师: 姓名:赵珊珊 年龄:31岁 性别 :女
var name string = "赵珊珊"
var age int = 31
var sex string = "女"
//马士兵老师:
var name2 string = "马士兵"
var age2 int = 45
var sex2 string = "男"
fmt.Println(name, age, sex, name2, age2, sex2)
}
结构体
第一种创建方式
package main
import "fmt"
//定义老师结构体,将老师中的各个属性 统一放入结构体中管理:
type Teacher struct{
//变量名字大写外界可以访问这个属性
Name string
Age int
School string
}
func main(){
//创建老师结构体的实例、对象、变量:
var t1 Teacher // var a int
fmt.Println(t1) //在未赋值时默认值:{ 0 }
t1.Name = "马士兵"
t1.Age = 45
t1.School = "清华大学"
fmt.Println(t1)
fmt.Println(t1.Age + 10)
}
第二种创建方式
package main
import "fmt"
// 定义老师结构体,将老师中的各个属性 统一放入结构体中管理:
type Teacher struct {
//变量名字大写外界可以访问这个属性
Name string
Age int
School string
}
func main() {
//创建老师结构体的实例、对象、变量:
var t1 Teacher = Teacher{"马士兵", 45, "清华大学"} // var a int
fmt.Println(t1)
}
第三种创建方式
package main
import "fmt"
// 定义老师结构体,将老师中的各个属性 统一放入结构体中管理:
type Teacher struct {
//变量名字大写外界可以访问这个属性
Name string
Age int
School string
}
func main() {
//创建老师结构体的实例、对象、变量:
var t *Teacher = new(Teacher)
// t是指针,t其实指向的就是地址,应该给这个地址的指向的对象的字段赋值:
(*t).Name = "马士兵"
(*t).Age = 45
// 为了符合程序员的编程习惯,go提供了简化的赋值方式:
t.School = "清华大学" // go编译器底层对t.Schoo1转化(*t).School="清华大学”
fmt.Println(*t)
}
第四种创建方式
package main
import "fmt"
// 定义老师结构体,将老师中的各个属性 统一放入结构体中管理:
type Teacher struct {
//变量名字大写外界可以访问这个属性
Name string
Age int
School string
}
func main() {
//创建老师结构体的实例、对象、变量:
var t *Teacher = &Teacher{"马士兵", 45, "清华大学"}
fmt.Println(*t)
}
结构体之间的转换
【1】结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
package main
import "fmt"
type Student struct {
Age int
}
type Person struct {
Age int
}
func main() {
var s Student = Student{10}
var p Person = Person{10}
s = Student(p)
fmt.Println(s)
fmt.Println(p)
}
【2】结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
package main
import "fmt"
type Student struct {
Age int
}
type Stu Student
func main() {
var s1 Student = Student{19}
var s2 Stu = Stu{19}
s1 = Student(s2)
fmt.Println(s1)
fmt.Println(s2)
}
方法
方法的引入
【1】方法是作用在指定的数据类型上、和指定的数据类型绑定,因此自定义类型,都可以有方法,而不仅仅是struct 【2】方法的声明和调用格式: 声明:
type A struct {
Num int
}
func (a A) test() {
fmt.Println(a.Num)
}
调用: var a A a.test()
(1)func (a A) test()相当于A结构体有一个方法叫test (2)(a A)体现方法test和结构体A绑定关系 (3)代码层面:
package main
import "fmt"
// 定义person结构体
type Person struct {
Name string
}
// 给Person结构体绑定方法test
func (p Person) test() {
fmt.Println(p.Name)
}
func main() {
// 创建结构体对象
var p Person
p.Name = "丽丽"
p.test()
}
注意: (1)test方法中参数名字随意起 (2)结构体Person和test方法绑定,调用test方法必须靠指定的类型:Person (3)如果其他类型变量调用test方法一定会报错。 (4)结构体对象传入test方法中,值传递,和函数参数传递一致。
package main
import "fmt"
// 定义person结构体
type Person struct {
Name string
}
// 给Person结构体绑定方法test
func (p Person) test() {
p.Name = "露露"
fmt.Println(p.Name)
}
func main() {
// 创建结构体对象
var p Person
p.Name = "丽丽"
p.test()
fmt.Println(p.Name)
}
方法的注意事项
(1)结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式 (2)如程序员希望在方法中,改变结构体变量的值,可以通过结构体指针的方式来处理
package main
import "fmt"
// 定义person结构体
type Person struct {
Name string
}
// 给Person结构体绑定方法test
func (p *Person) test() {
(*p).Name = "露露"
fmt.Println((*p).Name)
}
func main() {
// 创建结构体对象
var p Person
p.Name = "丽丽"
(&p).test()
fmt.Println(p.Name)
}
我们写程序的时候,可以直接简化:
package main
import "fmt"
// 定义person结构体
type Person struct {
Name string
}
// 给Person结构体绑定方法test
func (p *Person) test() {
p.Name = "露露"
fmt.Println(p.Name)
}
func main() {
// 创建结构体对象
var p Person
p.Name = "丽丽"
p.test()
fmt.Println(p.Name)
}
(3)Golang中的方法作用在指定的数据类型上的,和指定的数据类型绑定,因此自定义类型,都可以有方法,而不仅仅是struct,比如int , float32等都可以有方法
package main
import "fmt"
type integer int
func (i integer) print() {
i = 30
fmt.Println("i = ", i)
}
func (i *integer) change() {
*i = 30
fmt.Println("i = ", *i)
}
func main() {
var i integer = 20
i.print()
i.change()
fmt.Println(i)
}
(4)方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。 (5)如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出
以后定义结构体的话,常定义String()作为输出结构体信息的方法,在fmt.Println会自动调用
package main
import "fmt"
type Student struct {
Name string
Age int
}
func (s *Student) String() string {
str := fmt.Sprintf("Name = %v , Age = %v", s.Name, s.Age)
return str
}
func main() {
stu := Student{
Name: "丽丽",
Age: 20,
}
//传入地址,如果绑定了String方法就会自动调用
fmt.Println(&stu)
}
方法和函数的区别
【1】绑定指定类型: 方法:需要绑定指定数据类型 函数:不需要绑定数据类型
【2】调用方式不一样: 函数的调用方式: 函数名(实参列表) 方法的调用方式:变量.方法名(实参列表)
package main
import "fmt"
type Student struct {
Name string
}
// 定义方法:
func (s Student) test01() {
fmt.Println(s.Name)
}
// 定义函数:
func method01(s Student) {
fmt.Println(s.Name)
}
func main() {
//调用函数:
var s Student = Student{"丽丽"}
method01(s)
//方法调用:
s.test01()
}
【3】对于函数来说,参数类型对应是什么就要传入什么。
package main
import "fmt"
type Student struct{
Name string
}
//定义函数:
func method01(s Student){
fmt.Println(s.Name)
}
func method02(s *Student){
fmt.Println((*s).Name)
}
func main(){
var s Student = Student{"丽丽"}
method01(s)
//method01(&s)错误
method02(&s)
//method02(s)错误
}
【4】对于方法来说,接收者为值类型,可以传入指针类型,接受者为指针类型,可以传入值类型。
package main
import "fmt"
type Student struct{
Name string
}
//定义方法:
func (s Student) test01(){
fmt.Println(s.Name)
}
func (s *Student) test02(){
fmt.Println((*s).Name)
}
func main(){
var s Student = Student{"丽丽"}
s.test01()
(&s).test01()//虽然用指针类型调用,但是传递还是按照值传递的形式
(&s).test02()
s.test02()//等价
}
创建结构体实例时指定字段值
package main
import "fmt"
type Student struct {
Name string
Age int
}
func main() {
// 方式一:按照顺序赋值操作
var s1 Student = Student{"张三", 18}
fmt.Println(s1)
// 方式二:按照指定类型
var s2 Student = Student{Name: "李四", Age: 19}
fmt.Println(s2)
// 方式三:想要返回结构体的指针类型
var s3 *Student = &Student{Name: "王五", Age: 20}
fmt.Println(*s3)
var s4 *Student = &Student{"赵六", 21}
fmt.Println(*s4)
}
封装
【1】什么是封装: 封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作方法,才能对字段进行操作。
【2】封装的好处:
- 隐藏实现细节
- 提可以对数据进行验证,保证安全合理
【3】Golang中如何实现封装:
- 建议将结构体、字段(属性)的首字母小写(其它包不能使用,类似private,实际开发不小写也可能,因为封装没有那么严格)
- 给结构体所在包提供一个工厂模式的函数,首字母大写(类似一个构造函数)
- 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
func (var 结构体类型名)SetXxx(参数列表){
//加入数据验证的业务逻辑
var.Age =参数
}
- 提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值
func (var结构体类型名) GetXxx() (返回值列表){
return var.字段;
}
在目录下新建model/model.go
文件,内容如下
package model
// 名字为小写,其他包不能访问
type person struct {
Name string
Age int
}
// 定义工厂模式的函数,相当于有参构造器
func NewPerson(name string, age int) *person {
return &person{
Name: name,
Age: age,
}
}
// 定义set和get方法,对age字段进行封装,因为在方法中可以加一系列的限制操作,确保被封装字段的安全合理性
func (p *person) SetAge(age int) {
if age < 0 || age > 200 {
panic("年龄不合法")
}
p.Age = age
}
func (p *person) GetAge() int {
return p.Age
}
在目录下main.go
使用如下:
package main
import (
"demo01/model"
"fmt"
)
func main() {
// 创建person结构体的实例
p := model.NewPerson("张三", 18)
// 设置年龄
p.SetAge(20)
// 获取年龄
age := p.GetAge()
fmt.Println(age)
}
继承
继承的引入
【1】继承的引入: 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法,其它的结构体不需要重新定义这些属性和方法,只需嵌套一个匿名结构体即可。也就是说:在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。 【2】继承的优点: 提高代码的复用性、扩展性
代码实现
package main
import (
"fmt"
)
// 定义动物结构体:
type Animal struct {
Age int
Weight float32
}
// 给Animal绑定方法:喊叫:
func (an *Animal) Shout() {
fmt.Println("我可以大声喊叫")
}
// //给Animal绑定方法:自我展示:
func (an *Animal) ShowInfo() {
fmt.Printf("动物的年龄是:%v,动物的体重是:%v", an.Age, an.Weight)
}
// 定义结构体:Cat
type Cat struct {
//为了复用性,体现继承思维,嵌入匿名结构体:——》将Animal中的字段和方法都达到复用
Animal
}
// 对Cat绑定特有的方法:
func (c *Cat) scratch() {
fmt.Println("我是小猫,我可以挠人")
}
func main() {
//创建Cat结构体示例:
cat := &Cat{}
cat.Animal.Age = 3
cat.Animal.Weight = 10.6
cat.Animal.Shout()
cat.Animal.ShowInfo()
cat.scratch()
}
继承的注意事项
【1】结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。
package main
import (
"fmt"
)
// 定义动物结构体:
type Animal struct {
Age int
weight float32
}
// 给Animal绑定方法:喊叫:
func (an *Animal) Shout() {
fmt.Println("我可以大声喊叫")
}
// //给Animal绑定方法:自我展示:
func (an *Animal) showInfo() {
fmt.Printf("动物的年龄是:%v,动物的体重是:%v", an.Age, an.weight)
}
// 定义结构体:Cat
type Cat struct {
//为了复用性,体现继承思维,嵌入匿名结构体:——》将Animal中的字段和方法都达到复用
Animal
}
// 对Cat绑定特有的方法:
func (c *Cat) scratch() {
fmt.Println("我是小猫,我可以挠人")
}
func main() {
//创建Cat结构体示例:
cat := &Cat{}
cat.Animal.Age = 3
cat.Animal.weight = 10.6
cat.Animal.Shout()
cat.Animal.showInfo()
cat.scratch()
}
【2】匿名结构体字段访问可以简化。
func main() {
//创建Cat结构体示例:
cat := &Cat{}
cat.Animal.Age = 3
cat.Animal.weight = 10.6
cat.Animal.Shout()
cat.Animal.showInfo()
cat.scratch()
}
等同如下
func main() {
//创建Cat结构体示例:
cat := &Cat{}
cat.Age = 3
cat.weight = 10.6
cat.Shout()
cat.showInfo()
cat.scratch()
}
cat.Age --->cat对应的结构体中找是否有Age字段,如果有直接使用,如果没有就去找嵌入的结构体类型中的Age
【3】当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分。
package main
import (
"fmt"
)
// 定义动物结构体:
type Animal struct {
Age int
weight float32
}
// 给Animal绑定方法:喊叫:
func (an *Animal) Shout() {
fmt.Println("我可以大声喊叫")
}
// //给Animal绑定方法:自我展示:
func (an *Animal) showInfo() {
fmt.Printf("动物的年龄是:%v,动物的体重是:%v", an.Age, an.weight)
}
// 定义结构体:Cat
type Cat struct {
//为了复用性,体现继承思维,嵌入匿名结构体:——》将Animal中的字段和方法都达到复用
Animal
Age int
}
func (c *Cat) showInfo() {
fmt.Printf("~~~~~~~~动物的年龄是:%v,动物的体重是:%v", c.Age, c.weight)
}
// 对Cat绑定特有的方法:
func (c *Cat) scratch() {
fmt.Println("我是小猫,我可以挠人")
}
func main() {
//创建Cat结构体示例:
// cat := &Cat{}
// cat.Age = 3
// cat.weight = 10.6
// cat.Shout()
// cat.showInfo()
// cat.scratch()
cat := &Cat{}
cat.weight = 9.4
cat.Age = 10 //就近原则
cat.Animal.Age = 20
cat.showInfo() //就近原则
cat.Animal.showInfo()
}
【4】Golang中支持多继承:如一个结构体嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。为了保证代码的简洁性,建议大家尽量不使用多重继承,很多语言就将多重继承去除了,但是Go中保留了。
package main
import (
"fmt"
)
type A struct {
a int
b string
}
type B struct {
c int
d string
}
type C struct {
A
B
}
func main() {
//构建C结构体实例:
c := C{A{10, "aaa"}, B{20, "ccc"}}
fmt.Println(c)
}
【5】如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
package main
import (
"fmt"
)
type A struct {
a int
b string
}
type B struct {
c int
d string
a int
}
type C struct {
A
B
}
func main() {
//构建C结构体实例:
c := C{A{10, "aaa"}, B{20, "ccc", 50}}
fmt.Println(c.b)
fmt.Println(c.d)
fmt.Println(c.A.a)
fmt.Println(c.B.a)
}
【6】结构体的匿名字段可以是基本数据类型。
package main
import (
"fmt"
)
type A struct {
a int
b string
}
type B struct {
c int
d string
a int
}
type C struct {
A
B
int // 匿名字段基本数据类型
}
func main() {
//构建C结构体实例:
c := C{A{10, "aaa"}, B{20, "ccc", 50},888}
fmt.Println(c.b)
fmt.Println(c.d)
fmt.Println(c.A.a)
fmt.Println(c.B.a)
fmt.Println(c.int) // 访问匿名字段
}
【7】嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。
package main
import (
"fmt"
)
type A struct {
a int
b string
}
type B struct {
c int
d string
a int
}
type C struct {
A
B
int // 匿名字段基本数据类型
}
func main() {
//构建C结构体实例:
//c := C{A{10, "aaa"}, B{20, "ccc", 50}, 888}
// 直接指定匿名字段的值
c := C{
A{
a: 10,
b: "aaa",
},
B{
c: 20,
d: "ccc",
a: 50,
},
888,
}
fmt.Println(c.b)
fmt.Println(c.d)
fmt.Println(c.A.a)
fmt.Println(c.B.a)
fmt.Println(c.int) // 访问匿名字段
}
【8】嵌入匿名结构体的指针也是可以的。
package main
import "fmt"
type A struct {
a int
b string
}
type B struct {
c int
d string
a int
}
type C struct {
*A
*B
int
}
func main() {
c := C{&A{10, "hello"}, &B{20, "world", 50}, 888}
fmt.Println(c)
fmt.Println(*c.A)
fmt.Println(*c.B)
}
【9】结构体的字段可以是结构体类型的。(组合模式)
package main
import "fmt"
type A struct {
c int
d string
a int
}
type B struct {
a int
b string
c A // 组合模式
}
func main() {
b := B{1, "hello", A{2, "world", 99}}
fmt.Println(b)
fmt.Println(b.c.d)
}
接口
接口的引入
package main
import "fmt"
// 接口的定义:定义规则、定义规范,定义某种能力:
type SayHello interface {
//声明没有实现的方法:
sayHello()
}
// 接口的实现:定义一个结构体:
// 中国人:
type Chinese struct {
}
// 实现接口的方法---》具体的实现:
func (person Chinese) sayHello() {
fmt.Println("你好")
}
// 接口的实现:定义一个结构体:
// 美国人:
type American struct {
}
// 实现接口的方法---》具体的实现:
func (person American) sayHello() {
fmt.Println("hi")
}
// 定义一个函数:专门用来各国人打招呼的函数,接收具备SayHello接口的能力的变量:
func greet(s SayHello) {
s.sayHello()
}
func main() {
//创建一个中国人:
c := Chinese{}
//创建一个美国人:
a := American{}
//美国人打招呼:
greet(a)
//中国人打招呼:
greet(c)
}
总结:
(1)接口中可以定义一组方法,但不需要实现,不需要方法体。并且接口中不能包含任何变量。到某个自定义类型要使用的时候(实现接口的时候),再根据具体情况把这些方法具体实现出来。
(2)实现接口要实现所有的方法才是实现。
(3)Golang中的接口不需要显式的实现接口。Golang中没有implement关键字。(Golang中实现接口是基于方法的,不是基于接口的)
例如:
A接口 a,b方法
B接口 a,b方法
C结构体 实现了 a,b方法 ,那么C实现了A接口,也可以说实现了B接口 (只要实现全部方法即可,和实际接口耦合性很低,比Java松散得多)
(4)接口目的是为了定义规范,具体由别人来实现即可。
接口的注意事项
【1】接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量。
【2】只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
【3】一个自定义类型可以实现多个接口
package main
import "fmt"
type AInterface interface {
a()
}
type BInterface interface {
b()
}
type Stu struct {
}
func (s Stu) a() {
fmt.Println("aaaa")
}
func (s Stu) b() {
fmt.Println("bbbb")
}
func main() {
var s Stu
var a AInterface = s
var b BInterface = s
a.a()
b.b()
}
【4】一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现。
package main
import "fmt"
type CInterface interface {
c()
}
type BInterface interface {
b()
}
type AInterface interface {
BInterface
CInterface
a()
}
type Stu struct {
}
func (s Stu) a() {
fmt.Println("a")
}
func (s Stu) b() {
fmt.Println("b")
}
func (s Stu) c() {
fmt.Println("c")
}
func main() {
var s Stu
var a AInterface = s
a.a()
a.b()
a.c()
}
【5】interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
【6】空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋给空接口。
文件
简单案例
package main
import(
"fmt"
"os"
)
func main(){
//打开文件:
file,err := os.Open("d:/Test.txt");
if err != nil {//出错
fmt.Println("文件打开出错,对应错误为:",err)
}
//没有出错,输出文件:
fmt.Printf("文件=%v",file)
//.........一系列操作
//关闭文件:
err2 := file.Close();
if err2 != nil {
fmt.Println("关闭失败")
}
}
一次性读取文件
package main
import(
"fmt"
"io/ioutil"
)
func main(){
//备注:在下面的程序中不需要进行 Open\Close操作,因为文件的打开和关闭操作被封装在ReadFile函数内部了
//读取文件:
content,err := ioutil.ReadFile("d:/Test.txt")//返回内容为:[]byte,err
if err != nil {//读取有误
fmt.Println("读取出错,错误为:",err)
}
//如果读取成功,将内容显示在终端即可:
//fmt.Printf("%v",content)
fmt.Printf("%v",string(content))
}
读取文件(带缓冲区)
package main
import(
"fmt"
"os"
"bufio"
"io"
)
func main(){
//打开文件:
file,err := os.Open("d:/Test.txt")
if err != nil {//打开失败
fmt.Println("文件打开失败,err=",err)
}
//当函数退出时,让file关闭,防止内存泄露:
defer file.Close()
//创建一个流:
reader := bufio.NewReader(file)
//读取操作:
for {
str,err := reader.ReadString('\n')//读取到一个换行就结束
if err == io.EOF {//io.EOF 表示已经读取到文件的结尾
break
}
//如果没有读取到文件结尾的话,就正常输出文件内容即可:
fmt.Println(str)
}
//结束:
fmt.Println("文件读取成功,并且全部读取完毕")
}
写入文件
package main
import(
"fmt"
"os"
"bufio"
)
func main(){
//写入文件操作:
//打开文件:
file , err := os.OpenFile("d:/Demo.txt",os.O_RDWR | os.O_APPEND | os.O_CREATE,0666)
if err != nil {//文件打开失败
fmt.Printf("打开文件失败",err)
return
}
//及时将文件关闭:
defer file.Close()
//写入文件操作:---》IO流---》缓冲输出流(带缓冲区)
writer := bufio.NewWriter(file)
for i := 0; i < 10;i++ {
writer.WriteString("你好 马士兵\n")
}
//流带缓冲区,刷新数据--->真正写入文件中:
writer.Flush()
s :=os.FileMode(0666).String()
fmt.Println(s)
}
文件复制操作
package main
import(
"fmt"
"io/ioutil"
)
func main(){
//定义源文件:
file1Path := "d:/Demo.txt"
//定义目标文件:
file2Path := "d:/Demo2.txt"
//对文件进行读取:
content,err := ioutil.ReadFile(file1Path)
if err != nil {
fmt.Println("读取有问题!")
return
}
//写出文件:
err = ioutil.WriteFile(file2Path,content,0666)
if err != nil {
fmt.Println("写出失败!")
}
}
携程和管道
携程入门
请编写一个程序,完成如下功能: (1)在主线程中,开启一个goroutine,该goroutine每隔1秒输出"hello golang" (2)在主线程中也每隔一秒输出"hello msb",输出10次后,退出程序 (3)要求主线程和goroutine同时执行
package main
import(
"fmt"
"strconv"
"time"
)
func test(){
for i := 1;i <= 10;i++ {
fmt.Println("hello golang + " + strconv.Itoa(i))
//阻塞一秒:
time.Sleep(time.Second)
}
}
func main(){//主线程
go test() //开启一个协程
for i := 1;i <= 10;i++ {
fmt.Println("hello msb + " + strconv.Itoa(i))
//阻塞一秒:
time.Sleep(time.Second)
}
}
主死从随
- 如果主线程退出了,则协程即使还没有执行完毕,也会退出
- 当然协程也可以在主线程没有退出前,就自己结束了,比如完成了自己的任务
package main
import(
"fmt"
"strconv"
"time"
)
func test(){
for i := 1;i <= 1000;i++ {
fmt.Println("hello golang + " + strconv.Itoa(i))
//阻塞一秒:
time.Sleep(time.Second)
}
}
func main(){//主线程
go test() //开启一个协程
for i := 1;i <= 10;i++ {
fmt.Println("hello msb + " + strconv.Itoa(i))
//阻塞一秒:
time.Sleep(time.Second)
}
}
启动多个协程
package main
import(
"fmt"
"time"
)
func main(){
//匿名函数+外部变量 = 闭包
for i := 1;i <= 5;i++ {
//启动一个协程
//使用匿名函数,直接调用匿名函数
go func(n int){
fmt.Println(n)
}(i)
}
time.Sleep(time.Second * 2)
}
使用WaitGroup控制协程退出
【1】WaitGroup的作用: WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。---》解决主线程在子协程结束后自动结束
package main
import(
"fmt"
"sync"
)
var wg sync.WaitGroup //只定义无需赋值
func main(){
//启动五个协程
for i := 1 ;i <= 5;i++ {
wg.Add(1) //协程开始的时候加1操作
go func(n int){
fmt.Println(n)
wg.Done() //协程执行完成减1
}(i)
}
//主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
}
如果防止忘记计数器减1操作,结合defer关键字使用:
package main
import(
"fmt"
"sync"
)
var wg sync.WaitGroup //只定义无需赋值
func main(){
//启动五个协程
for i := 1 ;i <= 5;i++ {
wg.Add(1) //协程开始的时候加1操作
go func(n int){
defer wg.Done()
fmt.Println(n)
}(i)
}
//主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
}
可以最开始在知道协程次数的情况下先Add操作:
package main
import(
"fmt"
"sync"
)
var wg sync.WaitGroup //只定义无需赋值
func main(){
wg.Add(5)
//启动五个协程
for i := 1 ;i <= 5;i++ {
go func(n int){
defer wg.Done()
fmt.Println(n)
}(i)
}
//主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
}
注意:Add中加入的数字和协程的次数一定要保持一致
多个协程操纵同一数据案例(互斥锁)
多个协程操纵同一数据
package main
import(
"fmt"
"sync"
)
//定义一个变量:
var totalNum int
var wg sync.WaitGroup //只定义无需赋值
func add(){
defer wg.Done()
for i := 0 ;i < 100000;i++{
totalNum = totalNum + 1
}
}
func sub(){
defer wg.Done()
for i := 0 ;i < 100000;i++{
totalNum = totalNum - 1
}
}
func main(){
wg.Add(2)
//启动协程
go add()
go sub()
wg.Wait()
fmt.Println(totalNum)
}
结果:在理论上,这个totalNum结果应该是0 ,无论协程怎么交替执行,最终想象的结果就是0 但是事实上:不是
解决问题: 有一个机制:确保:一个协程在执行逻辑的时候另外的协程不执行 ----》锁的机制---》加入互斥锁
package main
import(
"fmt"
"sync"
)
//定义一个变量:
var totalNum int
var wg sync.WaitGroup //只定义无需赋值
//加入互斥锁:
var lock sync.Mutex
func add(){
defer wg.Done()
for i := 0 ;i < 100000;i++{
//加锁
lock.Lock()
totalNum = totalNum + 1
//解锁:
lock.Unlock()
}
}
func sub(){
defer wg.Done()
for i := 0 ;i < 100000;i++{
//加锁
lock.Lock()
totalNum = totalNum - 1
//解锁:
lock.Unlock()
}
}
func main(){
wg.Add(2)
//启动协程
go add()
go sub()
wg.Wait()
fmt.Println(totalNum)
}
读写锁的使用
golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁) 【1】互斥锁 其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别 ----性能、效率相对来说比较低
【2】读写锁 RWMutex是一个读写锁,其经常用于读次数远远多于写次数的场景. ---在读的时候,数据之间不产生影响, 写和读之间才会产生影响
package main
import(
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup //只定义无需赋值
//加入读写锁:
var lock sync.RWMutex
func read(){
defer wg.Done()
lock.RLock()//如果只是读数据,那么这个锁不产生影响,但是读写同时发生的时候,就会有影响
fmt.Println("开始读取数据")
time.Sleep(time.Second)
fmt.Println("读取数据成功")
lock.RUnlock()
}
func write(){
defer wg.Done()
lock.Lock()
fmt.Println("开始修改数据")
time.Sleep(time.Second * 10)
fmt.Println("修改数据成功")
lock.Unlock()
}
func main(){
wg.Add(6)
//启动协程 ---> 场合:读多写少
for i := 0;i < 5;i++ {
go read()
}
go write()
wg.Wait()
}
管道介绍
【1】管道(channel)特质介绍: (1)管道本质就是一个数据结构-队列 (2)数据是先进先出 (3)自身线程安全,多协程访问时,不需要加锁,channel本身就是线程安全的 (4)管道有类型的,一个string的管道只能存放string类型数据
管道入门案例
【1】管道的定义: var 变量名 chan 数据类型 PS1:chan管道关键字 PS2:数据类型指的是管道的类型,里面放入数据的类型,管道是有类型的,int类型的管道只能写入整数int PS3:管道是引用类型,必须初始化才能写入数据,即make后才能使用
package main
import(
"fmt"
)
func main(){
//定义管道 、 声明管道 ---> 定义一个int类型的管道
var intChan chan int
//通过make初始化:管道可以存放3个int类型的数据
intChan = make(chan int,3)
//证明管道是引用类型:
fmt.Printf("intChan的值:%v",intChan) // 0xc000112080
//向管道存放数据:
intChan<- 10
num := 20
intChan<- num
intChan<- 40
//注意:不能存放大于容量的数据:
//intChan<- 80
//在管道中读取数据:
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
fmt.Println(num1)
fmt.Println(num2)
fmt.Println(num3)
//注意:在没有使用协程的情况下,如果管道的数据已经全部取出,那么再取就会报错:
num4 := <-intChan
fmt.Println(num4)
//输出管道的长度:
fmt.Printf("管道的实际长度:%v,管道的容量是:%v",len(intChan),cap(intChan))
}
管道的关闭
【1】管道的关闭: 使用内置函数close可以关闭管道,当管道关闭后,就不能再向管道写数据了,但是仍然可以从该管道读取数据。
package main
import(
"fmt"
)
func main(){
//定义管道 、 声明管道
var intChan chan int
//通过make初始化:管道可以存放3个int类型的数据
intChan = make(chan int,3)
//在管道中存放数据:
intChan<- 10
intChan<- 20
//关闭管道:
close(intChan)
//再次写入数据:--->报错
//intChan<- 30
//当管道关闭后,读取数据是可以的:
num := <- intChan
fmt.Println(num)
}
管道的遍历
【1】管道的遍历: 管道支持for-range的方式进行遍历,请注意两个细节 1)在遍历时,如果管道没有关闭,则会出现deadlock的错误 2)在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
package main
import(
"fmt"
)
func main(){
//定义管道 、 声明管道
var intChan chan int
//通过make初始化:管道可以存放3个int类型的数据
intChan = make(chan int,100)
for i := 0;i < 100;i++ {
intChan<- i
}
//在遍历前,如果没有关闭管道,就会出现deadlock的错误
//所以我们在遍历前要进行管道的关闭
close(intChan)
//遍历:for-range
for v := range intChan {
fmt.Println("value = ",v)
}
}
协程和管道协同工作案例
【1】案例需求: 请完成协程和管道协同工作的案例,具体要求:
- 开启一个writeData协程,向管道中写入50个整数.
- 开启一个readData协程,从管道中读取writeData写入的数据。
- 注意: writeData和readDate操作的是同一个管道
- 主线程需要等待writeData和readDate协程都完成工作才能退出
package main
import(
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup //只定义无需赋值
//写:
func writeData(intChan chan int){
defer wg.Done()
for i := 1;i <= 50;i++{
intChan<- i
fmt.Println("写入的数据为:",i)
time.Sleep(time.Second)
}
//管道关闭:
close(intChan)
}
//读:
func readData(intChan chan int){
defer wg.Done()
//遍历:
for v := range intChan{
fmt.Println("读取的数据为:",v)
time.Sleep(time.Second)
}
}
func main(){//主线程
//写协程和读协程共同操作同一个管道-》定义管道:
intChan := make(chan int,50)
wg.Add(2)
//开启读和写的协程:
go writeData(intChan)
go readData(intChan)
//主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
}
声明只读只写管道
【1】管道可以声明为只读或者只写性质
package main
import(
"fmt"
)
func main(){
//默认情况下,管道是双向的--》可读可写:
//var intChan1 chan int
//声明为只写:
var intChan2 chan<- int // 管道具备<- 只写性质
intChan2 = make(chan int,3)
intChan2<- 20
//num := <-intChan2 报错
fmt.Println("intChan2:",intChan2)
//声明为只读:
var intChan3 <-chan int// 管道具备<- 只读性质
if intChan3 != nil {
num1 := <-intChan3
fmt.Println("num1:",num1)
}
//intChan3<- 30 报错
}
管道的阻塞
【1】当管道只写入数据,没有读取,就会出现阻塞:
package main
import(
"fmt"
_"time"
"sync"
)
var wg sync.WaitGroup //只定义无需赋值
//写:
func writeData(intChan chan int){
defer wg.Done()
for i := 1;i <= 10;i++{
intChan<- i
fmt.Println("写入的数据为:",i)
//time.Sleep(time.Second)
}
//管道关闭:
close(intChan)
}
//读:
func readData(intChan chan int){
defer wg.Done()
//遍历:
for v := range intChan{
fmt.Println("读取的数据为:",v)
//time.Sleep(time.Second)
}
}
func main(){//主线程
//写协程和读协程共同操作同一个管道-》定义管道:
intChan := make(chan int,10)
wg.Add(2)
//开启读和写的协程:
go writeData(intChan)
//go readData(intChan)
//主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
}
【2】写的快,读的满(管道读写频率不一致),不会出现阻塞问题:
package main
import(
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup //只定义无需赋值
//写:
func writeData(intChan chan int){
defer wg.Done()
for i := 1;i <= 10;i++{
intChan<- i
fmt.Println("写入的数据为:",i)
//time.Sleep(time.Second)
}
//管道关闭:
close(intChan)
}
//读:
func readData(intChan chan int){
defer wg.Done()
//遍历:
for v := range intChan{
fmt.Println("读取的数据为:",v)
time.Sleep(time.Second)
}
}
func main(){//主线程
//写协程和读协程共同操作同一个管道-》定义管道:
intChan := make(chan int,10)
wg.Add(2)
//开启读和写的协程:
go writeData(intChan)
go readData(intChan)
//主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
}
select功能
【1】select功能:解决多个管道的选择问题,也可以叫做多路复用,可以从多个管道中随机公平地选择一个来执行 PS:case后面必须进行的是io操作,不能是等值,随机去选择一个io操作 PS:default防止select被阻塞住,加入default
package main
import(
"fmt"
"time"
)
func main(){
//定义一个int管道:
intChan := make(chan int,1)
go func(){
time.Sleep(time.Second * 15)
intChan<- 10
}()
//定义一个string管道:
stringChan := make(chan string,1)
go func(){
time.Sleep(time.Second * 12)
stringChan<- "msbgolang"
}()
//fmt.Println(<-intChan)//本身取数据就是阻塞的
select{
case v := <-intChan :
fmt.Println("intChan:",v)
case v := <-stringChan :
fmt.Println("stringChan:",v)
default:
fmt.Println("防止select被阻塞")
}
}
defer+recover机制处理错误
【1】问题原因:多个协程工作,其中一个协程出现panic,导致程序崩溃 【2】解决办法:利用refer+recover捕获panic进行处理,即使协程出现问题,主线程仍然不受影响可以继续执行。
package main
import(
"fmt"
"time"
)
//输出数字:
func printNum(){
for i := 1;i <= 10;i++{
fmt.Println(i)
}
}
//做除法操作:
func devide(){
defer func(){
err := recover()
if err != nil{
fmt.Println("devide()出现错误:",err)
}
}()
num1 := 10
num2 := 0
result := num1 / num2
fmt.Println(result)
}
func main(){
//启动两个协程:
go printNum()
go devide()
time.Sleep(time.Second * 5)
}
网络编程
基于TCP协议的网络通信
创建客户端
package main
import(
"fmt"
"net" //所需的网络编程全部都在net包下
)
func main(){
//打印:
fmt.Println("客服端启动。。")
//调用Dial函数:参数需要指定tcp协议,需要指定服务器端的IP+PORT
conn,err := net.Dial("tcp","127.0.0.1:8888")
if err != nil {//连接失败
fmt.Println("客户端连接失败:err:",err)
return
}
fmt.Println("连接成功,conn:",conn)
}
创建服务器端
进行监听:(Listen函数在net包下)
package main
import(
"fmt"
"net" //所需的网络编程全部都在net包下
)
func main(){
//打印:
fmt.Println("服务器端启动了。。")
//进行监听:需要指定服务器端TCP协议,服务器端的IP+PORT
listen,err := net.Listen("tcp","127.0.0.1:8888")
if err != nil{//监听失败
fmt.Println("监听失败,err:",err)
return
}
//监听成功以后:
//循环等待客户端的链接:
for{
conn,err2 := listen.Accept()
if err2 != nil {//客户端的等待失败
fmt.Println("客户端的等待失败,err2:",err2)
}else{
//连接成功:
fmt.Printf("等待链接成功,con=%v ,接收到的客户端信息:%v \n",conn,conn.RemoteAddr().String())
}
}
}
处理终端数据
【1】客户端发送数据:
package main
import(
"fmt"
"net" //所需的网络编程全部都在net包下
"bufio"
"os"
)
func main(){
//打印:
fmt.Println("客服端启动。。")
//调用Dial函数:参数需要指定tcp协议,需要指定服务器端的IP+PORT
conn,err := net.Dial("tcp","127.0.0.1:8888")
if err != nil {//连接失败
fmt.Println("客户端连接失败:err:",err)
return
}
fmt.Println("连接成功,conn:",conn)
//通过客户端发送单行数据,然后退出:
reader := bufio.NewReader(os.Stdin)//os.Stdin代表终端标准输入
//从终端读取一行用户输入的信息:
str,err := reader.ReadString('\n')
if err != nil {
fmt.Println("终端输入失败,err:",err)
}
//将str数据发送给服务器:
n,err := conn.Write([]byte(str))
if err != nil{
fmt.Println("连接失败,err:",err)
}
fmt.Printf("终端数据通过客户端发送成功,一共发送了%d字节的数据,并退出",n)
}
【2】服务器端接收数据:
package main
import(
"fmt"
"net" //所需的网络编程全部都在net包下
)
func process(conn net.Conn){
//连接用完一定要关闭:
defer conn.Close()
for{
//创建一个切片,准备:将读取的数据放入切片:
buf := make([]byte,1024)
//从conn连接中读取数据:
n,err := conn.Read(buf)
if err != nil{
return
}
//将读取内容在服务器端输出:
fmt.Println(string(buf[0:n]))
}
}
func main(){
//打印:
fmt.Println("服务器端启动了。。")
//进行监听:需要指定服务器端TCP协议,服务器端的IP+PORT
listen,err := net.Listen("tcp","127.0.0.1:8888")
if err != nil{//监听失败
fmt.Println("监听失败,err:",err)
return
}
//监听成功以后:
//循环等待客户端的链接:
for{
conn,err2 := listen.Accept()
if err2 != nil {//客户端的等待失败
fmt.Println("客户端的等待失败,err2:",err2)
}else{
//连接成功:
fmt.Printf("等待链接成功,con=%v ,接收到的客户端信息:%v \n",conn,conn.RemoteAddr().String())
}
//准备一个协程,协程处理客户端服务请求:
go process(conn)//不同的客户端的请求,连接conn不一样的
}
}
反射
反射的引入
【1】反射可以做什么?
- 反射可以在运行时动态获取变量的各种信息,比如变量的类型,类别等信息
- 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
- 通过反射,可以修改变量的值,可以调用关联的方法。
- 使用反射,需要import ("reflect")
【2】反射相关的函数
- reflect.TypeOf(变量名),获取变量的类型,返回reflect.Type类型
- reflect.ValueOf(变量名),获取变量的值,返回reflect.Value类型(reflect.Value是一个结构体类型),通过reflect.Value,可以获取到关于该变量的很多信息。
对基本数据类型反射
【1】反射相关的函数
- reflect.TypeOf(变量名),获取变量的类型,返回reflect.Type类型
- reflect.ValueOf(变量名),获取变量的值,返回reflect.Value类型(reflect.Value是一个结构体类型),通过reflect.Value,可以获取到关于该变量的很多信息。
package main
import(
"fmt"
"reflect"
)
//利用一个函数,函数的参数定义为空接口:
func testReflect(i interface{}){//空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋给空接口。
//1.调用TypeOf函数,返回reflect.Type类型数据:
reType := reflect.TypeOf(i)
fmt.Println("reType:",reType)
fmt.Printf("reType的具体类型是:%T",reType)
//2.调用ValueOf函数,返回reflect.Value类型数据:
reValue :=reflect.ValueOf(i)
fmt.Println("reValue:",reValue)
fmt.Printf("reValue的具体类型是:%T",reValue)
//num1 := 100
//如果真想获取reValue的数值,要调用Int()方法:返回v持有的有符号整数
num2 := 80 + reValue.Int()
fmt.Println(num2)
//reValue转成空接口:
i2 := reValue.Interface()
//类型断言:
n := i2.(int)
n2 := n + 30
fmt.Println(n2)
}
func main(){
//对基本数据类型进行反射:
//定义一个基本数据类型:
var num int = 100
testReflect(num)
}
对结构体类型反射
【1】反射相关的函数
- reflect.TypeOf(变量名),获取变量的类型,返回reflect.Type类型
- reflect.ValueOf(变量名),获取变量的值,返回reflect.Value类型(reflect.Value是一个结构体类型),通过reflect.Value,可以获取到关于该变量的很多信息。
package main
import(
"fmt"
"reflect"
)
//利用一个函数,函数的参数定义为空接口:
func testReflect(i interface{}){//空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋给空接口。
//1.调用TypeOf函数,返回reflect.Type类型数据:
reType := reflect.TypeOf(i)
fmt.Println("reType:",reType)
fmt.Printf("reType的具体类型是:%T",reType)
//2.调用ValueOf函数,返回reflect.Value类型数据:
reValue :=reflect.ValueOf(i)
fmt.Println("reValue:",reValue)
fmt.Printf("reValue的具体类型是:%T",reValue)
//reValue转成空接口:
i2 := reValue.Interface()
//类型断言:
n,flag := i2.(Student)
if flag == true {//断言成功
fmt.Printf("学生的名字是:%v,学生的年龄是:%v",n.Name,n.Age)
}
}
//定义学生结构体:
type Student struct{
Name string
Age int
}
func main(){
//对结构体类型进行反射:
//定义结构体具体的实例:
stu := Student{
Name : "丽丽",
Age : 18,
}
testReflect(stu)
}
获取变量的类别
【1】获取变量的类别:两种方式:
(1)reflect.Type.Kind() (2)reflect.Value.Kind()
【2】Kind的值是常量值
package main
import(
"fmt"
"reflect"
)
//利用一个函数,函数的参数定义为空接口:
func testReflect(i interface{}){//空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋给空接口。
//1.调用TypeOf函数,返回reflect.Type类型数据:
reType := reflect.TypeOf(i)
//2.调用ValueOf函数,返回reflect.Value类型数据:
reValue :=reflect.ValueOf(i)
//获取变量的类别:
//(1)reType.Kind()
k1 := reType.Kind()
fmt.Println(k1)
//(2)reValue.Kind()
k2 := reValue.Kind()
fmt.Println(k2)
//获取变量的类型:
//reValue转成空接口:
i2 := reValue.Interface()
//类型断言:
n,flag := i2.(Student)
if flag == true {//断言成功
fmt.Printf("结构体的类型是:%T",n)
}
}
//定义学生结构体:
type Student struct{
Name string
Age int
}
func main(){
//对结构体类型进行反射:
//定义结构体具体的实例:
stu := Student{
Name : "丽丽",
Age : 18,
}
testReflect(stu)
}
【3】Type和 Kind 的区别 Type是类型, Kind是类别,Type和Kind 可能是相同的,也可能是不同的. 比如:var num int = 10 num的Type是int , Kind也是int 比如:var stu Studentstu的 Type是 pkg1.Student , Kind是struct
通过反射修改变量
【1】修改基本数据类型的值:
package main
import(
"fmt"
"reflect"
)
//利用一个函数,函数的参数定义为空接口:
func testReflect(i interface{}){//空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋给空接口。
reValue :=reflect.ValueOf(i)
//通过SetInt()来改变值:
reValue.Elem().SetInt(40)
}
func main(){
//对基本数据类型进行反射:
//定义一个基本数据类型:
var num int = 100
testReflect(&num) //传入指针地址
fmt.Println(num)
}
通过反射操作结构体的属性和方法
package main
import(
"fmt"
"reflect"
)
//定义一个结构体:
type Student struct{
Name string
Age int
}
//给结构体绑定方法:
func (s Student) CPrint(){
fmt.Println("调用了Print()方法")
fmt.Println("学生的名字是:",s.Name)
}
func (s Student) AGetSum(n1,n2 int) int{
fmt.Println("调用了AGetSum方法")
return n1 + n2
}
func (s Student) BSet(name string,age int){
s.Name = name
s.Age = age
}
//定义函数操作结构体进行反射操作:
func TestStudentStruct(a interface{}){
//a转成reflect.Value类型:
val := reflect.ValueOf(a)
fmt.Println(val)
//通过reflect.Value类型操作结构体内部的字段:
n1 := val.NumField()
fmt.Println(n1)
//遍历-获取具体的字段:
for i := 0; i < n1;i++{
fmt.Printf("第%d个字段的值是:%v",i,val.Field(i))
}
fmt.Println()
//通过reflect.Value类型操作结构体内部的方法:
n2 := val.NumMethod()
fmt.Println(n2)
//调用CPrint()方法:
//调用方法,方法的首字母必须大写才能有对应的反射的访问权限
//方法的顺序按照ASCII的顺序排列的,a,b,c,,,,,,索引:0,1,2,,,,,
val.Method(2).Call(nil)
//调用AGetSum方法:
//定义Value的切片:
var params []reflect.Value
params = append(params,reflect.ValueOf(10))
params = append(params,reflect.ValueOf(20))
result := val.Method(0).Call(params)
fmt.Println("AGetSum方法的返回值为:",result[0].Int())
}
func main(){
//定义结构体具体的实例:
s := Student{
Name : "丽丽",
Age : 18,
}
//调用TestStudentStruct:
TestStudentStruct(s)
}
通过反射修改变量
package main
import(
"fmt"
"reflect"
)
//定义一个结构体:
type Student struct{
Name string
Age int
}
//给结构体绑定方法:
func (s Student) CPrint(){
fmt.Println("调用了Print()方法")
fmt.Println("学生的名字是:",s.Name)
}
func (s Student) AGetSum(n1,n2 int) int{
fmt.Println("调用了AGetSum方法")
return n1 + n2
}
func (s Student) BSet(name string,age int){
s.Name = name
s.Age = age
}
//定义函数操作结构体进行反射操作:
func TestStudentStruct(a interface{}){
//a转成reflect.Value类型:
val := reflect.ValueOf(a)
fmt.Println(val)
n := val.Elem().NumField()
fmt.Println(n)
//修改字段的值:
val.Elem().Field(0).SetString("张三")
}
func main(){
//定义结构体具体的实例:
s := Student{
Name : "丽丽",
Age : 18,
}
//调用TestStudentStruct:
TestStudentStruct(&s)
fmt.Println(s)
}
依赖包下载
项目所有依赖包下载
// 下面这个命令专门用于更新并下载项目中所有依赖包的最新版本。它的作用是递归地从当前目录开始,下载或更新所有依赖包的最新版本(包括主模块和子模块)。
go get -u ./...
// go mod tidy:该命令会整理你的go.mod文件,移除不必要的依赖,并下载项目中需要的所有依赖包。
go mod tidy
依次运行上面两个命令即可下载当前项目所有所需依赖的最新版本
根据go.mod文件定义的依赖包下载
go mod download // 该命令会下载所有go.mod文件中定义的依赖包。
构建或者运行项目时下载
// 当你执行构建或运行命令时,Go也会自动下载并安装所有需要的依赖包。
go build 或 go run
// 或者
go run main.go