一、Go语言基础
运行.go 程序
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
第一种:先通过go build xxx编译成 .exe 文件再运行这个exe文件(不依赖环境);
第二种:直接使用 go run xxx命令
安装go的sdk
golang社区:https://golang.google.cn/dl/
在社区中能下载不同版本的golang
配置系统环境变量
1、配置GOROOT系统环境变量
2、配置go语言SDK环境变量
执行流程
执行流程的方式区别
1、在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件
变大了很多。生成可执行文件后,这个文件可以在没有go开发环境下运行;
- 使用第一种方式编译还可以进行重命名,go build -o 重命名.exe 源文件.exe
2、使用go run xxx执行代码时,如果要在另外一个机器上这么运行,则需要go
开发环境,否则无法执行。
语法注意事项
- 源文件以"go"为扩展名。
- 程序的执行入口是main()函数。
- 严格区分大小写。
- 大括号都是成对出现的。
- 定义的变量或者import的包如果没有使用到,代码不能编译通过。
- 方法由一条条语句构成,每个语句后不需要分号(Go语言会在每行后自动加分号),这也体现出Golang的简洁性。
- Go编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否则报错。
Go语言的标准库
go语言具有中文社区(https://studygolang.com/),在其中可以看到Go语言提供的标准库以及相关用法。
二、数据类型
基本数据类型声明变量
package main
import "fmt"
func main() {
// 声明后赋值
var a int
a = 10
fmt.Println(a)
// 声明并赋值
var b int = 20
fmt.Println(b)
// 语法糖声明赋值
c := 30
fmt.Println(c)
}
1、整数类型
使用整数类型声明变量
- 指定类型的变量的值不能大于类型的最大值例如int8不能大于127否则会报错
- 如果给一个未声明类型的变量赋值,则这个变量会具有隐式类型
- 利用fmt库的 %T 能获取当前变量的类型
- 使用unsafe库的Sizeof方法可以看当前变量占用字节数
2、浮点类型
浮点数类型主要分为了float32和float64,如果不给定类型则默认为float64类型,浮点数可能会存在精度缺失的问题。
3、字符类型
在Golang中如果要存储单个字符,一般使用byte保存,使用的是UTF-8字符编码:
- 字母,数字,标点等字符,底层是按照ASCII进行存储;
- 汉字使用的是unicode编码;
- 字符类型,本质上就是一个整数,也可以直接参与运算,输出字符的时候,会将对应的码值做一个输出;
- 使用fmt标准库的 ‘%c’ 能格式化输出,将字符原样输出。
package main
import "fmt"
func main() {
var a byte = 'a'
fmt.Println(a) //97
var b byte = '('
fmt.Println(b + 20) //40
var c int = '中'
fmt.Println(c) //20013
var d byte = 'A'
fmt.Printf("c5对应的具体的字符为:%c", d) //A
}
4、布尔类型
占用一个字节。
5、字符串类型
如果字符串类型中字符含有特殊字符则可以使用反引号``包裹。
复杂数据类型
1、指针类型
声明变量实际上我们的变量是保存在一个内存地址中的
func main(){
var a int = 10
//&符号+变量 就可以获取这个变量内存的地址
fmt.Println(&a) // 0xc0000a2058
// 定义指针类型,想要定义指针类型使用 *数据类型的方法
var b *int = &a
fmt.Println(b) // 0xc0000a2058
fmt.Println("%v",*b) // 10 ===> 使用*内存地址可以得到源地址
}
数据类型转换
1、默认值
2、数据类型转换
Go在不同类型的变量之间赋值时需要显式转换,并且只有显式转换(强制转换)
2.1、基本数据类型转string
方式1:fmt.Sprintf(“%参数”,表达式)
这里第一个参数传递的是需要转换的参数类型例如:%d(整数)、%f(浮点数)、%t(布尔值)、%c(字符类型)
var a int = 10
var a1 string = fmt.Sprintf("%d",a) // '10'
var b float32 = 10.01
var b1 string = fmt.Sprintf("%f",b) // '10.01'
var c bool = false
var c1 string = fmt.Sprintf("%t",c) // 'false'
var d byte = 'd'
var d1 string = fmt.Sprintf("%c",d) // 'd'
方式2:使用strconv包的函数
strconv包提供了FormatInt、FormatFloat、FormatBool等格式化方法,但使用方式较为复杂,限制较多一般不使用。
var a int = 18
var a1 string = strconv.FormatInt(int64(a),10)
//参数:第一个参数必须转为int64类型 ,第二个参数指定字面值的进制形式
2.2、string转基本数据类型
string转基本数据类型使用到了strconv包的函数
- strconv.ParseInt (目标字符串, 进制, 整数类型)
- strconv.ParseFloat (目标字符串, 浮点数类型)
- strconv.ParseBool (目标字符串)
以上函数会返回两个值,value数据,和err出现的错误
var a string = "19"
var a1 int64
a1,_ = strconv.ParseInt(a,10,64)
fmt.Printf("类型是:%T,a1=%v \n",a1,a1)
自定义包的使用
- 不能与标准库冲突
- 需要配置对于项目的GOPATH
- 引入自定义包时,直接从GOPATH\src后引入即可
- 工具包需要被其他包调用的属性名需要大写
工具包
package test
var TestA int = 10
主包
package main
import (
"fmt"
"module02/test"
)
func main() {
fmt.Println(test.TestA)
}
三、运算符
特殊的运算符
1、++与–
**算术运算符规则和JS基本一致,只有++,–有区别,**区别如下:
go语言里,++,–操作非常简单,只能单独使用,不能参与到运算中去,++,--只能在变量的后面,不能写在变量的前面 --a ++a
错误写法。
func main(){
var a int = 10
a++
fmt.Println(a) //11
a--
fmt.Println(a) //10
var b int = a + a-- // syntax error: unexpected -- at end of statement
fmt.Println(b)
}
2、&与*
&返回变量的存储地址
*取指针变量对应的数值
package main
import "fmt"
func main(){
var a int = 10
fmt.Println(&a) //0xc0000100b0
var b *int = &a
fmt.Println(b)
fmt.Println(*ptr) //10
}
获取用户终端输入
有两种方式:
- fmt.scanln()
//方式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)
- fmt.scanf()
//方式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)
四、流程控制
1、条件分支
分为了if语句和switch语句
if a := 20; a > 30 {
fmt.Println("A")
} else {
fmt.Println("B")
}
var b int = 20
switch b {
case 10, 20:
fmt.Println("10")
case 30:
fmt.Println("20")
fallthrough //switch穿透
default:
fmt.Println("其他")
}
var c int = 30
switch {
case c > 20:
fmt.Println("10")
case c > 30:
fmt.Println("20")
fallthrough //switch穿透
default:
fmt.Println("其他")
}
- 针对于if结构需要注意的是if后的小括号可以不写,但是大括号必须写
- 针对于switch语句的switch后可以不添加变量,可以直接将变量判断写在case后
- switch语句每个case不用跟break
- switch中每个case的代码块可以使用fallthrough进行switch穿透,直接运行到下一个case中
2、循环结构
普通的for循环或for range循环。for range类似于js中的foreach方法。
for e := 0; e < 5; e++ {
fmt.Println(e)
}
var str = "hello World!"
for index, value := range str {
fmt.Printf("%d,%c\n", index, value)
}
关键字 break continue return goto
使用方式和js一致,但添加了两个新的用法
- goto可以跳到标记标签
- label: 使用此形式可以定义一个标签
label:
for e := 0; e < 5; e++ {
for f := 0; f < 5; f++ {
if f == 3 {
continue label
}
fmt.Println(f)
}
}
// continue的作用是结束离它近的那个循环,继续离它近的那个循环,但这里结束的是里层循环,继续的是外层循环。
// goto
fmt.Println(1)
goto label
fmt.Println(2)
fmt.Println(3)
fmt.Println(4)
label:
fmt.Println(5)
// 最终打印1 5,会跳过2 3 4
五、函数与包
1、函数
package utils
import "fmt"
func AddNum (a int, b int) int {
sum := a + b
return sum
}
func AddNumOrigin (a int, b int) (int,int,int) {
sum := a + b
return sum, a, b
}
func MulPrint (args...int) {
for i := 0; i < len(args); i++ {
fmt.Println(args[i])
}
}
________________________________________________________
package main
import (
"fmt"
"module05/utils"
)
func main() {
var res = utils.AddNum(12,23)
var res2,or1,or2 = utils.AddNumOrigin(12,23)
fmt.Println(res)
fmt.Println(res2, or1, or2)
utils.MulPrint(1,2,3,4)
}
- 可以传递任意个参数,参数使用args…数据类型进行接收。
- 可以传递一个值的内存地址,这样在函数内部也能修改到函数外部传递进入的值。
package main
import "fmt"
//定义一个函数:
func test(num int) {
fmt.Println(num)
}
func test02(num1 int, num2 float32, testFunc func(int)) {
testFunc(1)
fmt.Println("-----test02")
}
func main() {
a := test
fmt.Printf("a的类型是:%T,test函数的类型是:%T \n", a, test)
//a的类型是:func(int),test函数的类型是:func(int)
a(10)
test02(10, 3.19, test)
test02(10, 3.19, test)
test02(10, 3.19, a)
}
- go语言也能将函数作为参数传递给另一个函数
- 支持对函数返回值命名
func test03(num1 int, num2 int) (int, int) {
sub := num1 - num2
sum := num1 + num2
return sum, sub
}
func test04(num1 int, num2 int) (sum int,sub int) {
sum = num1 - num2
sub = num1 + num2
return
}
2、包
什么是包?
在程序层面,所有使用相同 package 包名 的源文件组成的代码模块,在同一个包下不能有重复的声明。
使用包的原因:
- 我们不可能把所有的函数放在同一个源文件中,可以分类的把函数放在不同的源文件中
- 解决同名问题,在同一个文件中是不可以定义相同名字的函数的,此时可以用包来区分
包引用时也能起别名
package main
import (
"fmt"
alias "module05/utils"
)
func main() {
var res = alias.AddNum(12,23)
var res2,or1,or2 = alias.AddNumOrigin(12,23)
fmt.Println(res)
fmt.Println(res2, or1, or2)
}
3、关于init函数、main函数和其他函数的执行时机
先执行引入包中的函数、再执行当前包的函数
而这里针对于函数的优先级为:变量定义 > init函数 > main函数
4、匿名函数与闭包
Go支持匿名函数,如果希望函数只是使用一次,可以考虑使用匿名函数,在声明时就调用
针对于闭包的话,按照下面的例子解释更加清晰:
- 不使用闭包的时候:我想保留的值,不可以反复使用
- 闭包应用场景:闭包可以保留上次引用的某个值,我们可以反复使用上次的值
package main
import "fmt"
func getSum() func (int) int {
var sum int = 0
return func (num int) int{
sum = sum + num
return sum
}
}
func getSum01(sum int,num int) int{
sum = sum + num
return sum
}
func main(){
f := getSum()
fmt.Println(f(1))//1
fmt.Println(f(2))//3
fmt.Println(f(3))//6
fmt.Println(f(4))//10
fmt.Println(getSum01(0,1))//1
fmt.Println(getSum01(1,2))//3
fmt.Println(getSum01(3,3))//6
fmt.Println(getSum01(6,4))//10
}
5、系统函数
字符串函数
len() :统计字符串的长度
str := "golang你好"
fmt.Println(len(str)) // 12 (因为在golang中,汉字是三个字节)
for-range
str := "golang"
for i, v := range str {
fmt.Printf("%d,%c", i, v)
}
res := []rune(str)
str := "golang"
res := []rune(str)
fmt.Println(res) //[103 111 108 97 110 103]
for i := 0; i < len(res); i++ {
fmt.Printf("%c\n", res[i])
}
/*
g
o
l
a
n
g
*/
字符串转整数
n, err := strconv.Atoi("10")
整数转字符串
str := strconv.Itoa(10)
查找子串是否在指定的字符串中
strings.Contains("golanggolang", "go")
统计一个字符串有几个指定的子串
strings.Count("golanggolang","a")
不区分大小写的字符串比较
strings.EqualFold("go" , "Go")
返回子串在字符串第一次出现的索引值,如果没有返回-1
strings.lndex("golanggolang" , "a")
日期函数
时间和日期的函数,需要导入time包,所以你获取当前时间,就要调用**time.Now()**函数。
- Now()返回值是一个结构体,类型是:time.Time
package main
import (
"fmt"
"time"
)
func main(){
now := time.Now()
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())
}
格式化日期
//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)
按照指定格式输出,这里每个数字都是固定的不能改变,但是可以随意组合。
//这个参数字符串的各个数字必须是固定的,必须这样写
datestr2 := now.Format("2006/01/02 15/04/05")
fmt.Println(datestr2)
//选择任意的组合都是可以的,根据需求自己选择就可以(自己任意组合)。
datestr3 := now.Format("2006 15:04")
fmt.Println(datestr3)
内置函数
Golang设计者为了编程⽅便,提供了⼀些函数,这些函数不⽤
导包可以直接使⽤,我们称为Go的内置函数/内建函数。
常⽤函数:
(1)len函数:
统计字符串的⻓度,按字节进⾏统计
(2)new函数:
分配内存,主要⽤来分配值类型(int系列, float系列, bool,
string、数组和结构体struct),返回的是Type的指针
(3)make函数:
分配内存,主要⽤来分配引⽤类型(指针、slice切⽚、map、管
道chan、interface 等),返回的是Type本身
defer关键字
defer是Go里面的一个关键字,用在方法或函数前面,作为方法或函数的延迟调用;
作用是进行优雅释放资源。
官方对defer的解释中写到每次defer语句执行的时候,会把函数压栈,同时函数参数会被拷贝下来。这两点很重要:
-
一是说明当一个函数中有多个defer的时候,执行顺序是先进后出
-
二是说明延迟函数的参数在defer语句出现时就已经确定下来了
func CopyFile(dstFile, srcFile string) (wr int64, err error) {
src, err := os.Open(srcFile)
if err != nil {
return
}
dst, err := os.Create(dstFile)
if err != nil {
return
}
wr, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
func main() {
deferRun()
deferRun2()
}
func deferRun() {
var num = 1
defer test(&num)
num = 2
return
}
func test (b *int) {
fmt.Println(*b)
}
func deferRun2() {
var arr = [4]int{1, 2, 3, 4}
defer printArr(&arr)
arr[0] = 100
return
}
func printArr(arr *[4]int) {
for i := range arr {
fmt.Println(arr[i])
}
}
defer+recover机制处理错误
程序中出现错误/恐慌以后,程序被中断,⽆法继续执⾏。这里可以引入机制:defer+recover机制处理错误;
func main() {
test()
fmt.Println("除法成功")
fmt.Println("正常执行")
}
func test() {
defer func() {
err := recover()
if err != nil {
fmt.Println("错误信息", err)
}
}()
num1 := 10
num2 := 0 // 0不能做除数
result := num1 / num2
fmt.Println(result)
}
package main
import (
"fmt"
"errors"
)
func main() {
err := test()
if err != nil {
fmt.Println("自定义错误",err)
panic(err) //还有可能的情况就是,遇到错误后直接结束。
}
fmt.Println("除法成功")
fmt.Println("正常执行")
}
func test()(err error){
num1 := 10
num2 := 0
if num2 == 0 {
return errors.New("除数不能为0")
}else {
result := num1 / num2
fmt.Println(result)
return nil
}
}
六、数组
数组定义格式
var 数组名 [数组⼤⼩]数据类型
func main() {
var scores [5]int
scores[0] = 95
scores[1] = 91
scores[2] = 39
scores[3] = 60
scores[4] = 21
fmt.Println(scores)
}
数组的初始化方式
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)
}
二维数组
func main() {
// 二维数组的定义
var arr1 [3][2]int
// fmt.Printf("%T",arr1)
arr1[0][0] = 10
arr1[1][0] = 11
arr1[2][0] = 12
fmt.Println(arr1)
// 二维数组初始化
var arr2 = [2][2]int{{1,2},{3,4}}
fmt.Println(arr2)
}
数组的遍历
- for循环遍历
- for range遍历
for key, val := range 数组 {
...
}
七、切片(slice)
切片(slice)是golang中⼀种特有的数据类型;
切片是⼀种建⽴在数组类型之上的抽象,它构建在数组之上并且提供更强⼤的能⼒和便捷;
切片(slice)是对数组⼀个连续片段的引用,所以切片是⼀
个引用类型。这个片段可以是整个数组,或者是由起始和终止索
引标识的⼀些项的子集。需要注意的是,终止索引标识的项不包
括在切片内。
切片定义格式
方式一
var 切⽚名 []类型 = 数组的⼀个⽚段引⽤
func main() {
var arr = [5]int{1,2,3,4,5}
slice1 := arr[1:3]
fmt.Println(slice1) //[2,3]
// 切片的长度
fmt.Println(len(slice1)) //2
// 获取切片的容量,这个是可以动态变化的
fmt.Println(cap(slice1)) //4
}
方式二
var 切⽚名 type = make([], len,[cap])
slice2 := make([]int,4,20) //类型,长度,类型
fmt.Println(slice2)
// 切片的长度
fmt.Println(len(slice2))
// 获取切片的容量,这个是可以动态变化的
fmt.Println(cap(slice2))
方式三
slice3 := []int{1,2,3} //类型,长度,类型
fmt.Println(slice3)
// 切片的长度
fmt.Println(len(slice3))
// 获取切片的容量,这个是可以动态变化的
fmt.Println(cap(slice3))
切片的遍历
- for循环常规方式遍历
- for-range 结构遍历切片
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)
}
}
注意事项
1、简写方式:
- var slice = arr[0:end] 等价于 var slice = arr[:end]
- var slice = arr[start:len(arr)] 等价于 var slice = arr[start:]
- var slice = arr[0:len(arr)] 等价于 var slice = arr[:]
2、切片不能越界
3、切片后还能继续切片
4、切片增加元素
- 通过append函数将切片追加给切片:
//定义数组:
var intarr [6]int = [6]int{1,4,7,3,6,9}
//定义切片:
var slice []int = intarr[1:4]
slice2 := append(slice,88,50)
fmt.Println(slice2) //[4 7 3 88 50]
slice2 = append(slice2,slice...)
fmt.Println(slice2)//[4 7 3 88 50 4 7 3]
5、切片的拷贝
func main() {
//定义切片:
var a []int = []int{1, 4, 7, 3, 6, 9}
//再定义一个切片:
var b []int = make([]int, 10)
//拷贝:
copy(b, a)
//将a中对应数组中元素内容复制到b中对应的数组中
//元素多了会丢失多余的,元素少了会补0
fmt.Println(b) //[1 4 7 3 6 9 0 0 0 0]
}
八、map(映射)
映射(map), Go语言中内置的一种类型,它将键值对相关联,我们可以通过键 key来获取对应的值 value;
基本语法
// 声明map,但暂未分配空间
var map变量名 map[key type]value type
PS:key、value的类型:bool、数字、string、指针、channel 、还可以是只包含前面几个类型的接口、结构体、数组
PS:key通常为int 、string类型,value通常为数字(整数、浮点数)、string、map、结构体
PS:key:slice、map、function不可以
map的特点:
(1)map集合在使用前一定要make
(2)map的key-value是无序的
(3)key是不可以重复的,如果遇到重复,后一个value会替换前一个value
(4)value可以重复的
package main
import "fmt"
func main(){
//定义map变量:
var a map[int]string
a = make(map[int]string,10) //map可以存放10个键值对
//将键值对存入map中:
a[20095452] = "张三"
a[20095387] = "李四"
a[20097291] = "王五"
a[20095337] = "朱六"
a[20096699] = "张三"
//输出集合
fmt.Println(a) // map[20095385:朱六 20095387:李四 20095452:张三 20096699:张三20097291:王五]
}
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的操作
- 增加和更新操作:
map[“key”]= value ——》 如果key还没有,就是增加,如果key存在就是修改。 - 删除操作:
delete(map,“key”) , delete是一个内置函数,如果key存在,就删除该key-value,如果k的y不存在,不操作,但是也不会报错 - 清空操作:
(1)如果我们要删除map的所有key ,没有一个专门的方法一次删除,可以遍历一下key,逐个删除
(2)或者map = make(…),make一个新的,让原来的成为垃圾,被gc回收 - 查找操作:
value ,bool = map[key]
value为返回的value,bool为是否返回 ,要么true 要么false
九、面向对象
(1)Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。
(2)Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解Gelang是基于struct来实现OOP特性的。
(3)Golang面向对象编程非常简洁,去掉了传统OOP语言的方法重载、构造函数和析构函数、隐藏的this指针等等
(4)Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承:Golang没有extends 关键字,继承是通过匿名字段来实现。
结构体
创建结构体实例方式
方式一:直接创建
package main
import "fmt"
type Teacher struct {
Name string
Age int
School string
}
func main() {
var t1 Teacher
fmt.Println(t1)
t1.Name = "terry"
t1.Age = 23
t1.School = "东软"
fmt.Println(t1)
fmt.Println(t1.Age + 10)
}
方式二:创建时携带初始值
var t1 Teacher = Teacher{"terry", 23, "东软"}
方式三:返回的是结构体指针
var t1 *Teacher = &Teacher{"terry", 23, "东软"}
(*t1).Name = "张三"
fmt.Println(*t1)
方式四:返回的是结构体指针
var t1 *Teacher = &Teacher{"terry", 23, "东软"}
(*t1).Name = "张三"
fmt.Println(*t1)
结构体之间的转换
- 结构体之间转换必须要有完全相同的字段(名字、个数和类型)
- 结构体进行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)
}
方法的引入
- 方法是作用在指定的数据类型上、和指定的数据类型绑定,因此自定义类型,都可以有方法,而不仅仅是struct
- 方法的声明和调用格式
//声明:
type A struct {
Num int
}
func (a A) test() {
fmt.Println(a.Num)
}
//调用:
var a A
a.test()
注意事项
- 在方法的调用中,我们可以在外部传参是传递值或值对应的地址,来控制方法内是否能修改到外部的值;
- 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)
}
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
方法和函数的区别
- 绑定指定类型:
方法:需要绑定指定数据类型
函数:不需要绑定数据类型 - 调用方式不一样:
函数的调用方式: 函数名(实参列表)
方法的调用方式:变量.方法名(实参列表)
创建结构体实例时指定字段值
【1】方式1:按照顺序赋值操作
缺点:必须按照顺序有局限性
【2】方式2:按照指定类型
【3】方式3:想要返回结构体的指针类型
type Student struct {
Name string
Age int
}
func main() {
// 方式一
var s1 Student = Student{"小李", 19}
fmt.Println(s1)
// 方式二
var s2 Student = Student{Name: "Terry", Age: 18}
fmt.Println(s2)
// 方式三
var s3 *Student = &Student{Name: "Terry", Age: 18}
fmt.Println(*s3)
}
跨包创建结构体实例
- 直接暴露结构体,只需要在其他包将结构体首字母大小即可
- 当结构体首字母小写时可以使用暴露函数的方法来返回结构体
封装
继承
- 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体。
- 通过嵌入匿名结构体来进行继承
- 继承的优点,提高代码的复用性、扩展性
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.Age = 3
cat.Weight = 10.6
cat.Shout()
cat.ShowInfo()
cat.scratch()
}
注意事项
- 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
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)
}
- 结构体的匿名字段可以是基本数据类型、指针、结构体类型
接口
- 接口中可以定义一组方法,但不需要实现,不需要方法体。并且接口中不能包含任何变量。到某个自定义类型要使用的时候(实现接口的时候),再根据具体情况把这些方法具体实现出来。
- Golang中实现接口是基于方法的,不是基于接口的,实现接口要实现所有的方法才是实现;
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")
}
func greet(s SayHello) {
s.sayHello()
}
func main() {
c := Chinese{}
a := American{}
greet(a)
greet(c)
}
多态
可以按照统一的接口来调用不同的实现
例如上述案例,greet方法的参数s就是个多态参数;
断言
判断是否是该类型的变量: value, ok := element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型
func greet(s SayHello) {
s.sayHello()
//断言:
ch, flag := s.(Chinese) //看s是否能转成Chinese类型并且赋给ch变量
if flag == true {
ch.niuYangGe()
} else {
fmt.Println("美国人不会扭秧歌")
}
}
Type Switch
Type Switch 是 Go 语言中一种特殊的 switch 语句,它比较的是类型而不是具体的值。它判断某个接口变量的类型;
func greet(s SayHello) {
s.sayHello()
switch s.(type) { //type属于go中的一个关键字,固定写法
case Chinese:
ch := s.(Chinese)
ch.niuYangGe()
case American:
am := s.(American)
am.disco()
}
}
十、文件操作
打开关闭文件
os包下的File结构体封装了对文件的操作
File结构体—打开文件和关闭文件:
(1)打开文件,用于读取,传入一个字符串(文件的路径),返回的是文件的指针,和是否打开成功
OpenFile可配置文件打开模式,(可以利用"|"符号进行组合)
(2)关闭文件:(方法)
package main
import (
"fmt"
"os"
)
func main() {
//打开文件:
file, err := os.Open("C:/Users/admin/Desktop/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("C:/Users/admin/Desktop/test.txt") //返回内容为:[]byte,err
if err != nil { //读取有误
fmt.Println("读取出错,错误为:", err)
}
//如果读取成功,将内容显示在终端即可:
fmt.Printf("%v", content) //因为它是一个字节数组,所以需要转为string
fmt.Printf("%v", string(content))
}
读取文件(带缓冲区)
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
//打开文件:
file, err := os.Open("C:/Users/admin/Desktop/test.txt")
if err != nil { //打开失败
fmt.Println("文件打开失败,err=", err)
}
//当函数退出时,让file关闭,防止内存泄露:
defer file.Close()
//创建一个流:
reader := bufio.NewReader(file)
//读取操作:
for {
str, err := reader.ReadString('\n') //读取到一个换行就结束 如果读取完毕err会返回EOF
if err == io.EOF { //io.EOF 表示已经读取到文件的结尾
break
}
//如果没有读取到文件结尾的话,就正常输出文件内容即可:
fmt.Println(str)
}
//结束:
fmt.Println("文件读取成功,并且全部读取完毕")
}
写入文件
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
//写入文件操作:
//打开文件:
file, err := os.OpenFile("C:/Users/admin/Desktop/test.txt", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
if err != nil { //文件打开失败
fmt.Println("打开文件失败", 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)
}
这里0666代表的是权限:
四位数代表意思: 特殊权限用户,用户,组用户,其它用户
每位值代表意思:读是4,写是2,执行是1
0666 就是用户,组用户,其它用户都只有读写权限;
文件复制
package main
import(
"fmt"
"io/ioutil"
)
func main(){
//定义源文件:
file1Path := "C:/Users/admin/Desktop/test.txt"
//定义目标文件:
file2Path := "C:/Users/admin/Desktop/test2.txt"
//对文件进行读取:
content,err := ioutil.ReadFile(file1Path)
if err != nil {
fmt.Println("读取有问题!")
return
}
//写出文件:
err = ioutil.WriteFile(file2Path,content,0666)
if err != nil {
fmt.Println("写出失败!")
}
}
十一、协程
程序
是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。
进程(process)
是程序的一次执行过程。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。 (进程是动态的)是一个动的过程 ,进程的生命周期 : 有它自身的产生、存在和消亡的过程 。
线程(thread)
进程可进一步细化为线程, 是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。
协程(goroutine)
概念
又称为微线程,纤程,协程是一种用户态的轻量级线程
作用:在执行A函数的时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),注意这一切换过程并不是函数调用(没有调用语句),过程很像多线程,然而协程中只有一个线程在执行(协程的本质是个单个线程)
对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就将寄存器上下文和栈保存到某个其他地方,然后切换到另外一个任务去计算。在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态(注意:线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级)
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)
}
}
主死从随
- 如果主线程退出了,则协程即使还没有执行完毕,也会退出
- 当然协程也可以在主线程没有退出前,就自己结束了,比如完成了自己的任务
使用WaitGroup控制协程退出
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()
}
- 如果知道有几个协程那么可以先add协程数量
- Done操作可以配合defer关键字来使用,使其在协程执行最后再减1
互斥锁
当多个协程同时操作统一数据时,会出现更新错误的情况
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)
}
按理说结果应该是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(读写锁);
- 互斥锁
其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别
----性能、效率相对来说比较低
- 读写锁
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)
fmt.Println("修改数据成功")
lock.Unlock()
}
func main() {
wg.Add(6)
//启动协程 ---> 场合:读多写少
for i := 0; i < 5; i++ {
go read()
}
go write()
wg.Wait()
}
管道(channel)
- 管道本质就是一个数据结构-队列
- 数据是先进先出
- 自身线程安全,多协程访问时,不需要加锁,channel本身就是线程安全的
- 管道有类型的,一个string的管道只能存放string类型数据
管道的定义
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\n", 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))
管道的关闭
使用内置函数close可以关闭管道,当管道关闭后,就不能再向管道写数据了,但是仍然可以从该管道读取数据。
close(intChan)
管道的遍历
管道支持for-range的方式进行遍历,请注意两个细节
- 在遍历时,如果管道没有关闭,则会出现deadLock的错误
- 在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
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)
}
}
协程和管道协同工作
package main
import (
"fmt"
"sync"
"time"
)
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, 50)
wg.Add(2)
//开启读和写的协程:
go writeData(intChan)
go readData(intChan)
//主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
}
声明只读或只写管道
//默认情况下,管道是双向的--》可读可写:
var intChan1 chan int
//声明为只写:
var intChan2 chan <- int // 管道具备<- 只写性质
//声明为只读:
var intChan3 <- chan int // 管道具备<- 只读性质