golang小记
println(”%v“)==》%v 表示格式化输出
fmt包printf方法的参数一览
类型 | 参数 | 描述 |
---|---|---|
General | %v | 以默认的方式打印变量的值 |
%T | 打印变量的类型 | |
Integer | %+d | 带符号的整型,fmt.Printf("%+d", 255) 输出+255 |
%q | 打印单引号 | |
%o | 不带零的八进制 | |
%#o | 带零的八进制 | |
%x | 小写的十六进制 | |
%X | 大写的十六进制 | |
%#x | 带0x的十六进制 | |
%U | 打印Unicode字符 | |
%#U | 打印带字符的Unicode | |
%b | 打印整型的二进制 | |
Float | %f (=%.6f ) | 6位小数点 |
%e (=%.6e ) | 6位小数点(科学计数法) | |
%g | 用最少的数字来表示 | |
%.3g | 最多3位数字来表示 | |
%.3f | 最多3位小数来表示 | |
String | %s | 正常输出字符串 |
%q | 字符串带双引号,字符串中的引号带转义符 | |
%#q | 字符串带反引号,如果字符串内有反引号,就用双引号代替 | |
%x | 将字符串转换为小写的16进制格式 | |
%X | 将字符串转换为大写的16进制格式 | |
% x | 带空格的16进制格式 | |
Struct | %v | 正常打印。比如:{sam {12345 67890}} |
%+v | 带字段名称。比如:{name:sam phone:{mobile:12345 office:67890} | |
%#v | 用Go的语法打印。比如main.People{name:”sam”, phone:main.Phone{mobile:”12345”, office:”67890”}} | |
Boolean | %t | 打印true或false |
Pointer | %p | 带0x的指针 |
%#p | 不带0x的指针 |
golang变量:
1.var 定义变量
var 变量名 类型 = 表达式
var a int = 1
2.类型推导方式定义变量 短变量声明法(只能声明局部变量)
变量名 := 表达式
n := 1
注意:
1.在golang中,定义了的变量必须使用
2.定义了的变量初始化后才有值,否则输出位空
3.变量不支持重复声明
4.短变量声明法go会自动推导类型
3.匿名变量
func getName() (string,int) {
return "张三",10
}
var name,age = getName()
fmt.Println(name,age) ==》 "张三" 10
// _下划线匿名变量,如果方法返回两个值,而我们只需要一个值的时候就用下划线占位不需要的值
var _,age = getName()
fmt.Println(age) ==》 10
//匿名变量不占用命名空间,不分配空间所以不存在重复声明
4.局部变量,全局变量以及常量
局部变量 全局变量 常量(const)与JavaScript中的表达意思一致
常量的值不能改变
当const声明多个常量时,如果省略了值则表示和上面一行的值相同
const(
A="A"
B
C
)
fmt.println(A,B,C)==》"A" "A" "A"
在Go语言中声明常量时,可以不需要明确指定数据类型。Go编译器会根据常量的值自动推断其类型。这就是所谓的无类型常量。例如:
const PI = 3.14
在这个例子中,PI是一个无类型的浮点常量,Go编译器会自动将其类型推断为float64。
然而,你也可以在声明常量时指定其类型,这就是所谓的有类型常量。例如:
const PI float32 = 3.14
在这个例子中,PI是一个有类型的浮点常量,其类型被明确指定为float32。
无类型常量在编译时具有更高的精度,而有类型常量的精度则取决于其类型。
5.关于iota
iota是golang语言的常量计数器,只能在常量的表达式中使用
iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)中间插队也没关系,依旧自增计数
在Go语言中,iota
是一个特殊的常量计数器,它的存在主要是为了简化常量的定义和管理。iota
仅在const
常量组中使用,它的值在每个const
常量组内从0开始递增。每当遇到一个新的const
关键字,iota
的计数会重置为0。
iota
的主要用途是:
- 自动生成递增的常量值:当你需要一组递增的常量值时,
iota
可以自动为你生成这些值,而无需手动指定。
const (
a = iota // a = 0
b // b = 1
c // c = 2
)
- 生成位掩码:
iota
可以与按位操作符结合使用,以生成位掩码常量。这对于表示位集合或状态标志等场景非常有用。
const (
flag1 = 1 << iota // flag1 = 1
flag2 // flag2 = 2
flag3 // flag3 = 4
)
2.定义枚举类型:iota
可以用于定义枚举类型,使代码更具可读性。
type Weekday int
const (
Sunday Weekday = iota // Sunday = 0
Monday // Monday = 1
Tuesday // Tuesday = 2
// ...
)
总之,iota
的存在使得在Go语言中定义和管理常量更加方便、简洁和易于维护。
6.一次定义多个变量:
var (
name string = "ssm"
age int = 100
)
var suer,fab,nex string
suer = "111"
fab = "222"
nex = "333"
golang数据类型
1.数据类型分为:基本数据类型和复合数据类型
基本数据类型有:整型,浮点型,布尔型,字符串
符合数据类型有:数组,切片,结构体,函数,map,通道(chanel),接口等。
2.整型
整型分为以下两个大类:
有符号整型按长度分为:int8,int16,init32,int64
对应无符号整型:uint8,uint16,uint32,uint64
3.特殊整型
类型 | 描述 |
---|---|
uint | 32位操作系统上为:uint32,64位操作系统上为uint64 |
int | 32位操作系统上为:int32,64位操作系统上为int64 |
uintptr | 无符号整型,用于存放一个指针 |
4.类型转换
int 类型转换
在golang中不允许不同类型的整型直接运算,需要转换成一致类型才能运算
var a1 int32 = 10
var a2 int64 = 21
fmt.Println(int64(a1)+a2) ==》31
高位向低位转换时需要注意:会溢出
例如:
var n1 int16 = 130
fmt.Println(int8(n1)) ==》 -126 溢出了
5.数字字面量语法
golang1.13版本以后引入了数字字面量语法,这样便于开发者以二进制,八进制或十六进制浮点数的格式定义数字
%d:十进制输出
%b:二进制输出
%o:八进制输出
%x:十六进制输出
c := 128
fmt.Printf("十六进制输出c:%x", c) ==》十六进制输出c:80
fmt.Printf("二进制输出c:%b\n", c) ===》二进制输出c:10000000
6.浮点型
golang支持两种浮点型数:float32,float64
浮点型 | 最大范围 | 常量定义 |
---|---|---|
float32 | 3.4e38 | math.MaxFloat32 |
float64 | 1.8e308 | math.MaxFloat64 |
注意:
1.打印浮点数时,使用fmt包配合动词%f
2.golang中默认float是float64
var float1 float32 = 3.14
fmt.Printf("float1的值:%v,类型为:%T,动词:%f", float1, float1, float1)
===》float1的值:3.14,类型为:float32,动词:3.140000
fmt.Printf("占用字节数:%v", unsafe.Sizeof(float1))
===》占用字节数:4
fmt.Printf("保留一位小数:%.1f\n", float1)
===》保留一位小数:3.1
浮点型科学计数法
var num float32 = 3.14e2 //表示3.14*10的二次方
fmt.Printf("num的值:%f,类型为:%T\n", num, num)
===》num的值:314.000000,类型为:float32
var num float32 = 3.14e-2 //表示3.14/10的二次方
fmt.Printf("num的值:%f,类型为:%T\n", num, num)
===》num的值:0.031400,类型为:float32
浮点数精度丢失问题
导入decimal包解决: go get github.com/shopspring/decimal
var num1 float64 = 1.1
var num2 float64 = 2.2
sum := decimal.NewFromFloat(num1).Add(decimal.NewFromFloat(num2))
fmt.Printf("丢失精度:%v\n", num1+num2) ==》丢失精度:3.3000000000000003
fmt.Printf("使用decimal:%v\n", sum) ==》使用decimal:3.3
7.布尔型
Boolean只有 true false 并且默认值是false
注意:
- golang中不允许将整型强制转换为Boolean类型
- Boolean无法参与数值运算,也无法与其它类型进行转换
8.字符串
golang中的字符串以原生数据类型出现,使用字符串就像使用其它原生数据类型(int,bool,float32,float64等)一样。golang里的字符串的内部实现使用UTF-8编码。字符串的值为双引号中的内容,可以在golang的源码中直接添加非ASCII码字符。
方法 | 介绍 |
---|---|
len(str) | 求长度 |
+或fmt.Sprintf | 拼接字符串 |
strings.Split | 分割,语法strings.Split(字符串,以什么分割) |
strings.contains | 判断是否包含 |
strings.HasPrefix,strings.HasSuffix | 前缀/后缀判断 |
strings.Index(),strings.LastIndex() | 子串出现的位置 |
strings.Join(a[]string,sep string) | join操作 |
注意:
1.len(str)输出中文汉字的话一个汉字是2
2.fmt.Sprintf()使用
str1 := "你好"
str2 := "golang"
str3 := fmt.Sprintf("%v %v",str1,str2) ==》你好 golang
与fmt.printf的区别是,可以返回这个字符串,所以可以用于拼接字符串
3.strings.Split()使用
var str = "c-v-b-n-m-j-k-l-h-g"
var sp = strings.Split(str, "-")
fmt.Println(sp) //==》 [c v b n m j k l h g]
4.strings.Join()使用
var jo = strings.Join(sp, "*")
fmt.Println(jo) //==》 c*v*b*n*m*j*k*l*h*g
var qie = []string{"JavaScript", "Golang", "Fluter"}
var str = strings.Join(qie, "")
fmt.Println(qie, str) ==》[JavaScript Golang Fluter] JavaScriptGolangFluter
5.strings.LastIndex()
这里需要注意一下,这里的从后往前数找到第一个符合的字符串返回他的下标,查不到就返回-1
9.byte和rune类型
组成每个字符串的元素叫做“字符”,可以通过遍历字符串元素获得字符。字符用单引号`包裹
1.关于golang定义字符
var a = 'a'
//此处单引号内只能存在单个字符 并且默认都是int型的 他的值就是对应的ASCII码
fmt.Printf("值为:%v,类型为:%T", a, a)
==》值为:97,类型为:int32
fmt.Printf("原样输出字符:%c,类型为:%T", a, a)
==》原样输出字符:0,类型为:int3
golang字符有以下两种
1.uint8类型,或者叫byte型,代表ASCII码的一个字符。
2.rune类型,代表一个UTF-8字符
当需要处理中文,日文或者其它符合字符时,则需要用到rune类型。rune类型实际是一个int32。
golang使用了特殊工艺的rune类型来处理Unicode,让基于Unicode的文本处理更为方便,也可以使用byte型进行默认字符串处理,性能和拓展性都有照顾。
//定义一个字符串,输出字符串里面的字符
var str = "gorunner"
fmt.Printf("值:%v,原样输出:%c,类型:%T", str[2], str[2], str[2])
===》值:114,原样输出:r,类型:uint8
3.golang中汉字使用的是utf8编码,编码后的值就是int类型,Unicode编码10进制
var str = '国'
fmt.Printf("值:%v,原样输出:%c,类型:%T", str, str, str)
===》值:22269,原样输出:国,类型:int32
//尝试遍历输出 值[原样]
var str = "golang还挺有特色"
for i := 0; i < len(str); i++ { //byte
fmt.Printf(" %v[%c] ", str[i], str[i])
}
//===》103[g] 111[o] 108[l] 97[a] 110[n] 103[g] 232[è] 191[¿] 152[˜] 230[æ] 140[Œ] 186[º] 230[æ] 156[œ] 137[‰] 231[ç] 137[‰] 185[¹] 232[è] 137[‰] 178[²]
for _, r := range str { //rune
fmt.Printf(" %v[%c] ", r, r)
}
//===》103[g] 111[o] 108[l] 97[a] 110[n] 103[g] 36824[还] 25402[挺] 26377[有] 29305[特] 33394[色]
4.修改字符串
**要修改字符串,需要先将其转换成[]rune或[]byte,**完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
func changeString() {
s1 := "big"
//强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1)) //===》pig
s2 := "哈哈哈"
runeS2 := []rune(s2)
runeS2[0] = '蛙'
fmt.Println(string(runeS2)) //===》蛙哈哈
}
10.基本数据类型之间的转换
1.数值类型之间的相互转换 (整型和浮点型)
var a int8 = 20
var b int16 = 40
fmt.Printf("int相加之和:%d\n", int16(a)+b)
===》int相加之和:60
var c float32 = 20
var d float64 = 40
fmt.Printf("float相加之和:%v", float64(c)+d)
===》float相加之和:60
注意:转换时建议从低位转换成高位,高位转成低位的时候有可能会出现溢出的情况,这显然不是我们的本意。
2.其它类型转换成String类型
注意:
1.sprintf有格式化的功能,可以将其它类型转换成string类型,但是使用中需要注意转换的格式
int===》%d
float===》%f
bool===》%t
byte===》%c
var c float32 = 23.33
d := fmt.Sprintf("%f", c)
fmt.Printf("值:%v,类型:%T", d, d)
===》值:23.330000,类型:string
2.通过strconv包来把其它类型转换成string类型
var a int = 12
str1 := strconv.FormatInt(int64(a), 10)
//为什么strconv.FormatInt()传入int64(a)?
//第一个参数:必须是int64类型的数据,
//第二个参数:需要转换的进制。
//第三个参数:保留的小数点 -1(不对小数点格式化)
//第四个参数:格式化的类型 传入 64 32
fmt.Printf("值:%v,类型:%T", str1, str1)
===》值:12,类型:string
golang中的运算符
1.基本运算
注意:
1.在golang中,除法运算有点不同:如果运算的数都是整数,那么除后得出的值会去掉小数部分,保留整数部分。
2.取余 余数=被除数-(被除数/除数)*除数
3.在golang中自增和自减是单独的语句,并不是运算符所以只能单独使用,不能进行赋值操作
没有前自增,只有后自增,并且不能写在变量前面
oi := 1
oi++
fmt.Println(oi)
===》2
2.关系运算
。。。。与或非,逻辑与或非 没啥好记录的
3.赋值运算符
运算符 | 描述 |
---|---|
= | 简单的赋值运算符,将一个表达式的值赋给一个左值 |
+= | 相加后再赋值 |
-= | 相减后再赋值 |
*= | 相乘后再赋值 |
/= | 相除后再赋值 |
%= | 取余后再赋值 |
没啥好说的…
4.逻辑运算符 按位与或非
运算符 | 描述 |
---|---|
& | 两位均为1才为1 |
| | 两位有一个1就为1 |
^ | 相异或 两位不一样则为1 |
<< | 左移n位就是乘以2的n次方 |
>> | 右移n位就是除以2的n次方 |
var a = 5 //101
var b = 2 //010
fmt.Println("a&b", a&b) //000 值0 两位一样就为1
fmt.Println("a|b", a|b) //111 值7 两位有一个1就为1
fmt.Println("a^b", a^b) //111 值7 两位不一样就是1
fmt.Println("a<<b", a<<1) //a*b的b次方
fmt.Println("a>>b", a>>1) //a/b的b次方
golang流程控制
1.for range(键值循环)
golang中可以是使用for range遍历数组,切片,字符串,map及通道(channel)。
通过for range遍历的返回值有以下规律:
1.数组,切片,字符串返回索引和值
2.map返回键和值
3.通道(channel)只返回通道内的值
arr := []int{1, 2, 3, 4, 5}
for k, v := range arr {
fmt.Printf("键=%v,值=%v\n", k, v)
}
键=0,值=1
键=1,值=2
键=2,值=3
键=3,值=4
键=4,值=5
2.switch fallthrough穿透
fallthrough可以穿透到下一层case,无论符合不符合条件
为了满足c语言中的case来设计的
item := 30
switch {
case item < 10:
fmt.Printf("10")
case item > 20 && item <= 30:
fmt.Printf("20\n")
fallthrough
case item > 30:
fmt.Printf("30")
default:
fmt.Printf("SHURUCUOWU")
}
20
30
3.break跳出自定义循环
在for循环嵌套中,有时我们需要跳出整个大循环,而单单使用break无法达到我们的目的,但是golang给我们提供了 break label语法,可以让我们自定义跳出哪一层循环。
lable1:
for i := 0; i < 3; i++ {
for j := 0; j < 10; j++ {
if j == 3 {
break lable1
}
fmt.Printf("i=%v,j=%v\n", i, j)
}
}
}
i=0,j=0
i=0,j=1
i=0,j=2
同样的,continue也可以使用这个语法,但值得注意的是:continue label只能在for循环中使用
4.goto跳转标签
goto语句通过标签进行代码间的无条件跳转。goto可以在快速跳出循环,避免重复退出上有一定的帮助。在golang中goto语句能够简化一些代码的实现过程。
age := 20
if age > 18 {
fmt.Println("成年人")
goto lable1
}
fmt.Println("111")
fmt.Println("222")
lable1:
fmt.Println("333")
fmt.Println("444")
}
成年人
333
444
golang中的数组
1.数组的定义
var 数组变量名 [元素数量]类型
注意:数组长度是类型的一部分
2.数组初始化
1.方法一:直接赋值初始化
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
fmt.Println(arr)
[1 2 3]
2.方法二:声明并赋值
var arr = [3]int{1, 2, 3}
fmt.Println(arr)
[1 2 3]
3.方法三:根据初始值自行推断长度
var arr = [...]int{1, 2, 3}
fmt.Println(arr)
fmt.Println(len(arr))
[1 2 3]
3
4.方法四:根据下标指定val
arr := [...]int{0: 9, 1: 2, 2: 3}
fmt.Println(arr)
fmt.Println(len(arr))
[9 2 3]
3
3.多维数组,值类型,引用类型
这里提出两个概念:引用数据类型和值数据类型
1.值数据类型:a赋值给b 相当于在b的内存空间里储存了a的值,后续改变a的值,b的值不会跟着改变
2.引用数据类型:a赋值给b 相当于将a的地址赋值给b,后续改变a 的值,b的值会随着a的值改变而改变,因为地址是一样的,这就叫引用数据类型。
简单来说就是:值类型就是改变副本的值,不会改变本身的值,引用类型:改变副本的值会改变本身的值
3.定义多维数组
arr := [...][2]string{
{"理", "塘"},
{"王", "丁"},
{"真", "烟"},
}
fmt.Printf(arr[1][1])
丁
//注意:多维数组只有第一个[]可以使用拓展运算符进行自动推导
//for range遍历输出每一个元素
arr := [...][2]string{
{"理", "塘"},
{"王", "丁"},
{"真", "烟"},
}
for _, v1 := range arr {
for _, v2 := range v1 {
fmt.Println(v2)
}
}
}
==》
理
塘
王
丁
真
烟
golang中的切片
1.切片
切片(Slice)是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址,长度和容量
声明切片类型的基本语法如下:
var name []T
//基于数组定义切片,如果要获取数组的所有元素可以直接写[:]
a := [5]int{1, 2, 3, 4, 5}
b := a[:]
fmt.Printf("值:%v,类型:%T", b, b)
===》值:[1 2 3 4 5],类型:[]int
//关于切片的截取
a := [5]int{1, 2, 3, 4, 5}
b := a[:]
fmt.Printf("值:%v,类型:%T\n", b, b)
c := a[2:]
fmt.Printf("值:%v,类型:%T\n", c, c)
d := a[1:3]
fmt.Printf("值:%v,类型:%T\n", d, d)
===》
值:[1 2 3 4 5],类型:[]int
值:[3 4 5],类型:[]int
值:[2 3],类型:[]int
//切片再切片
a := []int{1, 2, 3, 4, 5}
b := a[:]
fmt.Printf("值:%v,类型:%T\n", b, b)
c := a[2:]
fmt.Printf("值:%v,类型:%T\n", c, c)
d := a[1:3]
fmt.Printf("值:%v,类型:%T\n", d, d)
===》
值:[1 2 3 4 5],类型:[]int
值:[3 4 5],类型:[]int
值:[2 3],类型:[]int
2.关于nil
在golang中,当你声明了一个变量,但还没有赋值时,golang会自动给你的变量赋值一个默认零值。这是每种类型对应的零值
类型 | 对应零值 |
---|---|
Boolean | false |
numbers | 0 |
string | “” |
pointers | nil |
slices | nil |
maps | nil |
channels | nil |
functions | nil |
interfaces | nil |
3.关于切片的长度和容量
切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置cap()函数求切片的容量
切片的长度就是它所包含的元素个数
切片的容量时候从它的第一个函数开始数,到其底层元素末尾的个数
1.切片s的长度和容量可通过表达式len(s)和cap(s)来获取
a := []int{1, 2, 3, 4, 5}
fmt.Printf("长度:%v,容量:%v\n", len(a), cap(a))
===》
长度:5,容量:5
2.使用make函数创建包含容量及长度的切片
sliceA := make([]int, 3, 5)
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
===》内容:[0 0 0],长度:3,容量:5
make()函数的用法:
变量名 := make([]T,长度,容量)
//修改切片的数值可直接通过切片名[下标] = 赋值
sliceA[0] = 10
sliceA[1] = 20
3.切片扩容 append()
在golang中无法通过下标的方式给切片扩容,但是golang提供了append()方法
我们可以先试试定义一个没有内容的slice切片,试着通过下标给它扩容
var sliceA []int
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
sliceA[0] = 1
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
===》内容:[],长度:0,容量:0
panic: runtime error: index out of range [0] with length 0
//我们可以看到报panic了,它告诉我们这个切片的长度为0,不能通过0下标给这个切片赋值
接下来我们使用append()方法给切片扩容
sliceA := []int{}
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
sliceA = append(sliceA, 1)
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
===》
内容:[],长度:0,容量:0
内容:[1],长度:1,容量:1
//append()方法的使用
append(切片名,数值,数值....) //理论上可以同时添加n个参数 每增加一个长度就+1
append()方法还可以合并切片
sliceA := []int{1, 2, 3, 4, 5}
sliceB := []int{6, 7, 8, 9, 0}
sliceA = append(sliceA, sliceB...)
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
===》
内容:[1 2 3 4 5 6 7 8 9 0],长度:10,容量:10
切片扩容的策略
sliceA := []int{}
for i := 0; i < 10; i++ {
sliceA = append(sliceA, i)
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
}
===》
内容:[0],长度:1,容量:1
内容:[0 1],长度:2,容量:2
内容:[0 1 2],长度:3,容量:4
内容:[0 1 2 3],长度:4,容量:4
内容:[0 1 2 3 4],长度:5,容量:8
内容:[0 1 2 3 4 5],长度:6,容量:8
内容:[0 1 2 3 4 5 6],长度:7,容量:8
内容:[0 1 2 3 4 5 6 7],长度:8,容量:8
内容:[0 1 2 3 4 5 6 7 8],长度:9,容量:16
内容:[0 1 2 3 4 5 6 7 8 9],长度:10,容量:16
//到这里我在思考一个问题:为什么切片的容量不是和长度一样依次+1呢?
//询问gpt得知:这是由于Go语言的切片(slice)内部实现机制决定的。Go语言的切片在进行append操作时,如果当前的容量无法满足新的元素,Go会自动为切片扩容。扩容的策略是:如果当前切片的长度小于1024,则扩容为原来的2倍;如果当前切片的长度大于或等于1024,则扩容为原来的1.25倍。
//在你的代码中,当长度从2增加到3时,原有容量2无法满足,所以容量变为原来的2倍,即4。当长度从4增加到5时,原有容量4无法满足,所以容量变为原来的2倍,即8。当长度从8增加到9时,原有容量8无法满足,所以容量变为原来的2倍,即16。这就是为什么容量不是和长度一样依次+1的原因。
4.使用copy()方法复制切片
在开发中我们可能会遇到一个问题,在引用数据类型中,我们把a赋值给b,当我们改变a值的时候,b值也会发生改变,改变b值,a值也改变,所以golang提供了copy()方法来复制切片,这有点类似与JavaScript es6的解构赋值。
sliceA := []int{1, 2, 3, 4, 5}
sliceB := make([]int, 5, 5)
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceB, len(sliceB), cap(sliceB))
copy(sliceB, sliceA)
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceB, len(sliceB), cap(sliceB))
sliceB[0] = 9527
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceB, len(sliceB), cap(sliceB))
===》
内容:[1 2 3 4 5],长度:5,容量:5
内容:[0 0 0 0 0],长度:5,容量:5
内容:[1 2 3 4 5],长度:5,容量:5
内容:[1 2 3 4 5],长度:5,容量:5
内容:[1 2 3 4 5],长度:5,容量:5
内容:[9527 2 3 4 5],长度:5,容量:5
5.删除切片中的元素
golang中并没有删除切片元素的专用方法,但我们可以使用切片本身的特性来删除元素
这里我想吐槽一下,JavaScript有length delete slice pop,为啥golang不搞一个呢?
sliceA := []int{1, 2, 3, 4, 5}
//如果我们要删除切片中的3,那我们可以使用append方法
sliceA = append(sliceA[:2], sliceA[3:]...)
fmt.Printf("内容:%v,长度:%v,容量:%v\n", sliceA, len(sliceA), cap(sliceA))
===》
内容:[1 2 4 5],长度:4,容量:5
golang内置Sort包对切片进行排序
1.升序
对于int,float64和string数组或是切片的排序,go分别提供了sort.Ints(),sort.Float64s()和sort.String()函数,默认都是从小到大排序。
sliceA := []int{1, 12, 31, 14, 51, 99}
sort.Ints(sliceA)
fmt.Println(sliceA)
===》[1 12 14 31 51 99]
2.降序
降序在语法上有些不同,首先需要转换一下排序方式
sliceA := []int{1, 12, 31, 14, 51, 99}
sort.Sort(sort.Reverse(sort.IntSlice(sliceA)))
fmt.Println(sliceA)
===》[99 51 31 14 12 1]
golang中map类型相关
map是一种无序的基于key-value的数据解构,go语言中,map是引用类型,必须初始化才能使用。
go语言中map的定义语法如下:
map[KeyType]ValueType
其中:
keytype表示键的类型
valuetype表示键对应的值的类型
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make:用于slice,map,和channel的初始化
make(map[KeyType]ValueType.[cap])
其中cap表示map的容量,该参数虽然不是必须的。
userinfo := make(map[string]string)
userinfo["name"] = "张三"
userinfo["age"] = "18"
userinfo["sex"] = "男"
fmt.Println(userinfo)
===》map[age:18 name:张三 sex:男]
map也支持在声明时填充元素
userinfo := map[string]string{
"name": "张三",
"age": "18",
"sex": "男",
}
fmt.Printf("%v\n", userinfo)
===》map[age:18 name:张三 sex:男]
遍历map
userinfo := map[string]string{
"name": "张三",
"age": "18",
"sex": "男",
}
for k, v := range userinfo {
fmt.Printf("key:%v,value:%v\n", k, v)
}
===》
key:name,value:张三
key:age,value:18
key:sex,value:男
map类型的crud
1.map的创建与修改
userinfo := map[string]string{
"name": "张三",
"age": "18",
"sex": "男",
}
for k, v := range userinfo {
if k == "age" && v == "18" {
userinfo["age"] = "20"
}
}
fmt.Println(userinfo)
===》
map[age:20 name:张三 sex:男]
2.map的查询
userinfo := map[string]string{
"name": "张三",
"age": "18",
"sex": "男",
}
v, ok := userinfo["age"]
fmt.Println(v, ok)
v, ok = userinfo["height"]
fmt.Println(v, ok)
===》
18 true
false
3.map删除键值对
使用delete()内建函数从map中删除一组键值对。delete()函数的格式如下:
delete(map 对象,key)
userinfo := map[string]string{
"name": "张三",
"age": "18",
"sex": "男",
}
fmt.Println(userinfo)
delete(userinfo, "age")
fmt.Println(userinfo)
===》
map[age:18 name:张三 sex:男]
map[name:张三 sex:男]
4.元素为map类型的切片
类似于JavaScript的[{}]形式
userinfo := make([]map[string]string, 2)
fmt.Println(userinfo)
fmt.Println(userinfo[0] == nil)
if userinfo[0] == nil {
userinfo[0] = map[string]string{
"name": "张三",
"age": "18",
"sex": "男",
}
}
if userinfo[1] == nil {
userinfo[1] = map[string]string{
"name": "李四",
"age": "19",
"sex": "女",
}
}
fmt.Println(userinfo)
===》
[map[] map[]]
true
[map[age:18 name:张三 sex:男] map[age:19 name:李四 sex:女]]
map的值可以为切片
userinfo := make(map[string][]string)
fmt.Println(userinfo)
userinfo["hobby"] = []string{
"吃饭",
"睡觉",
"打豆豆",
}
fmt.Println(userinfo)
===》
map[]
map[hobby:[吃饭 睡觉 打豆豆]]
map循环根据键值大小排序并依次输出
map1 := make(map[int]int, 10)
map1[0] = 21
map1[98] = 11
map1[23] = 1
map1[7] = 82
map1[54] = 917
map1[10] = 12
fmt.Println(map1)
var keyslice []int
for k, _ := range map1 {
keyslice = append(keyslice, k)
}
sort.Ints(keyslice)
fmt.Println(keyslice)
for _, v := range keyslice {
fmt.Printf("k:%v==v:%v\n", v, map1[v])
}
===》
map[0:21 7:82 10:12 23:1 54:917 98:11]
[0 7 10 23 54 98]
k:0==v:21
k:7==v:82
k:10==v:12
k:23==v:1
k:54==v:917
k:98==v:11
函数相关
1.定义函数
在golang中你可以这样定义函数
func 函数名 (接收的参数) (返回值类型){
函数体
}
函数的可变参数,可变参数时指函数的参数数量不固定,在golang中可变参数通过在参数名后加…表示
func sum(x ...int) int {
var numsum int = 0
for _, v := range x {
numsum += v
}
return numsum
}
umscore := sum(1, 2, 3, 4, 5, 6, 7, 8, 9)
fmt.Println(umscore)
===》
45
2.函数变量作用域
全局变量:定义在函数外部的变量,他在程序整个运行周期内都有效
局部变量:定义在函数内部的变量,无法在该函数外部使用
3.函数类型与变量
在golang中可以使用type关键字定义一个函数的类型
type可以自定义类型,也可以取别名区别在于:
type myInt int //自定义类型
type myFloat = float64 //起别名
type 类型名 func(int,int)int
type mymaths func(x, y int) int
//定义一个函数类型,接收两个int参数,返回一个int
func calc(x, y int, fn mymaths) int {
//定义一个函数,接收两个int参数一个mymaths方法,返回一个int
return fn(x, y)
}
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
sum := calc(1, 2, add)
fmt.Println(sum)
===》3
函数作为参数:
func calc(x, y int, fn mymaths) int {
return fn(x, y)
}
sum := calc(998, 998, func(x, y int) int {
return x * y
})
fmt.Println(sum)
==》996004
函数作为返回值:
函数内部定义函数只能定义匿名函数,匿名函数因为没有函数名,所以没有办法像普通函数一样调用,所以匿名函数需要保存在某个变量里面去或者作为立即执行函数来使用。
package main
import "fmt"
func main() {
var a = do("*")
fmt.Println(a(988, 2888))
}
type calc func(int, int) int
func do(o string) calc {
switch o {
case "+":
return add
case "-":
return sub
case "*":
return func(x, y int) int {
return x * y
}
default:
return nil
}
}
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
===》2853344
4.关于闭包
全局变量的特点:
1.常驻内存
2.污染全局
局部变量的特点:
1.不常驻内存
2.不污染全局
闭包:
1.可以让一个变量常驻内存
2.可以让一个变量不污染全局
3.闭包是指有权访问另一个函数作用域中的变量的函数
4.创建闭包的常见方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的变量
5.由于闭包里作用域返回的局部变量资源不会立刻销毁回收,所以可能会占用更多的内存
6.写法:函数嵌套一个函数,最后返回里面的函数
func adder() func(y int) int {
var i = 10
return func(y int) int {
i += y
return i
}
}
func main() {
fn := adder()
fmt.Printf("值:%v", fn(1))
fmt.Printf("值:%v", fn(1))
fmt.Printf("值:%v", fn(1))
}
===》值:11值:12值:13
5.defer语句
golang中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。
func f1() int {
a := 0
defer func() {
a++
}()
return a
}
func f2() (a int) {
defer func() {
a++
}()
return a
}
fmt.Println(f1())
fmt.Println(f2())
===》0 1
为什么f1输出0,f2输出1呢?
//这个问题涉及到Go语言的defer和return的执行顺序以及变量作用域的问题。
//在f1函数中,我们首先定义了一个局部变量a,并赋值为0。然后,我们使用defer关键字定义了一个匿名函数,这个匿名函数会在f1函数返回之后执行。在匿名函数中,我们将a的值加1。然后,我们返回a的值。因为在返回a的值时,defer定义的匿名函数还没有执行,所以返回的是a当前的值,即0。
//在f2函数中,我们使用了命名返回值a。在函数返回时,Go语言会先计算返回值,然后执行defer定义的匿名函数,最后再返回计算的结果。因此,在f2函数中,匿名函数将返回值a加1后,再返回,所以返回的结果是1。
//总的来说,这个问题的关键在于Go语言的defer机制和命名返回值的特性。
//命名返回值在defer之后返回
defer注册要延迟执行的函数时该函数所有的参数都需要确定其值
6.内置函数panic/recover
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回值为指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
注意:golang目前没有异常机制,但是使用panic/recover模式来处理错误。panic可以在任何地方引发,但recover只有在defer调用的函数中有效。
func loadFile(fileName string) error {
if fileName == "main.go" {
return nil
} else {
return errors.New("文件不存在")
}
}
func doFile(name string) {
defer func() {
err := recover()
if err != nil {
fmt.Println("正在给管理员发送日志")
}
}()
err := loadFile(name)
if err != nil {
panic(err)
}
}
doFile("xxx.go")
fmt.Println("继续执行...")
===》
正在给管理员发送日志
继续执行...
7.time包以及日期相关函数
1.获取当前日期
times := time.Now()
year := times.Year()
month := times.Month()
day := times.Day()
hour := times.Hour()
minute := times.Minute()
second := times.Second()
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second)
===》
2023-10-24 16:35:30
//%02d 表示2为宽度,如果不足2列就补上0
2.使用golang自带的格式化方法格式化日期
times := time.Now().Format("2006年01月02日 15时04分05秒")
fmt.Println(times)
===》
2023年10月24日 17时02分27秒
3.获取当前时间戳
times := time.Now().Unix()
fmt.Println(times)
===》
1698138019
4.时间戳转换日期字符串
times := time.Unix(1596958000, 0).Format("2006-01-02 15:04:05")
fmt.Println(times)
===》2020-08-09 15:26:40
注意:unix方法转字符串接收两个参数:参数1:毫秒,参数2:纳秒
5.日期字符串转时间戳
str := "2023年10月24日 17时02分27秒"
temp := "2006年01月02日 15时04分05秒"
timer, _ := time.ParseInLocation(temp, str, time.Local)
fmt.Println(timer.Unix())
===》1698138147
6.time包中定义的时间间隔类型
在Go语言的"time"包中,定义了以下常用的时间间隔:
time.Nanosecond
:纳秒time.Microsecond
:微秒time.Millisecond
:毫秒time.Second
:秒time.Minute
:分钟time.Hour
:小时
这些时间间隔都是time.Duration
类型的,你可以使用它们来进行时间相关的计算。例如,如果你想要表示3分钟,你可以写3 * time.Minute
。
unit := 3 * time.Millisecond
fmt.Println(unit)
===》3ms
8.定时器
1.使用time.NewTicker(时间间隔)来设置定时器
times := time.NewTicker(time.Second)
n := 5
for t := range times.C {
n--
fmt.Println(t)
if n == 0 {
times.Stop()
break
}
}
===》
2023-10-24 17:38:11.7573229 +0800 CST m=+1.010390601
2023-10-24 17:38:12.7618238 +0800 CST m=+2.014891501
2023-10-24 17:38:13.7595086 +0800 CST m=+3.012576301
2023-10-24 17:38:14.7600082 +0800 CST m=+4.013075901
2023-10-24 17:38:15.7553253 +0800 CST m=+5.008393001
2.使用time.Sleep(时间间隔类型)休眠
second := 0
for {
time.Sleep(time.Second)
second++
fmt.Println(second)
if second == 10 {
fmt.Println("10秒到了")
break
}
}
===》
1
2
3
4
5
6
7
8
9
10
10秒到了
golang中的指针
1.关于指针
指针也是一个变量,但它是一种特殊的变量,它储存的数据不是一个普通的值,而是另一个变量的内存地址。
2.指针地址和指针类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置,golang中使用&字符放在变量前面对变量进行取地址操作。
golang中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等…
a := 10
p := &a //&a表示取a的地址 类型为:*int(指针类型)
fmt.Printf("值:%v,类型:%T,内存地址:%p\n", a, a, &a)
fmt.Printf("值:%v,类型:%T,内存地址:%p\n", p, p, &p)
fmt.Printf("取出指针变量的值:%v", *p)
===》
值:10,类型:int,内存地址:0xc00000a0c8
值:0xc00000a0c8,类型:*int,内存地址:0xc000052020
取出指针变量的值:10
也可以通过内存地址修改变量的值:
a := 10
p := &a
fmt.Printf("修改前:值:%v,类型:%T,内存地址:%p\n", a, a, &a)
*p = 20
fmt.Printf("修改后:值:%v,类型:%T,内存地址:%p\n", a, a, &a)
===》
修改前:值:10,类型:int,内存地址:0xc000096068
修改后:值:20,类型:int,内存地址:0xc000096068
3.new函数分配内存
在golang中引用数据类型必须分配能存后才能使用
new是一个内置的函数,它的函数签名如下:
func new(Type)*Type
其中:
Type
表示类型,new函数只接收一个参数,这个参数是一个类型
*Type
表示类型指针,new函数返回一个指向该类型内存地址的指针,是一个指针类型
a := new(int)
b := new(bool)
fmt.Printf("a的类型:%T,内存地址:%p\n", a, &a)
fmt.Printf("b的类型:%T,内存地址:%p\n", b, &b)
===》
a的类型:*int,内存地址:0xc0000a8018
b的类型:*bool,内存地址:0xc0000a8020
a := new(int)
b := new(bool)
fmt.Printf("a的类型:%T,值:%v,指针变量对应的值:%v\n", a, a, *a)
fmt.Printf("b的类型:%T,值:%v,指针变量对应的值:%v\n", b, b, *b)
===》
a的类型:*int,值:0xc00000a0c8,指针变量对应的值:0
b的类型:*bool,值:0xc00000a0e0,指针变量对应的值:false
4.make函数分配内存
make也可用于分配内存,区别于new,make只适用于slice,map,channel的内存创建,并且返回的类型都是这三个类型的本身,而像new返回他们的指针类型。因为这三个类型都是引用类型,所以没有必要返回他们的指针
golang中的结构体
1.关于结构体
golang中没有类的概念,解构体与其它语言的类相似,和其它面向对象语言中的类相比:结构体具有更高的拓展性和灵活性。
基本数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再使用单一的基本数据类型就无法满足了,golang提供了一种自定义数据类型,可以封装多个基本数据类型。这就是结构体,英文名struct
2.结构体的定义
使用type和struct关键字来定义结构体
type 类型名 struct {
字段名 字段类型
字段名 字段类型
...
}
类型名:表示自定义结构体的名称,在同一个包内不能重复
字段名:表示结构体字段名。结构体的字段名必须唯一
字段类型:表示结构体字段的具体类型
3.结构体实例化
只有当结构体实例化时,才会真正地分配内存,才能使用结构体字段
var 结构体实例 结构体类型
//结构体首字母大写表示公有结构体,小写则为私有结构体
//第一种方法:直接定义实例化结构体
type Person struct {
name string
age int
sex string
}
var p Person
p.name = "张三"
p.age = 18
p.sex = "男"
fmt.Printf("值:%#v,类型:%T\n", p, p)
===》
值:main.Person{name:"张三", age:18, sex:"男"},类型:main.Person
//第二种方法:new方法实例化结构体
p1 := new(Person)
p1.Name = "李四"
p1.Age = 19
p1.Sex = "女"
fmt.Printf("值:%#v,类型:%T\n", p1, p1)
===》
值:&main.Person{Name:"李四", Age:19, Sex:"女"},类型:*main.Person
注意:*在golang中支持对结构体指针直接使用.来访问结构体成员。p1.name=“李四”其实在底层是(p2).name = “李四”
//第三种方法:指针赋值实例化结构体
p2 := &Person{}
p2.Name = "王五"
p2.Age = 20
p2.Sex = "男"
fmt.Printf("值:%#v,类型:%T\n", p2, p2)
===》
值:&main.Person{Name:"王五", Age:20, Sex:"男"},类型:*main.Person
//第四种方法:键值对实例化结构体
p3 := Person{
Name: "赵六",
Age: 21,
Sex: "女",
}
fmt.Printf("值:%#v,类型:%T\n", p3, p3)
===》
值:main.Person{Name:"赵六", Age:21, Sex:"女"},类型:main.Person
//第五种方法:简写键实例化结构体
p4 := &Person{
"钱七",
22,
"男",
}
fmt.Printf("值:%#v,类型:%T\n", p4, p4)
===》
值:&main.Person{Name:"钱七", Age:22, Sex:"男"},类型:*main.Person
4.结构体方法和接收者
golang中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接收者得函数。接收者的概念就类似于其它语言中的this或者self。
func (接收者变量 接收者类型) 方法名(参数列表)(返回参数){
函数体
}
//获取 get的接收者类型可以是变量也可以指针变量
type Person struct {
Name string
Age int
Sex string
}
func (p Person) Getinfo() {
fmt.Printf("姓名:%v,年龄:%v,性别:%v\n", p.Name, p.Age, p.Sex)
}
p4 := &Person{
"钱七",
22,
"男",
}
p4.Getinfo()
===》姓名:钱七,年龄:22,性别:男
//修改 update的接收者类型必须是指针变量
type Person struct {
Name string
Age int
Sex string
}
func (p Person) Getinfo() {
fmt.Printf("姓名:%v,年龄:%v,性别:%v\n", p.Name, p.Age, p.Sex)
}
func (p *Person) Setinfo(name string, age int, sex string) {
p.Name = name
p.Age = age
p.Sex = sex
}
p4 := &Person{
"钱七",
22,
"男",
}
p4.Getinfo()
p4.Setinfo("张三", 18, "男")
p4.Getinfo()
===》
姓名:钱七,年龄:22,性别:男
姓名:张三,年龄:18,性别:男
golang中结构体实例是独立的不会相互影响
func (p Person) Getinfo() {
fmt.Printf("姓名:%v,年龄:%v,性别:%v\n", p.Name, p.Age, p.Sex)
}
func (p *Person) Setinfo(name string, age int, sex string) {
p.Name = name
p.Age = age
p.Sex = sex
}
p4 := &Person{
"钱七",
22,
"男",
}
p5 := &Person{
"孙八",
23,
"女",
}
p4.Setinfo("张三", 18, "男")
p4.Getinfo()
p5.Getinfo()
p5.Setinfo("李四", 19, "女")
p4.Getinfo()
p5.Getinfo()
===》
姓名:张三,年龄:18,性别:男
姓名:孙八,年龄:23,性别:女
姓名:张三,年龄:18,性别:男
姓名:李四,年龄:19,性别:女
同样的,自定义类型也可以定义方法,步骤和结构体定义方法类似
func (m Myint) Getinfo() {
fmt.Println("我是自定义类型方法")
}
type Myint int
var a Myint = 10
a.Getinfo()
===》
我是自定义类型方法
5.结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段成为匿名字段
type Person struct {
string
int
}
6.结构体嵌套
type Person struct {
Name string
Age int
Hobby []string
map1 map[string]string
}
var p Person
p.Name = "张三"
p.Age = 18
p.Hobby = make([]string, 3, 5)
p.Hobby[0] = "篮球"
p.Hobby[1] = "足球"
p.Hobby[2] = "游泳"
p.map1 = make(map[string]string)
p.map1["phone"] = "909090"
p.map1["address"] = "北京"
fmt.Printf("值:%#v,类型:%T\n", p, p)
===》
值:main.Person{Name:"张三", Age:18, Hobby:[]string{"篮球", "足球", "游泳"}, map1:map[string]string{"address":"北京", "phone":"909090"}},类型:main.Person
type Use struct {
Name string
Password string
Address
}
type Address struct {
Name string
Phone string
City string
}
var user Use
user.Name = "张三"
user.Password = "123456"
user.Address.City = "理塘"
user.Address.Name = "最高峰"
user.Address.Phone = "123456789"
fmt.Printf("值:%#v,类型:%T\n", user, user)
user.City = "上海" //如果第一层结构体没找到会去嵌套结构体里面找
fmt.Printf("值:%#v,类型:%T\n", user, user)
===》
值:main.Use{Name:"张三", Password:"123456", Address:main.Address{Name:"最高峰", Phone:"123456789", City:"理塘"}},类型:main.Use
值:main.Use{Name:"张三", Password:"123456", Address:main.Address{Name:"最高峰", Phone:"123456789", City:"上海"}},类型:main.Use
7.结构体的继承
type Person struct {
Name string
Age int
}
type Student struct {
Hobbys string
Person
}
func (a Person) GetPersonInfo() {
fmt.Printf("%v在吃饭", a.Name)
}
func (s Student) GetStudentInfo() {
fmt.Printf("%v在狗叫", s.Name)
}
var s1 = Student{
Hobbys: "篮球",
Person: Person{
Name: "小二",
},
}
s1.GetStudentInfo()
s1.GetPersonInfo()
===》
小二在狗叫小二在吃饭
Golang结构体和JSON相互转换序列化反序列化
1.结构体与JSON序列化
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string
age int //私有属性无法被json包读取
Gender string
}
func main() {
s1 := Student{
Name: "bob",
age: 20,
Gender: "male",
}
fmt.Printf("%#v,type=%T\n", s1, s1)
jsonbyt, _ := json.Marshal(s1)
jsonstr := string(jsonbyt)
fmt.Println(jsonstr)
str1 := `{"Name":"bob","age":20,"Gender":"male"}`
var s2 Student
json.Unmarshal([]byte(str1), &s2)
fmt.Printf("%#v,type=%T\n", s2, s2)
}
===》
main.Student{Name:"bob", age:20, Gender:"male"},type=main.Student
{"Name":"bob","Gender":"male"}
main.Student{Name:"bob", age:0, Gender:"male"},type=main.Student
2.结构体标签Tag
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `json:"姓名"`
age int `json:"年龄"` //私有属性无法被json包读取
Gender string `json:"性别"`
}
func main() {
s1 := Student{
Name: "bob",
age: 20,
Gender: "male",
}
fmt.Printf("%#v,type=%T\n", s1, s1)
jsonbyt, _ := json.Marshal(s1)
jsonstr := string(jsonbyt)
fmt.Println(jsonstr)
str1 := `{"姓名":"bob","性别":"male"}`
var s2 Student
json.Unmarshal([]byte(str1), &s2)
fmt.Printf("%#v,type=%T\n", s2, s2)
}
===》
main.Student{Name:"bob", age:20, Gender:"male"},type=main.Student
{"姓名":"bob","性别":"male"}
main.Student{Name:"bob", age:0, Gender:"male"},type=main.Student
3.嵌套结构体JSON序列化与反序列化
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `json:"姓名"`
Age int `json:"年龄"` //私有属性无法被json包读取
Gender string `json:"性别"`
}
type Class struct {
Title string
Students []Student
}
func main() {
c := Class{
Title: "翻斗花园小班",
Students: make([]Student, 0),
}
for i := 1; i < 6; i++ {
gender := "girl"
if i%2 == 0 {
gender = "boy"
}
c.Students = append(c.Students, Student{
Name: fmt.Sprintf("bob_%v", i),
Age: i,
Gender: gender,
})
}
fmt.Printf("%#v\n", c)
jsonbyt, err := json.Marshal(c)
if err != nil {
fmt.Println(err)
} else {
jsonstr := string(jsonbyt)
fmt.Println(jsonstr)
}
}
===》
main.Class{Title:"翻斗花园小班", Students:[]main.Student{main.Student{Name:"bob_1", Age:1, Gender:"girl"}, main.Student{Name:"bob_2", Age:2, Gender:"boy"}, main.Student{Name:"bob_3", Age:3, Gender:"girl"}, main.Student{Name:"bob_4", Age:4, Gender:"boy"}, main.Student{Name:"bob_5", Age:5, Gender:"girl"}}}
{"Title":"翻斗花园小班","Students":[{"姓名":"bob_1","年龄":1,"性别":"girl"},{"姓名":"bob_2","年龄":2,"性别":"boy"},{"姓名":"bob_3","年龄":3,"性别":"girl"},{"姓名":"bob_4","年龄":4,"性别":"boy"},{"姓名":"bob_5","年龄":5,"性别":"girl"}]}
strJ := `{"Title":"翻斗花园小班","Students":[{"姓名":"bob_1","年龄":1,"性别":"girl"},{"姓名":"bob_2","年龄":2,"性别":"boy"},{"姓名":"bob_3","年龄":3,"性别":"girl"},{"姓名":"bob_4","年龄":4,"性别":"boy"},{"姓名":"bob_5","年龄":5,"性别":"girl"}]}`
c2 := &Class{}
errs := json.Unmarshal([]byte(strJ), c2)
if errs != nil {
fmt.Println(errs)
} else {
fmt.Printf("%#v\n", c2)
}
===》
&main.Class{Title:"翻斗花园小班", Students:[]main.Student{main.Student{Name:"bob_1", Age:1, Gender:"girl"}, main.Student{Name:"bob_2", Age:2, Gender:"boy"}, main.Student{Name:"bob_3", Age:3, Gender:"girl"}, main.Student{Name:"bob_4", Age:4, Gender:"boy"}, main.Student{Name:"bob_5", Age:5, Gender:"girl"}}}
Golang中的go mod以及Golang包的详解
1.Golang中包的定义
golang中的包可以分为三种:
1.系统内置包
2.自定义包
3.第三方包
2.go mod使用
1.go mod init初始化项目
go mod init "项目名称"
2.导入自定义包:这个和vue类似import 项目名/路径就可以了
Golang中init函数的说明
执行顺序:全局声明==》init()==》main()
Golang中的接口
golang中的接口是一种抽象数据类型,接口定义了对象的行为规范,只定义规范不是先。接口中定义的规范由具体的对象来实现。
1.golang接口的定义
行为规范的定义
在golang中接口interface是一种类型,一种抽象的类型。接口是一组函数method的集合,golang中的接口不能包含任何变量。
type 接口名 interface{
方法名1(参数列表1)返回值列表1
方法名2(参数列表2)返回值列表2
...
}
如果接口里有方法的话,必须要通过结构体或者通过自定义类型实现这个接口
接口的作用就是一种规范,必须要满足接口的方法才能够实现接口
接口也是一种数据类型,不需要显示实现。只需要一个变量有接口类型中的所有方法,那么这个变量就实现了这个接口
在golang中接口中的所有方法都没有方法体,接口定义了一个对象的行为规范,只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想
type Usber interface {
start()
stop()
}
type Camera struct {
Name string
}
type Phone struct {
Name string
}
type Computer struct {
}
func (c Camera) start() {
fmt.Printf("%v 启动!\n", c.Name)
}
func (c Camera) stop() {
fmt.Printf("%v 停止!\n", c.Name)
}
func (p Phone) start() {
fmt.Printf("%v 启动!\n", p.Name)
}
func (p Phone) stop() {
fmt.Printf("%v 停止!\n", p.Name)
}
func (c Computer) work(usb Usber) {
usb.start()
usb.stop()
}
func main() {
p := Phone{
Name: "原神手机",
}
c := Camera{
Name: "原神相机",
}
com := Computer{}
com.work(p)
com.work(c)
===》
原神手机 启动!
原神手机 停止!
原神相机 启动!
原神相机 停止!
2.空接口
1.空接口
接口可以不定义任何方法,没有定义任何方法的接口就是空接口。空接口表示没有任何约束,因此任何类型变量都可以实现空接口。
//go语言中接口可以不定义任何方法,没有定义任何方法的接口就是空接口。既然空接口表示没有任何约束,那么它存在的意义是什么呢?
空接口在Go语言中的存在有一定的意义,尽管它没有定义任何方法。以下是空接口的几个用途:
1.通用类型:空接口可以表示任意类型的值。当你需要接收或传递不同类型的值时,可以使用空接口作为参数或返回值类型。这样可以实现类似于泛型的功能,使代码更加灵活和通用。
2.容器类型:空接口可以用作容器类型,用于存储不同类型的值。通过将值存储在空接口类型的变量中,可以实现对不同类型值的集合或列表的管理。
3.类型断言与类型判断:空接口可以通过类型断言来判断具体的值类型,并根据需要进行类型转换。这在需要处理不同类型的值时非常有用。
4.与空指针的比较:空接口类型变量的零值是nil,可以将其与nil进行比较,以判断一个接口变量是否为零值。
虽然空接口没有定义任何方法,但它的存在使得Go语言更加灵活和动态。它允许我们在不知道具体类型的情况下处理值,提供了一种通用的方式来处理不同类型的数据。
//那我为什么还要定义接口,直接写不是更加方便吗?定义接口难道不是为了约束吗?
确实,定义接口的一个主要目的是为了约束和规范代码。通过定义接口,可以明确指定一个类型应该具备哪些方法,从而实现对类型的约束和规范。
使用接口的好处包括:
1. 代码解耦:通过接口,可以将代码分离为接口定义和实现两部分。这样,在不改变接口定义的情况下,可以灵活地替换不同的实现。这样的设计可以减少代码的耦合性,提高代码的可维护性和可扩展性。
2. 多态性:接口允许多态性的实现。一个类型实现了接口中定义的方法,就可以被当作该接口类型使用。这样,可以通过接口类型来统一处理不同的实现类型,提高代码的灵活性和可复用性。
3. 规范和文档化:接口的定义可以作为代码的规范和文档,明确指定了一个类型应该具备的方法和行为。这样,其他开发人员在使用该类型时,可以根据接口定义来编写代码,减少错误和提高代码的一致性。
4. 协作开发:接口的定义可以作为不同团队或开发者之间的契约,明确规定了接口的使用方式和行为。这样,在多人协作开发时,可以更好地协调和沟通,提高开发效率和代码质量。
总之,虽然直接编写代码可能更加方便,但通过定义接口,可以实现代码的约束、解耦和规范,提高代码的可维护性和可复用性。接口在面向对象编程中起到了重要的作用。
golang中的空接口也可以当作类型来使用,可以表示任意类型
var x interface{}
x = 1
fmt.Printf("值:%v,类型:%T\n", x, x)
x = "hello"
fmt.Printf("值:%v,类型:%T\n", x, x)
x = true
fmt.Printf("值:%v,类型:%T\n", x, x)
===》
值:1,类型:int
值:hello,类型:string
值:true,类型:bool
2.空接口作为参数使用
func show(x interface{}) {
fmt.Printf("值:%v,类型:%T\n", x, x)
}
show("你好啊哈哈哈哈")
show(true)
===》
值:你好啊哈哈哈哈,类型:string
值:true,类型:bool
3.map的值实现空接口
使用空接口实现可以保存任意值的字典
func show(x interface{}) {
fmt.Printf("值:%v,类型:%T\n", x, x)
}
xslices := make(map[string]interface{})
xslices["name"] = "张三"
xslices["status"] = false
xslices["age"] = 18
fmt.Printf("%v\n", xslices)
===》
map[age:18 name:张三 status:false]
3.类型断言
在实际运用中经常使用空接口,因为空接口可以代表任意的类型,但我们又想要知道这个值到底是什么类型,此时我们可以使用类型断言
x.(T)
其中:x:表示类型为interface{}的变量。T:表示断言x可能是的类型
返回两个参数,1.x转为T类型后的变量。2true或者false是否为判断的类型
var a interface{}
a = "你好啊"
v, ok := a.(string)
fmt.Println(v, ok)
if ok {
fmt.Printf("%v是string类型!", v)
} else {
fmt.Printf("断言失败")
}
===》
你好啊 true
你好啊是string类型!
实际运用:
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("变量v的值为 %v,类型为 %T\n", v, v)
case int:
fmt.Printf("变量v的值为 %d,类型为 %T\n", v, v)
case bool:
fmt.Printf("变量v的值为 %t,类型为 %T\n", v, v)
default:
fmt.Printf("未知类型\n")
}
}
justifyType("你好啊")
justifyType(false)
justifyType(100)
===》
变量v的值为 你好啊,类型为 string
变量v的值为 false,类型为 bool
变量v的值为 100,类型为 int
type Usber interface {
start()
stop()
}
type Camera struct {
Name string
}
type Phone struct {
Name string
}
type Computer struct {
}
func (c Camera) start() {
fmt.Printf("%v 启动!\n", c.Name)
}
func (c Camera) stop() {
fmt.Printf("%v 停止!\n", c.Name)
}
func (p Phone) start() {
fmt.Printf("%v原神启动!\n", p.Name)
}
func (p Phone) stop() {
fmt.Printf("%v 停止!\n", p.Name)
}
func (c Computer) work(usb Usber) {
v, ok := usb.(Phone)
if ok {
fmt.Printf("检测到设备为%v..正在为你启动手机原神\n", v)
usb.start()
} else {
fmt.Printf("检测到设备为%v..正在为你启动相机原神\n", v)
usb.start()
}
}
p := Phone{
Name: "手机",
}
c := Camera{
Name: "原神相机",
}
com := Computer{}
com.work(p)
com.work(c)
===》
检测到设备为{手机}..正在为你启动手机原神
手机原神启动!
检测到设备为{}..正在为你启动相机原神
原神相机 启动!
4.结构体值接收者和指针接收者实现接口的区别
值接收者:
如果结构体中的方法是值接收者,那么实例化后的结构体值类型和结构体指针类型都可以赋值给接口变量
func (p Phone) start() {
fmt.Printf("%v原神启动!\n", p.Name)
}
func (p Phone) stop() {
fmt.Printf("%v 停止!\n", p.Name)
}
var phone1 = Phone{
Name:"小米"
}
var phone2 = &Phone{
Name:"苹果"
}
var p1 Usber = phone1
p1.start() //ok
var p2 Usber = phone2
p2.start() //ok
//两种实例化都能实现接口
如果结构体中的方法是指针接收者,那么实例化后的结构体只有指针类型可以赋值给接口变量
func (p *Phone) start() {
fmt.Printf("%v原神启动!\n", p.Name)
}
func (p *Phone) stop() {
fmt.Printf("%v 停止!\n", p.Name)
}
var phone1 = Phone{
Name:"小米"
}
var phone2 = &Phone{
Name:"苹果"
}
var p1 Usber = phone1
p1.start() //no
var p2 Usber = &phone2
p2.start() //ok
//两种实例化只有p2能实现接口
5.结构体实现多接口,结构体指针接收者实现接口
package main
import "fmt"
type Animaler2 interface {
SetName(string)
}
type Animaler1 interface {
GetName() string
}
type Dog struct {
Name string
}
type Cat struct {
Name string
}
func (d *Dog) SetName(name string) {
fmt.Printf("Dog SetName %v\n", name)
d.Name = name
}
func (d Dog) GetName() string {
fmt.Println("Dog GetName")
return d.Name
}
func (c *Cat) SetName(name string) {
fmt.Printf("Cat SetName %v\n", name)
c.Name = name
}
func (c Cat) GetName() string {
fmt.Println("Cat GetName")
return c.Name
}
func main() {
dog := &Dog{"小黑子"}
var d1 Animaler1 = dog //表示让d1实现animaler1接口并且实例化结构体
var d2 Animaler2 = dog //表示让d2实现animaler2接口并且实例化结构体
fmt.Println(d1.GetName())
d2.SetName("小黑子2")
fmt.Println(d1.GetName())
cat := &Cat{"多多"}
var c1 Animaler1 = cat
var c2 Animaler2 = cat
fmt.Println(c1.GetName())
c2.SetName("多多2")
fmt.Println(c1.GetName())
}
===》
Dog GetName
小黑子
Dog SetName 小黑子2
Dog GetName
小黑子2
Cat GetName
多多
Cat SetName 多多2
Cat GetName
多多2
6.接口嵌套
package main
import "fmt"
type Ainterface interface {
SetName(string)
}
type Binterface interface {
GetName() string
}
type Animaler interface {
Ainterface
Binterface
}
type Dog struct {
Name string
}
type Cat struct {
Name string
}
func (d *Dog) SetName(name string) {
fmt.Printf("Dog SetName %v\n", name)
d.Name = name
}
func (d Dog) GetName() string {
fmt.Println("Dog GetName")
return d.Name
}
func (c *Cat) SetName(name string) {
fmt.Printf("Cat SetName %v\n", name)
c.Name = name
}
func (c Cat) GetName() string {
fmt.Println("Cat GetName")
return c.Name
}
func main() {
dog := &Dog{"小黑子"}
var d Animaler = dog
fmt.Println(d.GetName())
d.SetName("小黑子2")
fmt.Println(d.GetName())
cat := &Cat{"多多"}
var c Animaler = cat
fmt.Println(c.GetName())
c.SetName("多多2")
fmt.Println(c.GetName())
}
===》
Dog GetName
小黑子
Dog SetName 小黑子2
Dog GetName
小黑子2
Cat GetName
多多
Cat SetName 多多2
Cat GetName
多多2
7.空接口和类型断言使用细节
userinfo := make(map[string]interface{})
userinfo["name"] = "张三"
userinfo["age"] = 18
userinfo["hobbys"] = []string{"吃饭", "睡觉", "打豆豆"}
fmt.Println(userinfo["name"])
fmt.Println(userinfo["age"])
//fmt.Println(userinfo["hobbys"][1]) //打印失败(map index expression of type interface{})
v, _ := userinfo["hobbys"].([]string)
fmt.Println(v[0]) //打印成功
===》
张三
18
吃饭
Golang goroutine channel实现并发和并行
1.关于并行和并发
并发:多个线程同时竞争一个位置,竞争到才能执行,每一个时间段只有一个线程在执行
并行:多个线程可以同时执行,每一个时间段,可以有多个线程同时执行
通俗的讲多线程程序在单核cpu上运行就是并发,多线程程序在多核cpu上运行就是并行,如果线程数大于cpu核数,则多线程程序在多个cpu上面运行既有并行又有并发
2.Golang中的协程(goroutine)以及主线程
golang中的主线程:在一个golang程序的主线程上可以起多个协程。golang中多协程可以实现并行或者并发
3.goroutine的使用以及sync.WaitGroup
var wg sync.WaitGroup
func printText() {
for i := 0; i < 10; i++ {
fmt.Println("printText", i)
time.Sleep(time.Millisecond * 100)
}
wg.Done() //协程计数器-1
}
wg.Add(1) //协程计数器+1
go printText() //开启协程
for i := 0; i < 10; i++ {
fmt.Println("你好golang", i)
time.Sleep(time.Millisecond * 50)
}
wg.Wait() //等待协程执行完成
fmt.Println("主线程退出")
===》
你好golang 0
printText 0
你好golang 1
printText 1
你好golang 2
你好golang 3
printText 2
你好golang 4
你好golang 5
printText 3
你好golang 6
printText 4
你好golang 7
你好golang 8
printText 5
你好golang 9
printText 6
printText 7
printText 8
printText 9
主线程退出
4.设置golang并行运行时占用的cpu数量
//获取计算机cpu总数
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum:", cpuNum)
//设置使用多个cpu
runtime.GOMAXPROCS(cpuNum)
fmt.Println("fine")
5.for循环开启协程完成事务
var wg sync.WaitGroup
func printText(x int) {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Printf("正在打印%v协程的第%v条数据\n", x, i)
time.Sleep(time.Millisecond * 100)
}
}
func main() {
//获取计算机cpu总数
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum:", cpuNum)
//设置使用多个cpu
runtime.GOMAXPROCS(cpuNum)
fmt.Println("fine")
for i := 0; i < 10; i++ {
wg.Add(1)
go printText(i)
}
wg.Wait()
fmt.Println("打印完毕主进程退出...")
===》
正在打印8协程的第9条数据
正在打印4协程的第9条数据
正在打印9协程的第9条数据
正在打印5协程的第9条数据
正在打印3协程的第9条数据
正在打印1协程的第9条数据
...
打印完毕主进程退出...
Channel管道
管道是golang在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。channel是可以让一个goroutine发送特定值到另一个gorutine的通讯机制
1.channel类型
channel是一种类型,一种引用类型。声明管道类型的格式如下
var 变量 chan 元素类型
例子:
var ch1 chan int //声明一个传递整型的管道
var ch2 chan bool //声明一个传递布尔型的管道
var ch3 chan []int //声明一个传递int切片的管道
2.创建channel
声明的管道后需要使用make函数初始化之后才能使用
make(chan 元素类型,容量)
3.channel操作
管道有发送(send)、接收(receive)、关闭(close)三种操作
发送和接收都使用**<—**符号
//创建管道
ch1 := make(chan int, 3)
//储存值
ch1 <- 1
ch1 <- 10
ch1 <- 100
//接收值
x := <-ch1
//输出值
fmt.Println(x)
<-ch1 //取出10
y := <-ch1 //取出100赋值给y 此时管道长度为空
fmt.Println(y)
ch1 <- 1000 //再存一个
fmt.Printf("值:%v,容量:%v,长度:%v", ch1, cap(ch1), len(ch1)) //打印管道信息
===》
1
100
值:0xc00001c100,容量:3,长度:1
4.管道阻塞
1.无缓冲管道
如果创建管道的时候没有指定容量,那么这个就是无缓冲管道
没有指定容量或者存储超过容量数据时,取数据无长度时都会报错
5.循环遍历管道数据
使用for range遍历 需要关闭通道 否则会造成死锁
ch1 := make(chan int, 10)
go func() {
for i := 0; i <= 10; i++ {
ch1 <- i
}
close(ch1) //关闭通道
}()
for v := range ch1 {
fmt.Println(v)
}
===》
0
1
2
3
4
5
6
7
8
9
10
使用for遍历无需关闭通道
ch1 := make(chan int, 10)
go func() {
for i := 0; i <= 10; i++ {
ch1 <- i
}
// close(ch1) //关闭通道
}()
// for v := range ch1 {
// fmt.Println(v)
// }
for i := 0; i < 10; i++ {
fmt.Println(<-ch1)
}
===》正常输出
6.协程结合管道实现并行效果
func write(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
fmt.Printf("【写入】数据%v成功\n", i)
time.Sleep(time.Millisecond * 1000)
}
close(ch)
wg.Done()
}
func read(ch chan int) {
for v := range ch {
fmt.Printf("【读取】数据%v成功\n", v)
time.Sleep(time.Millisecond * 100)
}
wg.Done()
}
var wg sync.WaitGroup
ch1 := make(chan int, 10)
wg.Add(1)
go write(ch1)
wg.Add(1)
go read(ch1)
wg.Wait()
===》
【写入】数据0成功
【读取】数据0成功
【写入】数据1成功
【读取】数据1成功
【写入】数据2成功
【读取】数据2成功
【写入】数据3成功
【读取】数据3成功
【写入】数据4成功
【读取】数据4成功
【写入】数据5成功
【读取】数据5成功
【写入】数据6成功
【读取】数据6成功
【写入】数据7成功
【读取】数据7成功
【写入】数据8成功
【读取】数据8成功
【写入】数据9成功
【读取】数据9成功
7.单向管道
有的时候我们会将管道作为参数在多个任务函数间传递,很难多时候我们在不同的任务函数中使用管道都会对其进行限制,比如限制管道在函数中只能发送或只能接收
//默认情况下 管道是双向的,可读可写
var c chan int
c = make(chan int)
c <- 1
fmt.Println(<-c)
//定义只写
b := make(chan<- int, 2)
b <- 2
b <- 3
<-b
===》报错,只写不能读
//定义只读
a := make(<-chan int, 2)
a <- 1
===》报错,只读不能写
8.select多路复用
在某些场景下我们需要同时从多个通道接收数据。这个时候就可以用到golang中给我们提供的select多路复用。
通常情况通道在接收数据时,如果没有数据可以接收将会发生阻塞
使用select多路复用不需要关闭chan
a := make(chan int, 10)
for i := 0; i < 10; i++ {
a <- i
}
b := make(chan string, 10)
for i := 0; i < 10; i++ {
str := fmt.Sprintf("hello %d", i)
b <- str
}
for {
select {
case v := <-a:
fmt.Printf("intchan取值%v\n", v)
time.Sleep(time.Second * 1)
case v := <-b:
fmt.Printf("stringchan取值%v\n", v)
time.Sleep(time.Second * 1)
default:
fmt.Println("所有通道都取出值执行default...")
return
}
}
===》
stringchan取值hello 0
intchan取值0
stringchan取值hello 1
stringchan取值hello 2
intchan取值1
intchan取值2
intchan取值3
intchan取值4
stringchan取值hello 3
intchan取值5
intchan取值6
intchan取值7
stringchan取值hello 4
intchan取值8
stringchan取值hello 5
intchan取值9
stringchan取值hello 6
stringchan取值hello 7
stringchan取值hello 8
stringchan取值hello 9
所有通道都取出值执行default...
9.goroutine Recover解决协程中出现的panic
func f(from chan int) {
for i := 0; i < 10; i++ {
from <- i
fmt.Println("f:", i)
}
}
func mapT() {
defer func() {
if err := recover(); err != nil {
fmt.Println("mapT发生错误recover:", err)
}
}()
var m map[int]int
m[0] = 1
}
from := make(chan int, 10)
go f(from)
go mapT()
time.Sleep(time.Second)
===》
mapT发生错误recover: assignment to entry in nil map
f: 0
f: 1
f: 2
f: 3
f: 4
f: 5
f: 6
f: 7
f: 8
f: 9
Golang并发安全与锁
1.不进行任何锁
让我们再看看不对goroutine进行任何锁是怎么执行的
func write() {
fmt.Printf("...进行写操作 %v\n", time.Now().Unix())
time.Sleep(time.Second)
wg.Done()
}
func read() {
fmt.Printf("进行读操作... %v\n", time.Now().Unix())
time.Sleep(time.Second)
wg.Done()
}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 10; i++ {
wg.Add(1)
go read()
}
wg.Wait()
===》
...进行写操作 1698650938
...进行写操作 1698650938
...进行写操作 1698650938
进行读操作... 1698650938
...进行写操作 1698650938
...进行写操作 1698650938
...进行写操作 1698650938
...进行写操作 1698650938
进行读操作... 1698650938
...进行写操作 1698650938
...进行写操作 1698650938
进行读操作... 1698650938
进行读操作... 1698650938
进行读操作... 1698650938
进行读操作... 1698650938
进行读操作... 1698650938
进行读操作... 1698650938
进行读操作... 1698650938
进行读操作... 1698650938
...进行写操作 1698650938
//从输出中我们可以看出所有的的操作都是在同一时刻进行的
2.互斥锁
互斥锁时传统并发编程中对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,lock和unlock。lock锁定当前的共享资源,unlock进行解锁
func write() {
mutex.Lock()
fmt.Printf("...进行写操作 %v\n", time.Now().Unix())
time.Sleep(time.Second)
mutex.Unlock()
wg.Done()
}
func read() {
mutex.Lock()
fmt.Printf("进行读操作... %v\n", time.Now().Unix())
time.Sleep(time.Second)
mutex.Unlock()
wg.Done()
}
var wg sync.WaitGroup
var mutex sync.Mutex
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 10; i++ {
wg.Add(1)
go read()
}
wg.Wait()
===》加了互斥锁 go build -race main.go后就不会报错了 这句指令表示打包时检测是否存在竞争关系
...进行写操作 1698649246
...进行写操作 1698649247
...进行写操作 1698649248
...进行写操作 1698649249
...进行写操作 1698649250
...进行写操作 1698649251
...进行写操作 1698649252
...进行写操作 1698649253
进行读操作... 1698649254
...进行写操作 1698649255
进行读操作... 1698649256
进行读操作... 1698649257
...进行写操作 1698649258
进行读操作... 1698649259
进行读操作... 1698649260
进行读操作... 1698649261
进行读操作... 1698649262
进行读操作... 1698649263
进行读操作... 1698649264
进行读操作... 1698649265
//从输出中不难看出,加了互斥锁意味着无论是读写都得等待解锁了才能进行下一步操作
3.读写互斥锁
sync.RWMutex
互斥锁的本质是当一个goroutine访问的时候,其它goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。
其实,当我们对一个不会变化的数据只做”读“操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少goroutine同时读取都是没有影响的。
所以问题是出现在修改上,修改的数据需要同步,这样goroutine才可以感知到。真正的互斥应该是读取和修改,修改与修改,读取与读取没有互斥操作的必要。
因此,衍生出另一种锁,叫做读写锁
读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作时,其它goroutine既不能进行读操作,也不能进行写操作。
func write() {
mutex.Lock()
fmt.Printf("...进行写操作 %v\n", time.Now().Unix())
time.Sleep(time.Second)
mutex.Unlock()
wg.Done()
}
func read() {
mutex.RLock()
fmt.Printf("进行读操作... %v\n", time.Now().Unix())
time.Sleep(time.Second)
mutex.RUnlock()
wg.Done()
}
var wg sync.WaitGroup
var mutex sync.RWMutex
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 10; i++ {
wg.Add(1)
go read()
}
wg.Wait()
===》
...进行写操作 1698649816
进行读操作... 1698649817
进行读操作... 1698649817
进行读操作... 1698649817
进行读操作... 1698649817
进行读操作... 1698649817
进行读操作... 1698649817
进行读操作... 1698649817
进行读操作... 1698649817
进行读操作... 1698649817
进行读操作... 1698649817
...进行写操作 1698649818
...进行写操作 1698649819
...进行写操作 1698649820
...进行写操作 1698649821
...进行写操作 1698649822
...进行写操作 1698649823
...进行写操作 1698649824
...进行写操作 1698649825
...进行写操作 1698649826
//从上面的输出可以看出读写互斥锁 在写的时候不能进行读和写操作,读的时候可以并行读
Golang反射
1.gpt关于反射的解释
在Go语言中,反射是由reflect
包提供的一种机制,它可以在运行时更新变量和检查它们的值、调用它们的方法、并且对它们的结构进行操作。
以下是一些基本的反射操作:
-
reflect.TypeOf()
:返回一个变量的类型。例如,reflect.TypeOf(myVar)
将返回myVar
的类型。 -
reflect.ValueOf()
:返回一个变量的值。例如,reflect.ValueOf(myVar)
将返回myVar
的值。 -
Interface()
:将reflect.Value
转换回普通的接口{}。 -
NumField()
和Field()
:用于访问结构体的字段。例如,如果我们有一个结构体myStruct
,我们可以使用reflect.TypeOf(myStruct).NumField()
来获取它有多少个字段,然后使用reflect.ValueOf(myStruct).Field(i)
来访问第i
个字段。 -
NumMethod()
和Method()
:用于访问结构体的方法。例如,如果我们有一个结构体myStruct
,我们可以使用reflect.TypeOf(myStruct).NumMethod()
来获取它有多少个方法,然后使用reflect.ValueOf(myStruct).Method(i).Call(nil)
来调用第i
个方法。
反射是一个强大的工具,但也需要谨慎使用。由于反射操作在编译时不会进行类型检查,因此如果使用不当,可能会导致运行时错误。并且反射操作相比于普通操作来说,性能开销较大,所以在性能敏感的程序中应尽量避免使用。
使用反射可以在程序运行期间获取类型的反射信息,并且有能力修改它们。
可以实现的功能:
1.在程序运行期间动态的获取变量的各种信息,比如类型,类别
2.如果是结构体,可以通过反射获取结构体本身的信息,如结构体字段,结构体方法
3.可以修改变量的值,调用关联的方法
go语言中的变量分为两部分
1.类型信息:预先定义好的元信息
2.值信息:程序运行过程中可动态变化的。
2.reflect.TypeOf()获取任意值的类型对象
func Analysis(x interface{}) {
v := reflect.TypeOf(x)
fmt.Printf("[%v] 的类型为:%v typeName为:%v typeKind为:%v\n", x, v, v.Name(), v.Kind())
}
type myint int
type Person struct {
Name string
Age int
}
a := 10
b := 12.1
c := "反射"
d := true
var e myint = 88
var f Person = Person{"张三", 20}
g := &a
Analysis(a)
Analysis(b)
Analysis(c)
Analysis(d)
Analysis(e)
Analysis(f)
Analysis(g)
===》
[10] 的类型为:int typeName为:int typeKind为:int
[12.1] 的类型为:float64 typeName为:float64 typeKind为:float64
[反射] 的类型为:string typeName为:string typeKind为:string
[true] 的类型为:bool typeName为:bool typeKind为:bool
[88] 的类型为:main.myint typeName为:myint typeKind为:int
[{张三 20}] 的类型为:main.Person typeName为:Person typeKind为:struct
[0xc00000a0c8] 的类型为:*int typeName为: typeKind为:ptr
其中reflect.TypeOf(x)返回一个对象,对象包含两个方法
.Name返回类型名称,.Kind返回底层类型
3.reflect.ValueOf()
同样返回对象中存在.Kind返回底层类型
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
switch v.Kind() {
case reflect.String:
fmt.Printf("string类型的原始值%v\n", v.String())
case reflect.Int:
fmt.Printf("int类型的原始值%v\n", v.Int())
case reflect.Float32, reflect.Float64:
fmt.Printf("float类型的原始值%v\n", v.Float())
case reflect.Bool:
fmt.Printf("bool类型的原始值%v\n", v.Bool())
case reflect.Slice:
fmt.Printf("slice类型的原始值%v\n", v.Interface())
case reflect.Map:
fmt.Printf("map类型的原始值%v\n", v.Interface())
case reflect.Ptr:
fmt.Printf("ptr类型的原始值%v\n", v.Interface())
case reflect.Struct:
fmt.Printf("struct类型的原始值%v\n", v.Interface())
case reflect.Func:
fmt.Printf("func类型的原始值%v\n", v.Interface())
default:
fmt.Printf("未知类型\n")
}
}
a := []map[string]int{
{"a": 1, "b": 2},
{"a": 3, "b": 4},
{"a": 5, "b": 6},
}
b := true
c := "反射"
reflectValue(a)
reflectValue(b)
reflectValue(c)
===》
slice类型的原始值[map[a:1 b:2] map[a:3 b:4] map[a:5 b:6]]
bool类型的原始值true
string类型的原始值反射
4.通过反射设置变量的值
通过反射设置变量的值时,必须使用指针类型。原因在于,当您将变量传递给一个接受interface{}
类型参数的函数时,Go 会创建该变量的一个副本。如果您不使用指针类型,那么在函数内部对变量的修改将仅作用于这个副本,而不会影响到原始变量。
func SetReflectValue(x interface{}) {
v := reflect.ValueOf(x)
fmt.Printf("传入x的原始值为:%v 原始类型为:%v\n", v, v.Elem().Kind())
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(88)
}
}
var a int64 = 100
SetReflectValue(&a)
fmt.Println("修改后的值", a)
===》
传入x的原始值为:0xc00000a0c8 原始类型为:int64
修改后的值 88
5.结构体反射
1.与结构体相关的方法
package main
import (
"fmt"
"reflect"
"strconv"
)
func PrintfStructFn(s interface{}) {
// 打印结构体内的方法
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
fmt.Println("not struct")
return
} else {
// 1.通过类型变量的Method可以获取结构体内的方法
m := t.Method(0) //与结构体方法的顺序无关,与结构体方法的ASCII码有关
fmt.Printf("方法名=%s\n", m.Name)
fmt.Println("-------------------------------")
// 2.通过类型变量来获取这个结构体内有多少个方法
fmt.Printf("方法数量=%d\n", t.NumMethod())
m1, _ := t.MethodByName("Print")
fmt.Printf("方法名=%v\n", m1.Name)
fmt.Printf("方法类型=%v\n", m1.Type)
fmt.Println("-------------------------------")
// 3.通过值变量执行方法,(注意需要使用值变量,并且要注意参数)
// 使用v.Method(下标).Call(方法需要传入的参数)来执行结构体内的方法
v.Method(0).Call(nil)
v.MethodByName("Print").Call(nil)
fmt.Println("-------------------------------")
// 4.通过值变量执行方法(传递参数)
var params []reflect.Value
params = append(params, reflect.ValueOf("小黑子"))
v.MethodByName("Setinfo").Call(params)
// 5.执行方法获取方法的值
info := v.MethodByName("Print").Call(nil)
fmt.Println("info", info[0].Interface())
// 6.修改结构体属性的值
name := v.Elem().FieldByName("Name")
name.SetString("老黑子")
v.MethodByName("Print").Call(nil)
}
}
func PrintfStructField(s interface{}) {
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
fmt.Println("not struct")
return
} else {
// 1.通过类型变量里面的Field来获取结构字段
f := t.Elem().Field(0)
fmt.Printf("f字段名称=%v\n", f.Name)
fmt.Printf("f字段类型=%v\n", f.Type)
fmt.Printf("f字段Tag=%v\n", f.Tag.Get("json"))
// 2.通过类型变量里面的FieldByName来获取结构字段
f1, ok := t.Elem().FieldByName("Age")
if ok {
fmt.Printf("f1字段名称=%v\n", f1.Name)
fmt.Printf("f1字段类型=%v\n", f1.Type)
fmt.Printf("f1字段Tag=%v\n", f1.Tag.Get("json"))
}
// 3.通过类型变量里面的NumField来获取该结构体有几个字段
fieldCount := t.Elem().NumField()
fmt.Printf("字段总数=%v\n", fieldCount)
// 4.通过值变量获取结构体属性对应的值
fmt.Printf("f字段名称=%v\n", f.Name)
fmt.Printf("f字段类型=%v\n", f.Type)
fmt.Printf("f字段Tag=%v\n", f.Tag.Get("json"))
fmt.Printf("f字段值=%v\n", v.Elem().FieldByName(f.Name))
}
}
type Student struct {
Name string `json:"name" form:"username"`
Age int `json:"age"`
Sex string `json:"usersex"`
}
func (s Student) Print() []string {
fmt.Println(s.Name, s.Age, s.Sex)
return []string{s.Name, strconv.Itoa(s.Age), s.Sex}
}
func (s *Student) Setinfo(name string) {
s.Name = name
}
func main() {
s := Student{
"张三",
2,
"男",
}
// PrintfStructField(&s)
PrintfStructFn(&s)
}
===》
方法名=Print
-------------------------------
方法数量=2
方法名=Print
方法类型=func(*main.Student) []string
-------------------------------
张三 2 男
张三 2 男
-------------------------------
小黑子 2 男
info [小黑子 2 男]
老黑子 2 男
Golang文件目录操作
1.读取文件
方法1:以只读的方式打开文件
package main
import (
"fmt"
"io"
"os"
)
func main() {
// 以只读方式打开文件
file, err := os.Open("./main.go")
if err != nil {
fmt.Println("打开文件失败")
return
}
defer file.Close() //文件打开后必须关闭
fmt.Println("file", file)
// 读取文件
var tempSlice = make([]byte, 1024)
var strSlice []byte
for {
n, err := file.Read(tempSlice)
if err == io.EOF {
fmt.Println("读取完毕...")
break
}
if err != nil {
fmt.Println("读取文件失败", err)
return
}
fmt.Printf("读取文件成功,共读取%v个字节\n", n)
strSlice = append(strSlice, tempSlice[:n]...)
}
fmt.Println(string(strSlice))
}
方法2:bufio读取文件
func TwoReadFile() {
file, err := os.Open("./main.go")
defer file.Close()
if err != nil {
fmt.Println(err)
return
}
reader := bufio.NewReader(file)
var fileStr string
for {
str, err := reader.ReadString('\n') //\n表示一次读取一行
if err == io.EOF {
fmt.Println("读取完毕...")
fileStr += str //注意,当它读取完毕时可能还有返回值,所以这里还需要拼接
break
} else {
if err != nil {
fmt.Println(err)
return
}
// fmt.Println(str)
fileStr += str
}
}
fmt.Println(fileStr)
}
方法3:通过ioutil读取文件
func ThreeReadFile() {
byteStr, err := ioutil.ReadFile("./main.go")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(byteStr))
}
值得注意的是:在Go 1.16版本中,ioutil
包中的一些函数被标记为已弃用,这包括 ioutil.ReadFile
函数。这是因为Go团队对这些函数进行了重新组织,以便在语义上更清晰和一致。
ioutil.ReadFile
现在被推荐替换为 os.ReadFile
。所以,如果你的Go版本是1.16或更高,你应该使用 os.ReadFile
来读取文件,而不是 ioutil.ReadFile
。
例如,你可以将代码从:
data, err := ioutil.ReadFile("file.txt")
改为:
data, err := os.ReadFile("file.txt")
这两个函数的功能是完全相同的,只是它们所在的包不同。
2.写入文件
模式 | 含义 |
---|---|
os.O_WRONLY | 只写 |
os.O_CREATE | 创建文件 |
os.O_RDONLY | 只读 |
os.O_RDWR | 读写 |
os.O_TRUNC | 清空 |
os.O_APPEND | 追加 |
perm:文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01。
3种方法:
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func OneReadFile() {
// 以只读方式打开文件
file, err := os.Open("./main.go")
if err != nil {
fmt.Println("打开文件失败")
return
}
defer file.Close() //文件打开后必须关闭
fmt.Println("file", file)
// 读取文件
var tempSlice = make([]byte, 1024)
var strSlice []byte
for {
n, err := file.Read(tempSlice)
if err == io.EOF {
fmt.Println("读取完毕...")
break
}
if err != nil {
fmt.Println("读取文件失败", err)
return
}
fmt.Printf("读取文件成功,共读取%v个字节\n", n)
strSlice = append(strSlice, tempSlice[:n]...)
}
fmt.Println(string(strSlice))
}
func TwoReadFile() {
file, err := os.Open("./main.go")
defer file.Close()
if err != nil {
fmt.Println(err)
return
}
reader := bufio.NewReader(file)
var fileStr string
for {
str, err := reader.ReadString('\n') //\n表示一次读取一行
if err == io.EOF {
fmt.Println("读取完毕...")
fileStr += str
break
} else {
if err != nil {
fmt.Println(err)
return
}
// fmt.Println(str)
fileStr += str
}
}
fmt.Println(fileStr)
}
func ThreeReadFile() {
byteStr, err := os.ReadFile("./main.go")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(byteStr))
}
func OneWritFile() {
file, err := os.OpenFile("D:/golangIO.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
fmt.Println("打开文件失败")
return
}
defer file.Close()
//写入文件
for i := 0; i < 100; i++ {
file.WriteString("破电脑是真的卡...\r\n")
}
}
func TwoWritFile() {
file, err := os.OpenFile("D:/golangIO.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
fmt.Println("打开文件失败")
return
}
defer file.Close()
writer := bufio.NewWriter(file)
writer.WriteString("golang...\r\n")
writer.Flush()
}
func ThreeWritFile() {
str := "斤斤计较急急急急急急"
err := os.WriteFile("D:/golangIO.txt", []byte(str), 0666)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("写入完毕...")
}
func main() {
ThreeWritFile()
}
3.复制文件删除文件
package main
import (
"fmt"
"os"
)
func FileCopy(FilePath1 string, FilePath2 string) (err error) {
byteStr, err := os.ReadFile(FilePath1)
if err != nil {
fmt.Println(err)
return err
}
err1 := os.WriteFile(FilePath2, byteStr, 0666)
if err1 != nil {
return err1
}
return nil
}
func main() {
err := FileCopy("D:/output.log", "E:/output.txt")
if err != nil {
fmt.Println("复制失败", err)
} else {
fmt.Println("复制成功")
}
err1 := os.MkdirAll("./fuck", 0666)
if err1 != nil {
fmt.Println("文件夹创建失败", err1)
}
err2 := os.Remove("./fuck")
if err2 != nil {
fmt.Println("文件删除失败", err2)
}
err3 := os.RemoveAll("./fuck")
if err3 != nil {
fmt.Println("文件删除失败", err3)
}
err4 := os.Rename("./fuck", "./fucks")
if err4 != nil {
fmt.Println("文件重命名失败", err4)
}
}