这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
包管理
go中的包分为两类:main包和非main包
main包
程序的入口
非main包
用来包分类的。 我们编译运行的时候最好采用 go build方法,因为go run 默认是运行一个包,右击goLand也是一个意思。
输出
- 内置函数 (不推荐,因为官方说明可能后面会删除掉)
-
- println
- fmt包(推荐)
-
- fmt.Println
- fmt.Printf
变量
变量的声明方式
全局变量
全局变量必须使用var声明,不能使用:的方式
数据类型
- 基本数据类型
-
- 整型
-
-
- int8
- int16
- int32 (int默认)
- int64
- uint
- uint8
- uint16
- uint32
- uint64
- byte
-
-
- 浮点型
-
-
- float32
- float64
-
-
- 布尔型(bool)
- 字符串 (string)
- 字符型 : 没有单独的字符型,使用byte保存单独的字母字符
- 派生数据类型(复杂数据类型)
-
- 指针
- 数组
- 结构体
- 管道
- 函数
- 切片
- 接口
- map
整型
/*
无符号整型: uint8 uint16 uint32
整型: int8 int16(short) int32(默认int) int64(long)
特殊整型: uint(根据操作系统32、64) int uintptr(无符号整型,用于存放一个指针)
*/
func main() {
// 其他进制转十
x, y, z := 0b1000, 0o77, 0xff
fmt.Printf("2转10:%d", x) // 8
fmt.Printf("8转10:%d", y) // 63
fmt.Printf("16转10:%d", z) // 255
// 十转其他进制
num := 16
fmt.Printf("10转2:%b", num) // 10000
fmt.Printf("10转8:%o", num) // 20
fmt.Printf("10转16:%x", num) // 10
}
| 类型 | 等价 | 数据范围 |
|---|---|---|
| rune | int32 | -2^31--2^31-1 |
| byte | uint8 | 0--255 |
注意事项
获取对象的长度的内建len()函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用int和 uint。
浮点型
/*
float32 最大值:3.4e38(math.MaxFloat32)
float64(默认) 最大值:1.8e3058(math.MaxFloat64)
格式控制符: %f
*/
func main(){
pi := 3.1415
var e float32 = 2.73
fmt.Printf("pi=%f e=%f", pi, e)
}
布尔型
Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)和false(假)两个值。
注意事项
- 布尔类型变量的默认值为
false。 - Go 语言中不允许将整型强制转换为布尔型.
- 布尔型无法参与数值运算,也无法与其他类型进行转换
字符串
同样,字符串是一个不可变的。他底层是一个byte数组。所以可以把它转成切片然后修改。但是最好转为[]rune,因为转为[]byte的话不兼容中文,因为一个中文代表三个字节
其实string本质上也是一个切片,他由两部分组成,一部分是指向底层数组0号位置的指针,一部分是长度。
func main() {
// 使用双引号包裹的叫字符(UTF-8编码)
name := "Agoni"
fmt.Printf("name=%s", name)
// 多行字符串使用``包裹,内容原样输出
poem = `
字节和心脏只有一个在跳动
`
fmt.Println(poem)
fmt.Println("444"+4) // Error 不能和其他类型做拼接
}
字符串常用操作
| 操作函数 | 含义 | 使用方法 |
|---|---|---|
| len() | 求长函数 | len(str) |
| fmt.Sprintf() | 字符串拼接 | fmt.Sprintf("%s-%s", str1, str2) |
| strings.Split() | 分割 | strings.Split(str, 分隔符) |
| strings.Contains() | 包含 | strings.Contains(str, sub1) |
| strings.HasPrefix() | 前缀 | strings.HasPrefix(str, prefix) |
| strings.HasSuffix() | 后缀 | strings.HasSuffix(str, suffix) |
| strings.Index() | 返回子串首次出现位置 | strings.Index(str, sub) |
| strings.LastIndex() | 返回字串最后一次出现位置 | strings.LastIndex(str, sub) |
| strings.Join() | join拼接 | strings.Join(字符串数组, 连接符) |
字符型
Go中没有单独的字符型,使用byte保存单独的字母字符。所以他本质上是一个int类型,也就是说他可以和数组做算术运算
var c1 byte = 'a'
fmt.Println(c1) // 输出 97
//
fmt.Println(c+1) // 输出 98
转义符
| 转义符 | 含义 |
|---|---|
\r | 回车符(返回行首) |
\n | 换行符(直接跳到下一行的同列位置) |
\t | 制表符 |
' | 单引号 |
" | 双引号 |
\ | 反斜杠 |
格式控制符
| 格式控制符 | 含义 |
|---|---|
| %b | 二进制整型 |
| %d | 十进制整型 |
| %o | 八进制整型 |
| %x | 十六进制整型 |
| %f | 浮点型 |
| %s | 字符串 |
| %c | 字符型 |
| %t | 布尔型 |
| %T | 数据类型 |
| %v | 数据值 |
类型转换
var x int = 100
var y float64 = float64(x)
fmt.Println(y)
fmt.Printf("%T\n",y)
fmt.Printf("%T\n",x) // 原值保持不变
// 编译不会出错,但是会溢出,
var n3 int64 = 888888
var n4 int8 =int8(n3)
fmt.Println(n4) // 56
其他类型和string类型之间的转换
// 方法一:
var n1 int = 19
var n2 float32 = 3.12
//var n3 bool = false
//var n4 byte = 'a'
s1 := fmt.Sprintf("%d",n1)
fmt.Printf("%T,....%v\n",s1,s1)
s2 := fmt.Sprintf("%f",n2)
fmt.Printf("%T,....%v\n",s2,s2)
// 方法二:使用strconv包原生,极其麻烦,不推荐
// 方法三:使用strconv的Atoi进行整型和string之间的转换
i,_:=strconv.Atoi("-42") // 返回一个整数和一个error
print(i)
s:=strconv.Itoa(-42)
print(s)
// string--->bool
f,_ := strconv.ParseBool("true")
print(f)
strconv 包(简用)
strconv用于字符串和整型之间的类型转换
1.Atoi:字符串转整型
2.Itoa:整型转字符串
指针
指针就是内存地址
&: 可以获取地址
*:可以通过地址得到值
1.可以通过指针修改指向的值
2. 指针指向的一定是一个地址值
3. 指针变量的地址类型不可以不匹配
func main() {
n := 10
fmt.Println(n)
// var p *int = &n
p := &n //p指针指向n
*p = 20 // 通过指针修改值
fmt.Println(n)
}
输入
- fmt.Scanln(&name) // 遇到空格结束
- fmt.Scanf("%d %f %s",&n,&f,&s) 类似c语言
流程控制
- if....else
- switch
- for
注意:
- switch和其他语言不一样,他的表达式可以是任意类型,java只能是整数类型,而Go可以是任意类型!!
- switch不需要和break配合使用就可以自动停止!
函数
函数定义
func func_name(形参) 返回值类型 {
}
函数的参数传递
如果函数中的形参类型是基本数据类型和数组,那么值不会改变,也就是值传递。(重点记忆数组,因为这是和java的区别)
主要是因为局部变量的问题
func f2(nums [4]int) {
for i := 0; i < len(nums); i++ {
println(nums[i])
}
nums[0] = 100000
}
func main() {
nums := [4]int{12, 11, 243, 5}
f2(nums)
for i, num := range nums {
println(i, num)
}
// 输出还是 12, 11, 243, 5,并不是 100000, 11, 243, 5,因为数组是值传递
}
如果基本数据类型想要做引用传递,那么就将形参改为指针类型()使用的时候加上,然后实参传入的时候加上(&)
func f2(n *int) {
*n = 10;
}
func main() {
n:=20;
f2(&n)
fmt.Println(n)
}
Go中函数不支持重载,但是支持可变参数
可变参数是指:函数的形参数量可变。可变参数当做切片来处理
package main
import "fmt"
func f1(args ...string) {
// 可变参数当成切片来处理
fmt.Printf("%T\n", args)
for i := 0; i < len(args); i++ {
fmt.Println(args[i])
}
}
func main() {
f1()
fmt.Println("-----------------")
f1("a")
fmt.Println("-----------------")
f1("a", "b")
fmt.Println("-----------------")
f1("a", "b", "c")
}
在Go语言中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
func f3(n int) int {
return n
}
func main() {
n:=20;
a:=f3
fmt.Printf("%T\n",a) // func(int) int 这就是a的数据类型
a(n)
}
既然函数是一种数据类型,那么函数就可以作为形参传入了
func f3(n int) int {
return n
}
func f4(n1 int, n2 float32, f3 func(int) int) {
fmt.Println("-------------f4-----------------")
}
func main() {
n:=20;
a:=f3
fmt.Printf("%T\n",a)
a(n)
f4(12,12.9,f3)
f4(12,12.9,a)
}
Go支持自定义数据类型
就是c语言的给数据类型改名
一般是给函数类型改名,因为函数是一个数据类型,他名字太长了!! (太细了)
type myInt int // 定义了一个myInt类型
var n1 myInt = 30
var n2 int = 10
n2 = n1 // 注意这里是不可以的,因为环境认为这两种数据类型不相同,不能进行赋值操作
n2 = int(n1) // 可以进行强转完成
Go函数返回值可以是多个,返回值可以有名字也可以没有
init函数
在main函数之前执行
package main
import "fmt"
func init() {
fmt.Println("init执行")
}
func main() {
fmt.Println("主函数执行")
}
// 输出
/*
init执行
主函数执行
*/
问题:全局变量、init函数、主函数、主函数中引用其他源文件里面的init函数,他们直接的执行顺序?
答:导入的函数------>主函数的全局变量------>主函数中的init------->main函数
匿名函数
package main
import "fmt"
func main() {
// 匿名函数,在定义的同时并调用
a:=func(n1 int, n2 int) int {
return n1 + n2
}(1, 2)
fmt.Println(a)
// 不常用,因为匿名函数只是想用一次,没必要重复使用,这里其实就类似于java中的内部函数了
sub:= func(n1 int,n2 int) (r int) {
r = n1-n2;
return r
}
r:=sub(7,5)
fmt.Println(r)
}
闭包
就是函数的套娃,及其像java的静态变量
闭包:匿名函数+引用的变量/参数
package main
import "fmt"
// getNum是函数名,是一个无参的函数
// func(int) int是返回值类型,是一个函数类型
func getNum() func(int) int {
var sum = 0
// 匿名函数
return func (num int) int {
sum +=num
return sum
}
}
func main() {
f:=getNum()
fmt.Println(f(1)) // 1
fmt.Println(f(2)) // 3 这就类似java中的静态变量了,他是一个共享的
// 闭包:返回的匿名函数+匿名函数之外的变量(sum)
}
注意:匿名函数中引用的变量会一直保存到内存中,可以一直使用(类似java的静态属性),使用不能乱用闭包,会对内存消耗比较大。
闭包的应用场景:
闭包可以保存上次引用的某个值,传入一次就可以反复使用了。在后端登录的时候,session可能会用到这个功能
defer关键字
类似于finally,最后执行。
在Go程序中,如果遇到defer关键字,不会立即执行该语句,而是将这条语句压入栈中,然后继续执行其后面的语句
系统内置函数
runtime
runtime.Caller(),能拿到当前调用该函数的行号、函数名、包名
os
系统调用库
os.Args:获取命令行参数
数学库
除了常用的 max、min、abs等,还有:
- math.E:2.7几
- Modf(3.6) 返回两个值:整数部分和小数部分 3 和 0.6
- Log(3):以E为底3的对数
- 乱七八糟的
time库
now.Month() 输出的是字符串月份,直接强转为int就可以变成数字了,因为他底层就是一个int类型的类似枚举的东西
func main() {
now := time.Now()
fmt.Printf("%T, %v\n",now,now) // 返回的是结构体类型 time.Time
fmt.Println(now.Year())
fmt.Println(now.Month())
fmt.Println(int(now.Month()))
fmt.Println(now.Day())
fmt.Println(now.Hour())
fmt.Println(now.Minute())
fmt.Println(now.Second())
fmt.Println("---------------------")
// 日期格式化, 有返回值
s:=fmt.Sprintf("%d-%d-%d",now.Year(),now.Month(),now.Day())
fmt.Println(s)
// 方式二:
s=now.Format("2006-01-02 15:04:05")
fmt.Println(s)
// 时间做差
// 1.
start := time.Now()
fmt.Println(time.Now().Sub(start))
// 2.
begin := time.Now()
fmt.Println(time.Since(begin))
// 时间做加法,不能直接加 要转成Duration Time是一个时刻,Duration是时间段
dua := time.Duration(8 * time.Hour)
end := begin.Add(dua) // begin+八小时
fmt.Println(end)
}
定时器 ,只能用一次(两种方式;老)
fmt.Println(time.Now().Unix())
// 3s之后执行
tm := time.NewTimer(3 * time.Second)
defer tm.Stop()
<-tm.C
fmt.Println(time.Now().Unix())
<-time.After(3 * time.Second)
fmt.Println(time.Now().Unix())
用多次
// 类似定时器
tc := time.NewTicker(2 * time.Second)
defer tc.Stop()
for i := 0; i < 6; i++ {
<-tc.C // 每隔2s钟试图从管道中读数据,但是没有,所以就会被阻塞,2s之后就结束阻塞
fmt.Println(time.Now().Unix())
}
flag
脚本工具
flag包支持的命令行参数类型有bool、int、int64、uint、uint64、float float64、string、duration(时间间隔)。
| flag参数 | 有效值 |
|---|---|
| 字符串flag | 合法字符串 |
| 整数flag | 1234、0664、0x1234等类型,也可以是负数。 |
| 浮点数flag | 合法浮点数 |
| bool类型flag | 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。 |
| 时间段flag | 任何合法的时间段字符串。如”300ms”、”-1.5h”、”2h45m”。 合法的单位有”ns”、”us” /“µs”、”ms”、”s”、”m”、”h”。 |
定义命令行flag参数
flag.Type()
flag.Type(flag名, 默认值, 提示信息)*Type 例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
name := flag.String("参数名", "默认值", "提示信息")
age := flag.Int("age", 18, "年龄")
married := flag.Bool("married", false, "婚否")
delay := flag.Duration("d", 0, "时间间隔")
需要注意的是,此时name、age、married、delay返回值均为对应类型的指针。
flag.TypeVar()
基本格式如下: flag.TypeVar(Type指针, flag名, 默认值, 提示信息) 例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "时间间隔")
flag.Parse()
通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。
支持的命令行参数格式有以下几种:
- -flag xxx (使用空格,一个-符号)
- --flag xxx (使用空格,两个-符号)
- -flag=xxx (使用等号,一个-符号)
- --flag=xxx (使用等号,两个-符号)
其中,布尔类型的参数必须使用等号的方式指定。
Flag解析在第一个非flag参数(单个”-“不是flag参数)之前停止,或者在终止符”–“之后停止。
其他函数
flag.Args() ////返回命令行参数后的其他参数,以[]string类型
flag.NArg() //返回命令行参数后的其他参数个数
flag.NFlag() //返回使用的命令行参数个数
完整案例
func main() {
//定义命令行参数方式1
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "延迟的时间间隔")
//解析命令行参数
flag.Parse()
fmt.Println(name, age, married, delay)
//返回命令行参数后的其他参数
fmt.Println(flag.Args())
//返回命令行参数后的其他参数个数
fmt.Println(flag.NArg())
//返回使用的命令行参数个数
fmt.Println(flag.NFlag())
命令行参数使用提示:
$ ./flag_demo -help
Usage of ./flag_demo:
-age int
年龄 (default 18)
-d duration
时间间隔
-married
婚否
-name string
姓名 (default "张三")
正常使用命令行flag参数:
$ ./flag_demo -name 沙河娜扎 --age 28 -married=false -d=1h30m
沙河娜扎 28 false 1h30m0s
[]
0
4
字符串相关
| 操作函数 | 含义 | 使用方法 |
|---|---|---|
| len() | 求长函数 | len(str) |
| fmt.Sprintf() | 字符串拼接 | fmt.Sprintf("%s-%s", str1, str2) |
| strings.Split() | 分割 | strings.Split(str, 分隔符) |
| strings.Contains() | 包含 | strings.Contains(str, sub1) |
| strings.HasPrefix() | 前缀 | strings.HasPrefix(str, prefix) |
| strings.HasSuffix() | 后缀 | strings.HasSuffix(str, suffix) |
| strings.Index() | 返回子串首次出现位置 | strings.Index(str, sub) |
| strings.LastIndex() | 返回字串最后一次出现位置 | strings.LastIndex(str, sub) |
| strings.Join() | join拼接 | strings.Join(字符串数组, 连接符) |
注意:len函数,输出的是字节数,一个字母是一个字节,一个汉字是三个字节
字符串遍历
- 使用for-range
- 使用 []rune
package main
import "fmt"
func main() {
str:= "hello 世界"
fmt.Println(len(str))
for index, value := range str {
// 中文字符占三个字节,所以带汉字的话一般不使用这个遍历方式
fmt.Printf("%d %c\n",index,value)
}
// 如果字符串中带汉字,一般会转为切片
r:=[]rune(str)
fmt.Printf("%T\n",r) // []int32
for i := 0; i < len(r); i++ {
fmt.Printf("%c",r[i])
}
}
常用方法
package main
import (
"fmt"
"strings"
)
func main() {
s := "1,2,3,5"
sp := strings.Split(s, ",")
fmt.Println(sp)
// 统计一个字符串有几个指定的子串
count := strings.Count("123123454675", "12")
fmt.Println(count)
// 不区分大小写的字符串比较
fmt.Println(strings.EqualFold("go", "Go"))
// 区分的话就用==就可以了
fmt.Println("go" == "Go")
// 第一次出现的索引,没有就返回-1
index := strings.Index("hello", "xcl")
fmt.Println(index)
// 字符串替换
s = strings.ReplaceAll("2022/02/01", "/", "-")
strings.Replace("2022/02/01", "/", "-", -1) // n表示需要替换几个,-1表示所有的
fmt.Println(s)
// 大小写转换
s = strings.ToLower("HH")
fmt.Println(s)
s= strings.ToUpper(s)
fmt.Println(s)
// 去掉左右两边空格
strings.TrimSpace(s)
// 判断字符串是否一抹个字符串开始/结束
strings.HasPrefix(s,"h")
strings.HasSuffix(s,"h")
}
修改字符串
字符串本身是不可变的,但是我们可以使用[]rune或者[]byte切片的方式来达到修改的想换
func main() {
word := "pig"
food := "白萝卜"
// T(表达式) 强制类型转换
byteS1 := []byte(word)
runeS2 := []rune(food)
// 通过修改数组中的单个字符达到效果
byteS1[0] = 'b'
runeS2[0] = '红'
// 重新转换成字符串
word = string(byteS1)
food = string(runeS2)
fmt.Println(word)
fmt.Println(food)
}
日期相关
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Printf("%T, %v\n",now,now) // 返回的是结构体类型 time.Time
fmt.Println(now.Year())
fmt.Println(now.Month())
fmt.Println(int(now.Month()))
fmt.Println(now.Day())
fmt.Println(now.Hour())
fmt.Println(now.Minute())
fmt.Println(now.Second())
fmt.Println("---------------------")
// 日期格式化, 有返回值
s:=fmt.Sprintf("%d-%d-%d",now.Year(),now.Month(),now.Day())
fmt.Println(s)
// 方式二:
s=now.Format("2006-01-02 15:04:05")
fmt.Println(s)
}
内置函数
- len()
- make()
- new()
| 内置函数 | 介绍 |
|---|---|
| close | 主要用来关闭channel |
| len | 求长函数 |
| new | 用来分配内存,主要用来分配值类型,比如int,struct,返回的是指针 |
| make | 用来分配内存,主要用来分配引用类型,比如channel、map、slice |
| append | 用来追加元素到数组、slice中 |
| panic和recover | 用来做错误处理 |
new()
使用new分配内存,其第一个实参为类型,而不是值。返回值为指向该类型新分配的零值的指针
func main() {
// 1. new函数,主要用来给基本数据类型(值类型)做内存分配
n := new(int)
fmt.Printf("n的类型为:%T,n的值为:%v,n的地址为:%v, n指针指向的值为:%v",
n,n,&n,*n)
}
错误处理机制
- java出现异常的时候会报错,Go也会报错,并且后面的内容不执行了,直接结束了
- Go报的是panic
- Go没有try....catch捕获异常/错误的方法,他使用的是:defer+recover机制处理错误
package main
import "fmt"
func main() {
// 异常捕获机制
test()
fmt.Println("下面程序正常运行")
}
func test() {
// 使用defer+recover来捕获异常
defer func() {
err := recover()
if err != nil {
fmt.Println("错误被捕获")
fmt.Println(err)
}
}()
n1 := 10
n2 := 0
res := n1 / n2
fmt.Println(res) // panic: runtime error: integer divide by zero
}
自定义错误
package main
import (
"errors"
"fmt"
)
func main() {
err:=test()
if err != nil {
// 说明有异常
fmt.Println(err)
// 终止程序,也可以用return。区别就是 panic会报错,return不会
panic(err)
}
fmt.Println("下面程序正常运行")
}
func test() (err error){
n1:=10
n2:= 0
if n2 == 0 {
// 抛出自定义错误
return errors.New("自定义错误信息-----除数为0了")
}else {
res := n1/n2
fmt.Println(res)
return nil
}
}
数组
func main() {
var nums [5]int
for i := 0; i < len(nums); i++ {
fmt.Scanf("%d",&nums[i])
}
for _, num := range nums {
fmt.Println(num)
}
}
注意:
- Go语言中相同类型不同长度的数组是不同类型的,也就是说 [5]int,[10]int 是不同的类型
- Go语言中数组长度不可变,必须在声明的时候确定好长度
数组内存分析
数组是值类型,数组初始化之后,会在内存中开辟指定的空间,然后数组会指向第一个下标为0的位置,数组中存储的是下标为0的位置(也就是数组中存的是地址,是哪个地址呢?是下包为0的那个位置的地址)
func f1(nums [5]int) {
nums[0] = 1000
}
func main() {
var nums [5]int
for i := 0; i < len(nums); i++ {
fmt.Scanf("%d",&nums[i])
}
f1(nums) // 由于是值传递,所以这么传入不会改变数组内的值
for _, num := range nums {
fmt.Println(num)
}
}
要想做引用传递,有两种方式:1是变成指针类型、2是用切片
func main() {
var nums [5]int
for i := 0; i < len(nums); i++ {
fmt.Scanf("%d",&nums[i])
}
f1(&nums)
for _, num := range nums {
fmt.Println(num)
}
}
// 指针类型
func f1(nums *[5]int) {
nums[0] = 1000
}
数组初始化
func main(){
// 方式1 不做出初始化默认赋值
var a1 [3]bool //[false false false]
// 方式2
a2 = [3]bool{true, true ,true}
// 方式3:根据初始值自动推断数组的长度是多少
a3 := [...]int{1, 2, 3, 4, 5}
// 方式4:不全部初始
a4 := [5]int{1,2} //[1, 2, 0, 0, 0]
// 方式5:根据索引进行初始化
a5 := [5]string{1: "北京", 2: "上海"}
}
多维数组
// 多维数组的初始化
func main(){
// 多维数组的声明
var a1 int[3][3]
// 简单初始化
a1 = [3][3]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
// 有且只可以省略第一维的初始化
a2 := [...][3]string{
{"A", "B", "C"},
{"D", "E", "F"},
}
}
切片
切片是引用类型
切片的定义
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。
切片的声明
三种方式:
- 先定义一个切片,然后让切片去引用一个已经创建好的数组
- 使用make
-
- 这种方法是底层创建一个数组,对外不可见,使用不可以直接操作这个数组,要通过切片来间接的操作
- 定义一个切片,然后直接就指定具体数组
-
- 原理类似make,但是这种 创建出来的切片,长度、容量相等,不能自定义指定容量
/*
1.切片的判空:
要检查切片是否为空请始终使用len(s) == 0来判断
而不应该使用s == nil来判断
2.切片之间是不能比较的,
我们不能使用 == 操作符来判断两个切片是否含有全部相等元素。
3.切片唯一合法的比较操作是和nil比较。
一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。
但是我们不能说一个长度和容量都是0的切片一定是nil。
*/
func main() {
// 切片建立在数组之上
nums:=[3]int{1,23,41}
slice:=nums[1:3]
fmt.Println(slice)
// 切片的初始化和数组的初始化类型但是不用指定长度
a := []int{1, 2, 3} // [1, 2, 3]
b := []bool{} // []空切片但不等于nil
// 切片的判空
var s1 []int // 长度:0 容量:0 s1==nil
s2 := []int{} // 长度:0 容量:0 s2!=nil
s3 := make([]int, 0, 0) // 长度:0 容量:0 s3!=nil
}
切片内存分析
- 切片是一个引用数据类型,对应着java的list类型。
- 他由三部分组成,是一个结构体。由底层数组指针、切片长度、切片容量组成。
- 底层数组指针直接指向数组的内存位置,所以说他是一个引用类型。
- 通过切片可以改变底层数组的值
切片的长度(len)和容量(cap)
1.切片指向了一个底层的数组
2.切片的长度就是它元素的个数
3.切片的容量是底层数组从切片的第一个元素到最后一个元素的数量
func main() {
// 定义一个底层的数组
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 通过切片表达式创建切片
s1 := a[:4] // [0, 1, 2, 3]
s2 := a[4:9] // [4, 5, 6, 7, 8]
s3 := a[5:] // [5, 6, 7, 8, 9]
s4 := s3[:3] // [5, 6, 7]
fmt.Println(len(s1), cap(s1)) // 长度:4 容量:10
fmt.Println(len(s2), cap(s2)) // 长度:5 容量:6
fmt.Println(len(s3), cap(s3)) // 长度:5 容量:5
fmt.Println(len(s4), cap(s4)) // 长度:3 容量:5
// 当切片中的某一个元素修改后,对应的底层的数组发生改变
s4[0] = 521 //这里s4[0]对应着底层的a[5]所有切片中包含a[5]的全部发生变化
fmt.Println(s1) // [0, 1, 2, 3]
fmt.Println(s2) // [4, 521, 6, 7, 8]
fmt.Println(s3) // [521, 6, 7, 8, 9]
fmt.Println(s4) // [521, 6, 7]
fmt.Println(a) // [0, 1, 2, 3, 4, 521, 6, 7, 8, 9]
}
切片的本质
1.切片就是一个框,框住了一块连续的内存。
2.切片属于引用类型,真正的数据都是保存在底层数组里的
3.切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。
切片s2 := a[3:6],相应示意图如下:
切片的动态增长(append函数)
1.调用append函数必须用原来的切片变量接受返回值
2.使用append函数追加元素,原来的底层数组放不下时候,Go底层就会把底层数组换一个
3.必须接受append返回值
func main() {
a := []int{} // 声明一个初始化的切片 也可以采取make()函数进行初始化
// append可以追加多个元素类似于python中的append和extend结合体
a = append(a, 1, 2, 3) // a = [1, 2, 3]
// 追加切片b...
b := []int{4, 5}
a = append(a, b...) // a = [1, 2, 3, 4, 5]
// 通过var声明的零值切片可以在append()函数直接使用,无需初始化。
var s int[]
s.append(s ,1, 2, 3) // s =[1, 2, 3]
}
底层原理:
- 底层追加元素的时候对数组进行扩容,老数组扩容为新数组
- 创建一个新数组,将老数组里面的值复制到新数组中,然后在新数组中append元素
- 类似java中的ArrayList和StringBuffer、StringBuild等
扩容原理slice.go
// 部分代码
newcap := old.cap
doublecap := newcap + newcap
/* 如果新申请容量(cap)大于2倍的旧容量(old.cap),
最终容量(newcap)就是新申请的容量(cap)。*/
if cap > doublecap {
newcap = cap
/* 如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,
即(newcap=doublecap)*/
} else {
if old.len < 1024 {
newcap = doublecap
} else {
/*
如果旧切片长度大于等于1024
则最终容量(newcap)从旧容量<old.cap>开始循环增加原来的1/4,
直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap) >= (cap)
*/
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
if newcap <= 0 {
newcap = cap
}
}
}
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。
Copy函数
a := []int{1, 2, 3}
b := a
c := make([]int, len(a))
// 由于切片是引用类型,所以a和b共同指向同一块内存地址
b[0] = 520
// 修改b则a也会发生改变
fmt.Println(a) // [520, 2, 3]
fmt.Println(b) // [520, 2, 3]
// 可以使用copy()复制一个新的切片空间
copy(c,a)
删除切片元素
// 切片中没有删除元素的方法可以通过再切片来删除
a := []int{9, 5, 9, 5, 2, 0}
// 删除第2个位置的元素:5
a := append(a[:2], a[3:]...)
fmt.Println(a) // [9, 9, 5, 2, 0]
map
Go语言中提供的映射关系容器为map,其内部使用散列表(hash)实现。
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用
map定义
// 类似于python中的字典类型
map[KeyType]ValueType //KeyType 键的类型 ValueType 值的类型
// map类型的变量默认初始值为nil,需要使用make()函数来分配内存。
make(map[KeyType]ValueType, [cap]) //其中cap表示map的容量,如果不传,默认为16
cap默认大小是16,为了提高效率,Map的容量会是2的幂,且不能使用cap()去获取容量,能使用len()去获取容量。
map的创建
func main() {
// map的创建方式
// 1.
var a map[int]string
a = make(map[int]string,10) // 10表示的是可以存放的数量
a[34]="mhy"
fmt.Println(len(a)) // 不能用cap
fmt.Println(a)
// 2.
b:=make(map[int]string) /// 可以不传,默认为16
b[34]="mhy"
fmt.Println(b)
// 3.
c:= map[int]string{
34:"mhy",
35:"sb",
}
// 同样有key唯一的性质
c[35]="yy"
fmt.Println(c)
// 4.嵌套定义,类似Map<string,Map>
d :=make(map[string]map[int]string)
d["班级"] = make(map[int]string)
d["班级"][34]="张三"
fmt.Println(d)
}
判断某个键是否存在
value, ok := score["zhangsan"]
// key存在ok为true value为对应值 key不存在ok为false value为该类型的零值
删除某个键值对
使用delete函数,传入map和对应key
c:= map[int]string{
34:"mhy",
35:"sb",
}
// 同样有key唯一的性质
c[35]="yy"
delete(c,34)
清空map
- Go中没有专门清空map的操作,只能遍历这个map,然后拿到key,做对应清除
- 或者make一个新的空的,然后替换掉,使老的成为一个垃圾,然后被GC
map的遍历
func main() {
scoreMap := make(map[string]int,3)
scoreMap["张三"] = 90
scoreMap["李四"] = 80
scoreMap["法外狂徒"] = 60
// 通过for range 遍历
for k,v := range scoreMap{
fmt.Println(k, v)
}
// 只遍历key
for k := range scoreMap {
fmt.Println(k)
}
// 可以通过下面方式有序遍历
keys := []string{"法外狂徒", "张三", "李四"}
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
注意: 遍历map时的元素顺序与添加键值对的顺序无关。(与java相同)
面向对象
结构体
结构体定义
使用type和struct关键字来定义结构体,具体代码格式如下:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
type 类型名 struct {
字段名 字段类型
字段名 字段类型
......
}
注意:在结构体定义完成之后,会给每个字段添加上默认零值
结构体初始化方式
type Teacher struct {
Name string
Age int
}
func main() {
// 1.类似java构造器初始化
t:=Teacher{"mhy",21}
fmt.Println(t)
t1:=Teacher{
Name: "mhy",
Age: 21,
}
fmt.Println(t1)
t11:=&Teacher{
"mhy",
211,
}
fmt.Println(t11)
// 2. 公共属性的话,直接利用.复制初始化
var t2 Teacher
t2.Name="mhy"
t2.Age=19
fmt.Println(t2)
// 3.使用指针+new
var t3 *Teacher = new(Teacher)
// 由于返回的是指针,使用需要用*来取值/复制
(*t3).Name="mhy"
(*t3).Age=10
fmt.Println(*t3)
// 但是为了方便和习惯,Go提供了直接赋值的方式,底层其实还是转为了指针
t4:=new(Teacher)
t4.Age=11
t4.Name="mhy"
fmt.Println(*t4)
// 4.相当于new的实例化
t5:=&Teacher{}
t5.Name="mhy"
t5.Age=25
fmt.Println(*t5)
}
注意的是:使用new函数来初始化的时候,返回的是指针,但是为了简化和习惯,提供了直接方法。
构造器/工厂模式
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
// Person结构体
type person struct {
name string
age int
gender string
}
// person的构造方法
func NewPerson(name string, age int, gender string) *person{
return &person{
name: name
age: age
gender: gender
}
}
func main () {
p1 := newPerson("Agoni", "15", "男")
fmt.Println(p1) // &{Agoni 15 男}
fmt.Println(*p1) // {Agoni 15 男}
}
方法
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self、this之类的命名。例如,Person类型的接收者变量应该命名为p,Connector类型的接收者变量应该命名为c等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
// 姓名的set方法
func (p *Person) SetName(newName string) {
p.name = newName
}
// 姓名的get方法
func (p Person) GetName() string {
return p.name
}
func main() {
p1 := NewPerson("Agoni", 15, "男")
p1.SetName("Gala")
fmt.Println(p1.GetName()) // Gala
}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
指针类型的接收者(一般有复制操作的时候需要用指针)
// Set方法
func (p *Person) SetName(newName string) {
p.name = newName
}
值类型的接收者
// Get方法
func (p Person) GetName() string {
return p.name
}
什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
注意:基本数据类型不能定义方法,这也和java有点类似
结构体嵌套
匿名结构体
func main() {
var user struct {
name string
password string
}
user.name = "Agoni"
user.password = "12345678"
fmt.Println(user) // {Agoni 12345678}
fmt.Printf("%T", user) // struct { name string; password string }
}
\
结构体的嵌套(类似java的组合和继承)
package main
import "fmt"
// 文章
type Article struct {
Id int
Content string
Comment // 匿名字段,会默认使用类型名作为字段名
}
// 评论
type Comment struct {
Id int
CommentTime string
}
func main() {
article := Article{
Id: 1,
Content: "test",
Comment: Comment{
Id: 1,
CommentTime: "2020",
},
}
fmt.Println(article)
}
继承
- 其实上面的组合就已经实现了 "继承"。
- 在go中,没有真正意义上的继承,而是通过结构体里面嵌套子结构体的方式,然后就可以调取子结构体里面的方法
- 父结构体可以访问子类的所有字段和方法(包括小写) ,这和java很不一样
- 在嵌套结构体里面,如果存在重名属性/方法,外面对象调用哪个结构体的方法/属性的时候就会直接调用哪个结构体的。不存在重写的概念!!!!
- 如果一个结构体里面嵌入了多个结构体,正好这俩结构体有相同的属性/方法,并且该结构体本身没有这些属性和方法,那么在访问的时候必须指定结构体的名字,否则就会报错。(这是因为在go中嵌套遵循就近原则,但是本身结构体里面没有,但是嵌入的确有相同的,就会不知道找谁)
- 上述说的全是匿名嵌套,如果是嵌入了一个有名的结构体,那么就不是继承了,叫组合!!!!
- 如果是组合的话,访问组合的结构体的字段或者方法的时候,就必须加上结构体的名字
结构体中嵌入匿名结构体是叫继承,嵌入有名结构体叫组合!!!!
多态
go中的多态是由接口实现的
多态数组
接口
- 在Go语言中接口(interface)是一种类型,一种抽象的类型。
- 只要是自定义类型都可以实现接口,不一定只是结构体
- go里面要求接口里面只能有方法,不能有其他东西(属性)
- 一个接口可以继承多个接口,那么要实现这个接口的话,必须实现其所有方法(包括继承的接口的方法)
- interface是一个引用类型,没初始化的话默认为nil
// 为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。 默念三遍
接口是一种类型!!!!
接口是一种类型!!!!
接口是一种类型!!!!
interface是一组method的集合,是duck-type programming的一种体现,类似于是其他语言中的多态
为什么使用接口
1.比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
2.比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
3.比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?
type cat struct{}
type dog struct{}
type tiger struct{}
type sayer interface {
say()
}
// say 猫叫
func (c cat) say() {
fmt.Println("喵喵喵~~~")
}
// say 狗叫
func (d dog) say() {
fmt.Println("旺旺旺~~~")
}
// say 老虎叫
func (t tiger) say() {
fmt.Println("嗷呜嗷呜~~~")
}
// stroke 抚摸函数,根据抚摸的动物不同,来发出对应的叫声
func stroke(animal sayer) {
animal.say()
}
func main() {
c := cat{}
d := dog{}
// 传入猫对象
stroke(c) //喵喵喵~~~
// 传入狗对象
stroke(d) // 旺旺旺~~~
/*
这是我们就实现了通过传递不同类型的对象,实现同一个say方法;
当我在再新自定义类型时,只要实现了say,就实现了接口
*/
t := tiger{}
stroke(t) // 嗷呜嗷呜~~~
}
接口的定义
Go语言提倡面向接口编程。
每个接口由数个方法组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
......
}
其中:
- 接口名:使用
type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
实现接口的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。而不需要像java中写明需要实现哪个接口
注意:
- 可以实现多个接口
- 并且,一个接口的方法,不一定完全需要有一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现(也就是父类和子类加一起实现了这个接口的所有方法,那么也算父类实现了这个接口)
package main
import "fmt"
type animal interface {
say()
move()
}
type aaa interface {
say()
move()
}
type Dog struct {
Name string
}
func (d Dog)say() {
fmt.Println("叫")
}
func (d Dog)move() {
fmt.Println("跑")
}
func main() {
var a animal
var b aaa
d:=Dog{"sb"}
d.say()
d.move()
a=d
a.say()
a.move()
b=d
b.say()
b.move()
}
接口类型变量
接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,animal类型的变量能够存储dog类型的变量。
// 新定义cat对象
type cat struct {
name string
}
// 同样实现say、move方法
// cat实现了say()方法
func (c cat) say() {
fmt.Println("喵喵喵~~~")
}
// cat实现了move()方法
func (c cat) move() {
fmt.Printf("%s会动\n", c.name)
}
// animal类型的变量也能够存储cat的变量
a = c
a.say() // 喵喵喵~~~
a.move() // 球球会动
不同接收者实现接口
type Mover interface {
move()
}
type dog struct {}
值接收者
// 此时实现接口的是dog类型:
func (d dog) move() {
fmt.Println("狗会动")
}
func main() {
var m Mover
wangcai := dog{} // 旺财是dog类型
m = wangcai // m可以接收dog类型
fugui := &dog{} // 富贵是*dog类型
m = fugui // m可以接收*dog类型 m = *fugui
x.move()
}
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui。但是反过来之后就没有了,也就是说没有对值变量求指针类型的语法糖。下面就给了解释和案例。
指针接收者
func (d *dog) move() {
fmt.Println("狗会动")
}
func main() {
var m Mover
var wangcai = dog{} // 旺财是dog类型
m = wangcai // m不可以接收dog类型(编译上就过不去)
var fugui = &dog{} // 富贵是*dog类型
m = fugui // m可以接收*dog类型
}
此时实现Mover接口的是*dog类型,所以不能给m传入dog类型的wangcai,此时m只能存储*dog类型的值。
接口嵌套
接口与接口间可以通过嵌套创造出新的接口。 也就是接口也可以继承接口
// 我们之前的animal接口就可以看成Sayer接口和Mover接口的结合
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
空接口(重点)
空接口的定义
1.空接口是指没有定义任何方法的接口。
2.任何类型都实现了空接口。 也就类似于java中的Object类
3.空接口类型的变量可以存储任意类型的变量。
func main() {
// 定义一个空接口
var x interface{}
fmt.Println("空接口:")
fmt.Printf("%T,%v\n", x, x) // <nil>,<nil>
// 使用x来接受其他类型变量
name := "Agoni"
x = name
fmt.Println("接受name变量后:")
fmt.Printf("%T,%v\n", x, x) // string,Agoni
// 接收age变量
age := 16
x = age
fmt.Println("接受age变量后:")
fmt.Printf("%T,%v\n", x, x) // int,16
// 接受hobby变量
hobby := []string{"basketball", "run"}
x = hobby
fmt.Println("接受age变量后:")
fmt.Printf("%T,%v\n", x, x) // []string,[basketball run]
}
空接口的应用
- 它作为形参的话,实参可以传递任何类型
func f(args interface{}) {
fmt.Println(args)
}
func main() {
f(1)
f(1.3)
map1:=make(map[int]int)
map1[0]=0
f(map1)
}
类型断言
也就是判断这个值是啥类型
空接口可以存储任意类型的值,想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
v, ok := x.(T)
其中:
- x:表示类型为
interface{}的变量 - T:表示断言
x可能是的类型。
func judgmentType(x interface{}) {
// x.(type) 只能再switch语句中使用
switch v := x.(type) {
case string:
fmt.Printf("this value is string:%v\n", v)
case int:
fmt.Printf("this value is int:%v\n", v)
case bool:
fmt.Printf("this value is bool:%v\n", v)
default:
fmt.Println("It's beyond what I can identify")
}
}
func main() {
var x interface{}
s := "myLover"
x = s
// x.(T)判断x的动态类型是否为T类型 返回value,ok true 返回动态值 false 返回判断类型对应的初始值
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("我不是字符串")
}
// judgmentType的使用
food := "rice"
dayHours := 24
love := []string{"吃饭", "睡觉", "打豆豆"}
judgmentType(food) // this value is string:rice
judgmentType(dayHours) // this value is int:24
judgmentType(love) // It's beyond what I can identify
}
结构体与JSON序列化
结构体标签(Tag)
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。
例如不要在key和value之间添加空格。
结构体中的引用类型
type Person struct {
name string
age int8
dream []string // 切片类型
}
// 这种写法会导致如果在外面对传入的newDream进行修改会导致
func (*p Person) setDream(newDream []string) {
p.dream = newDream
}
// 正确写法是开辟一个新的空间对旧的切片进行copy操作
func (*p Person) SetDream(newDream []string) {
p.dream = make([]string, len(newDream))
copy(p.dream, newDream)
}
func main(){
p1 := &Person{
name: "Agnoi",
age: 15,
dream: []string{"吃饭", "睡觉", "打豆豆"},
}
p2 := &Person{
name: "Mark",
age: 20,
dream: []string{"打游戏", "睡觉"},
}
newDream := []string{"打电竞"}
p1.setDream(newDream)
p2.SetDream(newDream)
newDream[0] = "好吃懒做"
fmt.Println(p1.dream) // 好吃懒做 ps:由于共用一块地址,修改newDream导致p1中的dream也发生改变
fmt.Println(p2.dream) // 打电竞
}
\
\
\
反射
- 变量 = type+value。 就是说一个变量由类型和值组成。
- type+value ----> pair
- 反射就是基于pari的。
import (
"fmt"
"reflect"
)
type User struct {
Id int
Name string
Age int8
}
func (u User) GetName() string {
return u.Name
}
// 反射
func main() {
var n = 2.3445646
reflectNum(n)
fmt.Println("------------------")
user := User{
1,
"mhy",
21,
}
f(user)
}
// 基本数据类型
func reflectNum(arg interface{}) {
fmt.Println(reflect.TypeOf(arg))
fmt.Println(reflect.ValueOf(arg))
}
// 复合类型
func f(arg interface{}) {
intputType := reflect.TypeOf(arg)
intputValue := reflect.ValueOf(arg)
// 拿到结构体里面的属性的信息
for i := 0; i < intputType.NumField(); i++ {
field := intputType.Field(i)
value := intputValue.Field(i).Interface()
fmt.Printf("%s, %v = %v\n", field.Name, field.Type, value)
}
for i := 0; i < intputType.NumMethod(); i++ {
method := intputType.Method(i)
fmt.Printf("%s %v\n",method.Name,method.Type)
}
}
结构体标签(tag)
底层利用反射实现的。
package main
import (
"encoding/json"
"fmt"
)
// 一般tag用来做json转换和gorm操作
type User struct {
ID int `json:"id"`
Age int8 `json:"age"`
Name string `json:"name"`
}
func main() {
user := User{
1,
14,
"mhy",
}
jsonStr, err := json.Marshal(user)
if err != nil {
fmt.Println("err")
}
fmt.Printf("%s\n",jsonStr)
var u User
// 带上指针,表示解析到这个变量中
err = json.Unmarshal(jsonStr, &u)
fmt.Printf("%v\n",u)
}
文件操作(os)
首先得打开文件,然后在进行读/写操作
打开文件方式
- os.Open():仅支持于文件的读操作
- os.OpenFile():有多种文件操作的模式可供选择
Open()
os.Open()函数能够打开一个文件,返回一个*File和一个err。对得到的文件实例调用close()方法能够关闭文件
fileObj, err := os.Open(`D:\Code\Go\src\code\os_demo\poem.txt`)
if err != nil {
// 打开文件出现错误退出
fmt.Printf("open file failed, err:%v", err)
return
}
// 关闭文件
defer func(fileObj *os.File) {
// fileOjb.Close返回一个err
_ = fileObj.Close()
}(fileObj)
OpenFile()
os.OpenFile()函数能够以指定模式打开文件,从而实现文件写入相关功能
其中:
name:要打开的文件名 flag:打开文件的模式。 模式有以下几种:
| 模式 | 含义 |
|---|---|
| os.O_WRONLY | 只写 |
| os.O_CREATE | 创建文件 |
| os.O_RDONLY | 只读 |
| os.O_RDW | 读写 |
| os.O_TRUNC | 清空 |
| os.O_APPEND | 追加 |
perm:文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01。
读取文件
fileObj.Read()
Read方法定义如下:
func (f *File) Read(b []byte) (n int, err error)
它接收一个字节切片,返回读取的字节数和可能的具体错误,读到文件末尾时会返回0和io.EOF
func readByFileObj() {
// 使用相对路径时只能使用go build
fileObj, err := os.Open(`D:\Code\Go\src\code\os_demo\poem.txt`)
if err != nil {
// 打开文件出现错误退出
fmt.Printf("open file failed, err:%v", err)
return
}
// 关闭文件
defer func(fileObj *os.File) {
_ = fileObj.Close()
}(fileObj)
var temp = make([]byte, 128)
for {
n, err := fileObj.Read(temp)
if err == io.EOF {
// 把当前读出的字节数打印出来然后退出
fmt.Print(string(temp[:n]))
break
}
if err != nil {
// 读取出现错误退出
fmt.Printf("read from file failed, err:%v", err)
return
}
fmt.Print(string(temp[:n]))
}
fmt.Println()
}
bufio.NewReader()
bufio是在file的基础上封装了一层API,支持更多的功能。
// readBybufio 通过bufio来读取文件数据
func readBybufio() {
fileOjb, err := os.Open(`D:\Code\Go\src\code\os_demo\poem.txt`)
if err != nil {
fmt.Printf("open file failed,err:%v", err)
return
}
defer func(fileOjb *os.File) {
err := fileOjb.Close()
if err != nil {
fmt.Printf("file close failed,err:%v", err)
return
}
}(fileOjb) // 关闭文件
reader := bufio.NewReader(fileOjb)
for {
line, err := reader.ReadString('\n') // 注意是字符不是字符串
if err == io.EOF {
// 当文件读完时退出
fmt.Print(line)
break
}
if err != nil {
fmt.Printf("read by bufio failed,err:%v", err)
return
}
fmt.Print(line)
}
fmt.Println()
}
ioutil.ReadFile()
io/ioutil包的ReadFile方法能够读取完整的文件,只需要将文件名作为参数传入。
// 通过ioutil读取文件
func readByioutil() {
content, err := ioutil.ReadFile(`D:\Code\Go\src\code\os_demo\poem.txt`)
if err != nil {
fmt.Printf("read by ioutil failed,err:%v", err)
return
}
fmt.Println(string(content))
fmt.Println()
}
写入文件
Write和WriteString
// Write和WriteString
func writeByFileObj() {
fileObj, err := os.OpenFile(`D:\Code\Go\src\code\os_demo\temp.txt`, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file failed,err:%v", err)
return
}
// 关闭文件-以后必须顺手写了
defer func(fileObj *os.File) {
_ = fileObj.Close()
}(fileObj)
// Write 通过字节写入文件
words := "This is for you,I think so.\n"
_, err = fileObj.Write([]byte(words))
if err != nil {
fmt.Printf("write by file failed,err:%v", err)
return
}
// WriteString 直接写入字符串
words = "I can't deny that I miss you Nancy\n"
_, err = fileObj.WriteString(words)
if err != nil {
fmt.Printf("write by file failed,err:%v", err)
return
}
}
bufio.NewWriter()
// bufio.NewWriter()
func writeBybufio() {
fileObj, err := os.OpenFile(`D:\Code\Go\src\code\os_demo\temp.txt`, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file failed, err:%v", err)
return
}
//关闭文件
defer func(fileObj *os.File) {
_ = fileObj.Close()
}(fileObj)
writer := bufio.NewWriter(fileObj)
words := "In any case, I will return to my hometown in the end\n"
// 将数据先写入到缓冲区,这样的优势减少于磁盘之间的交互
_, err = writer.WriteString(words)
if err != nil {
fmt.Printf("write by bufio failed,err:%v", err)
return
}
// 将缓冲区中的内容写入到文件中去
err = writer.Flush()
if err != nil {
fmt.Printf("File write failed,err:%v", err)
return
}
}
ioutil.WriteFile()
// ioutil 写入文件
func writeByioutil() {
words := "I love you so much"
err := ioutil.WriteFile(`D:\Code\Go\src\code\os_demo\temp.txt`, []byte(words), 0666)
if err != nil {
fmt.Printf("read by ioutil failed,err:%v", err)
return
}
}
单元测试
testing 为 Go 语言 package 提供自动化测试的支持。通过 go test 命令,能够自动执行如下形式的任何函数:
func TestXxx(*testing.T)
注意:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母。
testing框架内部有main方法,他的逻辑就是 将xxx_test.go的文件引入,然后调用TestXxx的函数。
所以一个test函数才能在没有主函数的情况下运行
- go test -v 测试命令
- 测试文件的文件名必须为 xxx_test.go ,该文件要包TestXxx的函数
- 测试的程序要和主程序放在不同的文件夹
- 需要测试的函数要和 xxx_test.go的文件放在同一个包下
- 一个测试文件中,可以包含多个测试用例函数
- PASS表示测试用例运行成功,FAL表示测试用例运行失败
- 测试单个文件,一定要带上被测试的源文件
-
- go test -v cal_test.go cal.go
- 测试单个方法
-
- go test -v test.run TestAdd
一个例子
文件目录结构
- 主函数
package main
func Add(n int) int {
sum := 0
for i := 0; i < n; i++ {
sum += i
}
return sum
}
func main() {
}
- cal.go (这个名字随便起)
package test01
func Add(n int) int {
sum := 0
for i := 0; i < n; i++ {
sum += i
}
return sum
}
- cal_test.go
package test01
import "testing"
func TestAdd(t *testing.T) {
res:= Add(100)
if res==5050 {
t.Fatalf("执行错误")
}
t.Logf("执行正确")
}
单元测试---规则
- 所有测试文件以——test.go结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到TestMain(m *testing.M) 中
func TestMain(m *testing.M){
// 测试前:数据装载、配合初始化等前置工作
code:=m.Run()
os.Exit(code)
}
- 这里有一点注意的是,测试代码和正式代码可以放在同一个包里,以命名区分就可
单元测试---assert
go get github.com/stretchr/testify/assert (安装开源包)
使用方法和SpringBoot的Test类似。都有一个输出值和一个期望值。
单元测试---覆盖率
go test 测试文件 被测试文件 --cover
单元测试---Mock
使用包 monkey
go get github.com/bock/monkey
基准测试
- go test -bench=
- 和单元测试类似
- 函数命名:BenchmarkXxxx(b *testing.B)
- 基准测试也支持并行
b.RunParaller(func(pb *testing.PB){
for pb.Next(){
}
})
基准测试---优化
Go自带的rand随机函数在并发下性能不好,字节跳动开源了一个fastrand比较强
并发(goroutine)
并发与并行
并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。
并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。
Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
Go语言还提供channel在多个goroutine间进行通信。goroutine和channel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。
goroutineu优势
在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?
Go语言中的goroutine就是这样一种机制(程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行),goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
go协程的特点
- 独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
协程在for循环里的执行规则:
- 如果主线程退出了,协程即使还没有执行完毕,也会退出
- 如果协程执行完了,主线程还没执行完,那么主线程继续执行
- 也就是说主要依据主线程的情况执行
使用goroutine
Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
var wg = sync.WaitGroup{}
func printHello() {
fmt.Println("hello")
wg.Done() // ‘子线程’执行结束,计数减一
}
func main() {
wg.Add(1) // 每开启一个‘子线程’计数器加1(有多少个子线程可以加几)
go printHello()
fmt.Println("every day")
wg.Wait() // 等待所有的‘子线程结束’,‘主线程’再结束(计数为0)
}
MPG模型
- M:操作系统的主线程
- P:协程执行需要的上下文
- G:协程
Channel
问题引出
用协程的方式求1-n中各个数的阶乘,将结果放到map中?
注意:如果整数过大导致int越界,需要用uint64来定义
package main
import (
"fmt"
"time"
)
/*
用协程的方式求1-n中各个数的阶乘,将结果放到map中
*/
// 全局变量
var res = make(map[int]int, 10)
func f(n int) int {
var mul = 1;
for i := 1; i <= n; i++ {
mul *=i;
}
res[n] = mul
return mul
}
func main() {
for i := 1; i < 20; i++ {
go f(i)
}
time.Sleep(time.Second * 3)
for _, v := range res {
fmt.Println(v)
}
}
报错:fatal error: concurrent map writes,出现了并发写
解决办法:
- 加锁
-
- 加一个互斥锁就可以了
- 互斥锁在sync包下
- 但是现在还有一个问题,就是主程序需要等协程运行完之后才能进行,也就是说主线程必须睡一会儿,但是需要等多久呢??? 这个问题没有处理
/*
用协程的方式求1-n中各个数的阶乘,将结果放到map中
*/
var (
res = make(map[int]int, 10)
// 声明一个全局互斥锁
lock sync.Mutex
)
func f(n int) {
var mul = 1
for i := 1; i <= n; i++ {
mul *= i
}
lock.Lock()
res[n] = mul
lock.Unlock()
}
func main() {
for i := 1; i < 20; i++ {
go f(i)
}
time.Sleep(time.Second * 3)
for _, v := range res {
fmt.Println(v)
}
}
- channel 这里使用了WaitGroup和channel
package main
import (
"fmt"
"sync"
)
/*
用协程的方式求1-n中各个数的阶乘,将结果放到map中
*/
var (
res = make(map[int]int)
wg sync.WaitGroup
)
func initC(c chan int) {
defer wg.Done()
for i := 1; i <= 5; i++ {
c <- i
}
close(c)
}
func r(c chan int) {
defer wg.Done()
for {
num, ok := <-c
if !ok {
break
}
var m = 1
for i := 1; i <= num; i++ {
m *= i
}
fmt.Printf("----->%v\n", m)
res[num] = m
}
}
func main() {
c := make(chan int)
wg.Add(2)
go initC(c)
go r(c)
//time.Sleep(3 * time.Second)
wg.Wait()
for i, v := range res {
fmt.Printf("%v , %v\n", i, v)
}
}
- channel本质就是一个队列
- 他本身就是线程安全的,当多个goroutine访问时候,不需要加锁就看
- 管道是有类型的,他可以设置里面能放啥类型。如果放任意类型,就设置为空接口
- 如果设置的是任意类型的话,取的时候用类型断言的方法
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel类型
channel是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量 chan 元素类型
创建channel
通道是引用类型,通道类型的空值是nil。
var ch chan int
fmt.Println(ch) // <nil>
声明的通道后需要使用make函数初始化之后才能使用,创建channel的格式如下:
make(chan 元素类型, [缓冲大小])
channel的缓冲大小是可选的。根据是否有缓冲大小可以分成
- 有缓冲的通道
- 无缓冲的通道
channel操作
- 发送(send) (写)
- 接受(receive) (取/读)
- 关闭(close)
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
channel操作的注意事项
- channel只能存放指定的数据类型
- channel中如果数据满了就不能放了,要不就会报错
- 如果从channel中取出数据之后不是满的状态了,就可以放了
- 在没有使用协程的情况下,如果channel中空了,那么取(读)的时候就会报错
管道的关闭
- 管道使用系统内置函数close()关闭,当channel关闭之后,就不能再向channel中写数据了,但是可以取
关闭后的通道有以下特点(面试常问):
-
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
ps:发送和接收都使用<-符号。
管道的遍历
- 支持for-range的方式,但是需要注意两点:
-
- 在遍历的时候,如果管道没有关闭,就会出现死锁问题
- 在遍历的时候,如果已经关闭,则会正常遍历出来数据,遍历完之后就会退出遍历
协程和管道的小案例
package main
import (
"fmt"
"time"
)
// 利用管道和两个协程实现 一个协程写数据,一个协程读数据,并且要保证在这俩协程在主程序之前结束,而且不会出现并发安全问题
func main() {
c := make(chan int, 10)
// 开另一个管道,表示啥时候结束
quit := make(chan int)
go w(c)
go r(c,quit)
// 以前这里要让主线程睡觉,现在要用类似自旋锁的方式
for {
if <-quit==0 {
break
}
}
fmt.Println("main over。")
}
func r(c ,quit chan int) {
for v := range c {
fmt.Printf("读出的数据为:%v\n",v)
time.Sleep(time.Second)
}
quit<-0
close(quit)
}
func w(c chan int) {
for i := 0; i < 10; i++ {
c<-i
fmt.Printf("放入数据:%d\n",i)
time.Sleep(time.Second)
}
close(c)
}
问题:统计1-2000000的数组中哪些是素数
package main
import "fmt"
// 统计1-8000的数组中哪些是素数
func main() {
c := make(chan int)
res := make(chan int)
exitChan := make(chan int, 4)
go WriteToChan(c)
for i := 0; i < 4; i++ {
go primeNum(c, res, exitChan)
}
go func() {
for {
if len(exitChan) == 4 {
break
}
}
//for i := 0; i < 4; i++ {
// <-exitChan
//}
// 关闭res管道
close(res)
}()
for v := range res {
fmt.Println(v)
}
fmt.Println("结束")
}
func primeNum(c, res, exitChan chan int) {
for {
num, ok := <-c
if !ok {
break
}
var flag = true
for i := 2; i < num; i++ {
if num%i == 0 {
flag = false
break
}
}
if flag {
res <- num
}
}
exitChan <- 0
}
func WriteToChan(c chan int) {
for i := 1; i < 10; i++ {
c <- i
}
close(c)
}
管道的阻塞
如果编译器运行过程中发现一个管道只有写,没有读,那么就会阻塞。
读和写的频率不一致没有关系,因为是异步的
问题
// 现在我们先使用以下语句定义一个通道:
ch := make(chan int)
// 将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
// 从一个通道中接收值。
x := <- ch // 从ch中取出值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
对于上述的案例会引发panic(deadlock错误)
- 有缓冲的通道:可以比喻为快递员把快递放到快递点,收件人再从快递点取出快递,而快递点存放快递的数量是有数量限制的
- 无缓冲的通道:可以比喻为外卖员必须把外卖送到你的手中,而必须存在那个订收外卖的人,外卖员才能去送单
go程通信问题(无缓存)
虽然go程和主函数是以并发执行,但是,下述代码每次执行结果都会相同。
因为当一个go程往管道(c)中写入数据之前,如果有其他go程想获取c中数据,他是获取不到的,需要阻塞等待,直到管道中有数据之后才结束阻塞。
同理,如果管道中的数据一直没有被获取使用,那么该go程也会被阻塞挂起,形成死锁问题
func main() {
c := make(chan int)
go func() {
defer fmt.Println("go程结束")
fmt.Println("go程开始运行。。。。")
c <- 666 // 将666发送给c
}()
num := <-c // 从c中接受数据,并赋值给num
fmt.Println(num)
fmt.Println("main 结束。。。。")
}
go程通信问题(有缓存)
有缓存的话可以存放对应数量的内容,但是如果超过缓存的容量,那超过的会被挂起,等待其他go程释放完之后才能进入。
import (
"fmt"
"time"
)
func main() {
// 有缓存的管道(缓存容量为3)
c := make(chan int, 3)
go func() {
defer fmt.Println("go程执行结束,,。。。")
for i := 0; i < 3; i++ {
c <- i
fmt.Println(i, "进入管道", "此时管道的长度为:", len(c), "容量为:", cap(c))
}
}()
time.Sleep(2*time.Second)
for i := 0; i < 3; i++ {
num := <-c
fmt.Println(num)
}
fmt.Println("main over")
}
Channel与range
for range从通道循环取值
func main() {
c := make(chan int,3)
go func() {
for i := 0; i < 3; i++ {
c<-i
}
// 关闭channel
close(c)
}()
for data := range c {
fmt.Println(data)
}
}
Channel与select
select可以同时监控多个管道。
select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
下面给个小例子
package main
import "fmt"
func fib(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
// 如果c可写,则读case就会进来
x = y
y += x
case <-quit:
fmt.Println("退出")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
fib(c,quit)
go func() {
for i := 0; i < 5; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
}
channel默认是双向的(可读可写)
可以设置为仅读或者仅写
使用select可以解决从管道取数据的阻塞问题
从管道中读取数据的时候必须先close,然后才能读取,否则会有死锁/阻塞问题
因为有时候可能不知道啥会儿close管道,所以可以使用select可以解决死锁/阻塞问题
泛型
在1.18出现
网络编程
TCP服务端和客户端通信
TCP黏包
数据库
Go语言内置的库定义了数据库的规范,没有具体的实现。他的*DB是一个数据库连接池,原生支持池子,是并发安全的。
Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的数据库驱动。使用database/sql包时必须注入(至少)一个数据库驱动。
我们常用的数据库基本上都有完整的第三方实现。例如:MySQL驱动
下载依赖
go get -u github.com/go-sql-driver/mysql -u是下载最新的和间接依赖的