前言: 这是golang初学者
CrispCookie
根据找的教程pdf归纳出的markdown笔记(写的比较粗糙), 写的时候发现超过2w字typora就有明显卡顿了, 太逊了。go是一门比较缝合怪<杂糅了各种高级语言的语法>的一门语言,有着python的切片, c的指针, java的GC机制等等语法特性, 并且还有着一些让人两眼一黑的语法, 比如开头字母大写表示public, 小写表示private, 结构体的继承语法, 都让初学者学习过程颇有趣味性。当然go的优点和潜力也显而易见, 多协程的低内存消耗一骑绝尘等, 我也希望通过go将来能有碗饭吃
环境搭建 GO语言mac环境搭建 下载安装Golang Go 官网下载地址:https://golang.org/dl/
Go 官方镜像站(推荐):https://golang.google.cn/dl/
安装软件 1.双击下一步下一步进行安装
2.验证安装是否成功
3.查看go环境
说明: Go1.11 版本之后无需手动配置环境变量,使用 go mod 管理项目,也不需要非得把项 目放到 GOPATH 指定目录下,你可以在你磁盘的任何位置新建一个项目. Go1.13 以后可以彻底不要 GOPATH了
下载goland, 简单省事无脑
变量声明 变量的来历 程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存 上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性 会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接 通过这个变量就能找到内存上对应的数据了
变量类型 变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个 多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型 、 浮点型 、布尔型 等。 Go 语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用
GO 语言中变量的声明 Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字 。Go 语言中关键字 和保留字都不能用作变量名。 Go 语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且 Go 语言的变 量声明后必须使用
var 声明变量
1 2 3 var name string var age int var isOk bool
1 2 3 4 5 6 package mainimport "fmt" ;func main () { var username="张三" var age int =20 fmt.Println(username,age) }
一次定义多个变量 1 var identifier1, identifier2 type
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { var username, sex username = "张三" sex = "男" fmt.Println(username, sex) } var a, b, c, d = 1 , 2 , 3 , false
批量声明变量的时候指定类型 1 2 3 4 5 6 7 8 var (a string b int c bool ) a = "张三" b = 10 c = true fmt.Println(a,b,c)
批量声明变量并赋值 1 2 3 4 5 6 7 var ( a string = "张三" b int = 20 c bool = true ) fmt.Println(a, b, c) fmt.Println(a,b,c)
变量的初始化 Go 语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被 初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为 0。 字符串变量的默认值 为空字符串。 布尔型变量默认为 false。 切片、函数、指针变量的默认为 nil 。 当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:
举个例子:
1 2 var name string = "zhangsan" var age int =18
或者一次初始化多个变量并赋值
1 var name, age = "zhangsan" , 20
类型推导 有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完 成初始化
1 2 var name = "Q1mi" var age = 18
短变量声明法 在函数内部,可以使用更简略的 := 方式声明并初始化变量。
注意 :短变量只能用于声明局部变量 ,不能用于全局变量的声明
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "fmt" ) var m = 100 func main () { n := 10 m := 200 fmt.Println(m, n) }
使用变量一次声明多个变量,并初始化变量
1 2 m1, m2, m3 := 10 , 20 , 30 fmt.Println(m1, m2, m3)
匿名变量 在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable). 匿名变量用一个下划线_表示,例如:
1 2 3 4 5 6 7 8 9 func getInfo () (int , string ) { return 10 , "张三" } func main () { _, username := getInfo() fmt.Println(username) }
匿名变量不占用命名空间,不会分配内存 ,所以匿名变量之间不存在重复声明
注意事项:
1、函数外的每个语句都必须以关键字开始(var、const、func 等)
2、:=不能使用在函数外。
3、_多用于占位,表示忽略值。
Go 语言中的常量 相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的 声明和变量声明非常类似,只是把 var 换成了 const,常量在定义的时候必须赋值
使用 const 定义常量 1 2 const pi = 3.1415 const e = 2.7182
声明了 pi 和 e 这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了
多个 常量也可以一起声明:
1 2 3 4 const ( pi = 3.1415 e = 2.7182 )
const 同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如
1 2 3 4 5 const ( n1 = 100 n2 n3 )
上面示例中,常量 n1、n2、n3 的值都是 100
基本数据类型 Golang 数据类型介绍 Go 语言中数据类型分为:基本数据类型和复合数据类型
基本数据类型 有: 整型、浮点型、布尔型、字符串
复合数据类型 有: 数组、切片、结构体、函数、map、通道(channel)、接口等
整形 整型分为以下两个大类:
有符号整形按长度分为:int8、int16、int32、int64
对应的无符号整型:uint8、uint16、uint32、uint64
无符号所能表示的数据长度时对应有符号的两倍
类型
范围
占用空间
有无符号
int8
(-128 到 127) -2 ^7 到 2 ^7 -1
1 个字节
有
int16
(-32768 到 32767) -2 ^15 到 2 ^15 -1
2 个字节
有
int32
(-2147483648 到 2147483647) -2 ^31 到 2 ^31 -1
4 个字节
有
int64
(-9223372036854775808 到 9223372036854775807) -2 ^63 到 2 ^63 -1
8 个字节
有
uint8
(0 到 255) 0 到 2 ^8 -1
1 个字节
无
uint16
(0 到 65535) 0 到 2 ^16 -1
2 个字节
无
uint32
(0 到 4294967295) 0 到 2 ^32 -1
4 个字节
无
uint64
(0 到 18446744073709551615) 0 到 2 ^64 -1
8 个字节
无
关于字节 字节也叫 Byte,是计算机数据的基本存储单位。8bit(位)=1Byte(字节) 1024Byte(字节)=1KB 1024KB=1MB 1024MB=1GB 1024GB=1TB 。在电脑里一个中文字是占两个字节的
特殊整形
类型
描述
uint
32 位操作系统上就是 uint32,64 位操作系统上就是 uint64
int
32 位操作系统上就是 int32,64 位操作系统上就是 int64
uintptr
无符号整型,用于存放一个指针
注意: 在使用 int 和 uint 类型时,不能假定它是 32 位或 64 位的整型,而是考虑 int 和 uint 可能在不同平台上的差异
注意事项: 实际项目中整数类型、切片、 map 的元素数量等都可以用 int 来表示。在涉及 到二进制传输、为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint
1 2 3 4 5 6 7 8 9 package mainimport ( "fmt" ) func main () { var num int64 num = 123 fmt.Printf("值:%v 类型%T" , num, num) }
unsafe.Sizeof unsafe.Sizeof(n1) 是 unsafe 包的一个函数,可以返回 n1 变量占用的字节数
1 2 3 4 5 6 7 8 9 10 package mainimport ( "fmt" "unsafe" ) func main () { var a int8 = 120 fmt.Printf("%T\n" , a) fmt.Println(unsafe.Sizeof(a)) }
3.int 不同长度直接的转换 1 2 3 4 5 6 7 8 9 10 package mainimport ( "fmt" ) func main () { var num1 int8 num1 = 127 num2 := int32 (num1) fmt.Printf("值:%v 类型%T" , num2, num2) }
4.数字字面量语法 Go1.13 版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮 点数的格式定义数字,例如:
v := 0b00101101, 代表二进制的 101101,相当于十进制的 45。
v := 0o377,代表八进制的 377,相当于十进制的 255。
v := 0x1p-2,代表十六进制的 1 除以 2²,也就是 0.25。
而且还允许我们用 _ 来分隔数字,比如说: v := 123_456 等于 123456。
三. 浮点型 Go 语言支持两种浮点型数:float32 和 float64。这两种浮点型数据格式遵循 IEEE754 标准: float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32。float64 的 浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64
打印浮点数时,可以使用 fmt 包配合动词%f,代码如下:
1 2 3 4 5 6 7 8 9 package mainimport ( "fmt" "math" ) func main () { fmt.Printf("%f\n" , math.Pi) fmt.Printf("%.2f\n" , math.Pi) }
1.Go 语言中浮点数默认是 float64 1 2 num := 1.1 fmt.Printf("值:%v--类型:%T" , num, num)
2.Golang 中 float 精度丢失问题 几乎所有的编程语言都有精度丢失这个问题,这是典型的二进制浮点数精度损失问题,在定 长条件下,二进制小数和十进制小数互转可能有精度丢失
1 2 3 4 5 6 7 8 9 d := 1129.6 fmt.Println((d * 100 )) var d float64 = 1129.6 fmt.Println((d * 100 )) m1 := 8.2 m2 := 3.8 fmt.Println(m1 - m2)
可以使用第三方包来解决精度丢失问题
3.golang科学计数法表示浮点类型 1 2 3 4 num8 := 5.1234e2 num9 := 5.1234E2 num10 := 5.1234E-2 fmt.Println("num8=" , num8, "num9=" , num9, "num10=" , num10)
四. 布尔值 Go 语言中以 bool 类型进行声明布尔型数据,布尔型数据只有 true(真)和 false(假)两个 值
注意:
布尔类型变量的默认值为 false
Go 语言中不允许将整型强制转换为布尔型.
布尔型无法参与数值运算,也无法与其他类型进行转换
1 2 3 4 5 6 7 8 9 10 package mainimport ( "fmt" "unsafe" ) func main () { var b = true fmt.Println(b, "占用字节:" , unsafe.Sizeof(b)) }
五. 字符串 Go 语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、 float32、float64 等)一样。 Go 语言里的字符串的内部实现使用 UTF-8 编码。 字符串的值 为双引号(“”)中的内容,可以在 Go 语言的源码中直接添加非 ASCII 码字符,例如
1 2 s1 := "hello" s2 := "你好"
1.字符串转义符
转义符
含义
\r
回车符(返回行首)
\n
换行符(直接跳到下一行的同列位置)
\t
制表符
\‘
单引号
\“
双引号
\\
反斜杠
举个例子,我们要打印一个 Windows 平台下的一个文件路径:
1 2 3 4 5 6 7 package mainimport ( "fmt" ) func main () { fmt.Println("str := \"c:\\Code\\demo\\go.exe\"" ) }
2.多行字符串 Go 语言中要定义一个多行字符串时,就必须使用反引号字符:
1 2 3 4 5 s1 := `第一行 第二行 第三行 ` fmt.Println(s1)
反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出
3.字符串常用操作
len(str)求字符串的长度
1 2 var str = "this is str" fmt.Println(len (str))
拼接字符串
1 2 3 4 var str1 = "你好" var str2 = "golang" fmt.Println(str1 + str2) var str3 = fmt.Sprintf("%v %v" , str1, str2)fmt.Println(str3)
strings.Split 分割字符串
1 2 3 var str = "123-456-789" var arr = strings.Split(str, "-" ) fmt.Println(arr)
拼接字符串
1 2 3 var str = "this is golang" var flag = strings.Contains(str, "golang" )fmt.Println(flag)
判断首字符尾字母是否包含指定字符
1 2 3 4 var str = "this is golang" var flag = strings.HasPrefix(str, "this" )fmt.Println(flag) var str = "this is golang" var flag = strings.HasSuffix(str, "go" )fmt.Println(flag)
判断字符串出现的位置
1 2 3 4 var str = "this is golang" var index = strings.Index(str, "is" ) fmt.Println(index) var str = "this is golang" var index = strings.LastIndex(str, "is" ) fmt.Println(index)
Join 拼接字符串
1 2 3 var str = "123-456-789" var arr = strings.Split(str, "-" )var str2 = strings.Join(arr, "*" )fmt.Println(str2)
六. byte 和 rune 类型 组成每个字符串的元素叫做“字符”,可以通过遍历字符串元素获得字符。 字符用单引号(’) 包裹起来,如:
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { a := 'a' b := '0' fmt.Println(a) fmt.Println(b) fmt.Printf("%c--%c" , a, b) }
字节(byte) :是计算机中 数据处理 的基本单位,习惯上用大写 B 来表示,1B(byte,字节) = 8bit(位
字符 :是指计算机中使用的字母、数字、字和符号
一个汉字占用 3 个字节 一个字母占用一个字节
1 2 3 4 a := "m" fmt.Println(len (a)) b := "张" fmt.Println(len (b))
Go 语言的字符有uint8和rune两种:
uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
rune 类型,代表一个 UTF-8 字符。
当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型实际是一个 int32。 Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可 以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { s := "hello 张三" for i := 0 ; i < len (s); i++ { fmt.Printf("%v(%c) " , s[i], s[i]) } fmt.Println() for _, r := range s { fmt.Printf("%v(%c) " , r, r) } fmt.Println() }
输出: 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 229(å) 188(¼) 160( ) 228(ä) 184(¸) 137() 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 24352(张) 19977(三)
因为 UTF8 编码下一个中文汉字由 3 个字节组成,所以我们不能简单的按照字节去遍历一个 包含中文的字符串,否则就会出现上面输出中第一行的结果。 字符串底层是一个 byte 数组,所以可以和[]byte 类型相互转换。字符串是不能修改的 字符 串是由 byte 字节组成,所以字符串的长度是 byte 字节的长度。 rune 类型用来表示 utf8 字 符,一个 rune 字符由一个或多个 byte 组成。
rune 类型实际是一个 int32 1 2 c3 := "营" c4 := '营' fmt.Printf("C3 的类型%T--C4 的类型%T" , c3, c4)
七. 修改字符串 要修改字符串,需要先将其转换成[]rune 或[]byte,完成后再转换为 string。无论哪种转换, 都会重新分配内存,并复制字节数组
1 2 3 4 5 6 7 8 9 10 11 12 func changeString () { s1 := "big" byteS1 := []byte (s1) byteS1[0 ] = 'p' fmt.Println(string (byteS1)) s2 := "白萝卜" runeS2 := []rune (s2) runeS2[0 ] = '红' fmt.Println(string (runeS2)) }
数据类型转换
Go 语言中只有强制类型转换,没有隐式类型转换
数值类型之间的相互转换 数值类型包括:整形和浮点型
整形转换 1 2 3 4 5 6 7 8 package mainimport "fmt" func main () { var a int8 = 20 var b int16 = 40 var c = int16 (a) + b fmt.Printf("值:%v--类型%T" , c, c) }
整形,浮点型转换 1 2 3 4 5 6 7 8 package mainimport "fmt" func main () { var a float32 = 3.2 var b int16 = 6 var c = a + float32 (b fmt.Printf("值:%v--类型%T" , c, c) }
转换的时候建议从低位转换成高位,高位转换成低位的时候如果转换不成功就会溢出,和我 们想的结果不一样比如:
1 2 3 4 5 6 package mainfunc main () {var a int16 = 129 var b = int8 (a) println ("b=" , b) }
比如计算直角三角形的斜边长时使用 math 包的 Sqrt()函数,该函数接收的是 float64 类型的 参数,而变量 a 和 b 都是 int 类型的,这个时候就需要将 a 和 b 强制类型转换为 float64 类型
1 2 3 4 5 var a, b = 3 , 4 var c int c = int (math.Sqrt(float64 (a*a + b*b))) fmt.Println(c)
其他类型转换为String类型 sprintf 把其他类型转换成 string 类型 注意:sprintf 使用中需要注意转换的格式
int 为%d
float 为%f
bool 为%t
byte 为%c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport "fmt" func main () { var i int = 20 var f float64 = 12.456 var t bool = true var b byte = 'a' var strs string var strs string strs = fmt.Sprintf("%d" , i) fmt.Printf("str type %T ,strs=%v \n" , strs, strs) strs = fmt.Sprintf("%f" , f) fmt.Printf("str type %T ,strs=%v \n" , strs, strs) strs = fmt.Sprintf("%t" , t) fmt.Printf("str type %T ,strs=%v \n" , strs, strs) strs = fmt.Sprintf("%c" , b) fmt.Printf("str type %T ,strs=%v \n" , strs, strs) }
使用 strconv 包里面的几种转换方法进行转换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "strconv" ) func main () { var num1 int = 20 s1 := strconv.Itoa(num1) fmt.Printf("str type %T ,strs=%v \n" , s1, s1) var num2 float64 = 20.113123 s2 := strconv.FormatFloat(num2, 'f' , 2 , 64 ) fmt.Printf("str type %T ,strs=%v \n" , s2, s2) s3 := strconv.FormatBool(true ) fmt.Printf("str type %T ,strs=%v \n" , s3, s3) var num3 int64 = 20 s4 := strconv.FormatInt(num3, 10 ) fmt.Printf("类型 %T ,strs=%v \n" , s4, s4) }
String 类型转换成数值类型
以下均使用strconv包
string 类型转换成 int 类型 1 2 3 var s = "1234" i64, _ := strconv.ParseInt(s, 10 , 64 ) fmt.Printf("值:%v 类型:%T" , i64, i64)
string 类型转换成 float 类型 1 2 3 4 str := "3.1415926535" v1, _ := strconv.ParseFloat(str, 32 ) v2, _ := strconv.ParseFloat(str, 64 ) fmt.Printf("值:%v 类型:%T\n" , v1, v1) fmt.Printf("值:%v 类型:%T" , v2,
string 类型转换成 bool 类型(意义不大) 1 2 b, _ := strconv.ParseBool("true" ) fmt.Printf("值:%v 类型:%T" , b, b)
string 转字符 1 2 3 4 5 s := "hello 张三" for _, r := range s { fmt.Printf("%v(%c) " , r, r) } fmt.Println()
数值类型没法和 bool 类型进行转换 注意 :在 go 语言中数值类型没法直接转换成 bool 类型 bool 类型也没法直接转换成数值类型
运算符 算数运算符
注意: ++(自增)和–(自减)在 Go 语言中是单独的语句,并不是运算符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" ) func main () { fmt.Println("10+3=" , 10 +3 ) fmt.Println("10-3=" , 10 -3 ) fmt.Println("10*3=" , 10 *3 ) fmt.Println("10/3=" , 10 /3 ) fmt.Println("10.0/3=" , 10.0 /3 ) fmt.Println("10%3=" , 10 %3 ) fmt.Println("-10%3=" , -10 %3 ) fmt.Println("10%-3=" , 10 %-3 ) fmt.Println("-10%-3=" , -10 %-3 ) }
**注意: **在 golang 中,++ 和 – 只能独立使用 错误写法 如下:
1 2 3 4 var i int = 8 var a int a = i++ a = i--
**注意: **在 golang 中没有前++ 错误写法 如下
1 2 3 4 var i int = 1 ++i --i fmt.Println("i=" , i
++ –正确写法:
1 2 3 var i int = 1 i++ fmt.Println("i=" , i)
关系运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" ) func main () { var n1 int = 9 var n2 int = 8 fmt.Println(n1 == n2) fmt.Println(n1 != n2) fmt.Println(n1 > n2) fmt.Println(n1 >= n2) fmt.Println(n1 < n2) fmt.Println(n1 <= n2) flag := n1 > n2 fmt.Println("flag=" , flag) }
逻辑运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport ( "fmt" ) func main () { var age int = 40 if age > 30 && age < 50 { fmt.Println("ok1" ) } if age > 30 && age < 40 { fmt.Println("ok2" ) } if age > 30 || age < 50 { fmt.Println("ok3" ) } if age > 30 || age < 40 { fmt.Println("ok4" ) } if age > 30 { fmt.Println("ok5" ) } if !(age > 30 ) { fmt.Println("ok6" ) } }
逻辑运算符短路演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" ) func test () bool { fmt.Println("test..." ) return true } func main () { var i int = 10 if i < 9 && test() { fmt.Println("ok..." ) } if i > 9 || test() { fmt.Println("hello..." ) } }
赋值运算符
crispcookie: 这章不是有手就行
选择循环 if else(分支结构) 1.if条件判断基本写法 Go 语言中 if 条件判断的格式如下:
1 2 3 4 5 6 7 if 表达式 1 {分支 1 } else if 表达式 2 { 分支 2 } else { 分支 3 }
当表达式 1 的结果为 true 时,执行分支 1,否则判断表达式 2,如果满足则执行分支2,都 不满足时,则执行分支3, if 判断中的 else if 和 else 都是可选的,可以根据实际需要进行选择
注意: Go 语言规定与 if 匹配的 左括号{必须与 if 和表达式放在同一行 ,{放在其他位置会触发编译错误。 同理,与 else 匹配的{也必须与 else 写在同一行,else 也必须与上一个 if 或 else if 右边的大括号在同一行。
例如:
1 2 3 4 5 6 7 8 9 10 func ifDemo1 () { score := 65 if score >= 90 { fmt.Println("A" ) } else if score > 75 { fmt.Println("B" ) } else { fmt.Println("C" ) } }
if 条件判断特殊写法 if 条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句 ,再根据变量值进行判断,举个例子:
1 2 3 4 5 6 7 8 if score := 56 ; score >= 90 { fmt.Println("A" ) } else if score > 75 { fmt.Println("B" ) } else { fmt.Println("C" ) }
for循环 Go 语言中的所有循环类型均可以使用 for 关键字来完成。
for 循环的基本格式: for 初始语句;条件表达式;结束语句{
循环体语句
}
条件表达式返回 true 时循环体不停地进行循环,直到条件表达式返回 false 时自动退出循环。
1 2 3 for i := 0 ; i < 10 ; i++ { fmt.Println(i) }
for 循环的初始语句可以被忽略,但是初始语句后的分号必须要写,例如:
1 2 3 4 i := 0 for ; i < 10 ; i++ {fmt.Println(i) }
for 循环的初始语句和结束语句都可以省略,例如:
1 2 3 4 5 i := 0 for i < 10 { fmt.Println(i) i++ }
注意: Go 语言中是没有 while 语句的,我们可以通过 for 代替
for 无限循环 for {
循环体语句
}
for 循环可以通过 break、goto、return、panic 语句强制退出循环。
1 2 3 4 5 6 7 8 9 10 k := 1 for { if k <= 10 { fmt.Println("ok~~" , k) } else { break } k++ }
for range(键值循环) Go 语言中可以使用 for range 遍历数组、切片、字符串、map 及通道(channel)。 通过 for range 遍历的返回值有以下规律
数组、切片、字符串返回索引和值
map 返回键和值
通道(channel)只返回通道内的值
1 2 3 4 5 6 7 8 9 str := "abc 上海" for index, val := range str { fmt.Printf("index=%d, val=%c \n" , index, val) } str := "abc 上海" for _, val := range str { fmt.Printf("val=%c \n" , val) }
switch case 使用 switch 语句可方便的对大量的值进行条件判断
Go 语言规定每个 switch 只能有一个 default 分支
switch语法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 extname := ".a" switch extname { case ".html" : fmt.Println("text/html" ) break case ".css" : fmt.Println("text/css" ) break case ".js" : fmt.Println("text/javascript" ) break default : fmt.Println("格式错误" ) break }
Go 语言中每个 case 语句中可以不写 break,不加 break 也不会出现穿透 的现象 如下例子
1 2 3 4 5 6 7 8 9 10 11 12 extname := ".a" switch extname { case ".html" : fmt.Println("text/html" ) case ".css" : fmt.Println("text/css" ) case ".js" : fmt.Println("text/javascript" ) default : fmt.Println("格式错误" ) }
一个分支可以有多个值,多个 case 值中间使用英文逗号分隔
1 2 3 4 5 6 7 8 9 n := 2 switch n { case 1 , 3 , 5 , 7 , 9 : fmt.Println("奇数" ) case 2 , 4 , 6 , 8 : fmt.Println("偶数" ) default : fmt.Println(n) }
另一种写法:
1 2 3 4 5 6 7 8 9 switch n := 7 ; n { case 1 , 3 , 5 , 7 , 9 : fmt.Println("奇数" ) case 2 , 4 , 6 , 8 : fmt.Println("偶数" ) default : fmt.Println(n) }
注意: 上面两种写法的作用域
分支还可以使用表达式,这时候 switch 语句后面不需要再跟判断变量 。例如:
1 2 3 4 5 6 7 8 9 10 11 age := 56 switch { case age < 25 : fmt.Println("好好学习吧!" ) case age > 25 && age <= 60 : fmt.Println("好好工作吧!" ) case age > 60 : fmt.Println("好好享受吧!" ) default : fmt.Println("活着真好!" ) }
2. switch 的穿透 fallthrought fallthrough语法可以执行满足条件的 case 的下一个 case,是为了兼容 C 语言中的 case 设计 的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func switchDemo5 () { s := "a" switch { case s == "a" : fmt.Println("a" ) fallthrough case s == "b" : fmt.Println("b" ) case s == "c" : fmt.Println("c" ) default : fmt.Println("..." ) } }
fallthrough默认只能穿透一层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var num int = 10 switch num { case 10 : fmt.Println("ok1" ) fallthrough case 20 : fmt.Println("ok2" ) fallthrough case 30 : fmt.Println("ok3" ) default : fmt.Println("没有匹配到.." ) }
五. break(跳出循环) Go 语言中 break 语句用于以下几个方面
用于循环语句中跳出循环,并开始执行循环之后的语句。
break 在 switch(开关语句)中在执行一条 case
在多重循环中,可以用标号 label 标出想 break 的循环。
1. switch(开关语句)中在执行一条 case后跳出语句的作用。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 extname := ".a" switch extname { case ".html" : fmt.Println("text/html" ) break case ".css" : fmt.Println("text/css" ) break case ".js" : fmt.Println("text/javascript" ) break default : fmt.Println("格式错误" ) break }
2. for 循环中默认 break 只能跳出一层循环 1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" func main () { for i := 0 ; i < 2 ; for j := 0 ; j < 10 ; j++ { if j == 2 { break } fmt.Println("i j 的值" , i, "-" , j) } } }
1 2 3 4 5 6 7 8 9 k := 1 for { if k <= 10 { fmt.Println("ok~~" , k) } else { break } k++ }
3. 在多重循环中,可以用标号 label 标出想 break 的循环 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { lable2: for i := 0 ; i < 2 ; i++ { for j := 0 ; j < 10 ; j++ { if j == 2 { break lable2 } fmt.Println("i j 的值" , i, "-" , j) } } }
continue(继续下次循环) continue 语句可以结束当前循环,开始下一次的循环迭代过程 仅限 在 for 循环内使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport "fmt" func main () { for i := 0 ; i < 2 ; i++ { for j := 0 ; j < 4 ; j++ { if j == 2 { continue } fmt.Println("i j 的值" , i, "-" , j) } } }
在 continue 语句后添加here标签时,表示开始标签对应的循环 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import "fmt" func main () { here: for i := 0 ; i < 2 ; i++ { for j := 0 ; j < 4 ; j++ { if j == 2 { continue here } fmt.Println("i j 的值" , i, "-" , j) } } }
goto(跳转到指定标签) goto 语句通过标签 进行代码间的无条件跳转。goto 语句可以在快速跳出循环、避免重复退 出上有一定的帮助。Go 语言中使用 goto 语句能简化一些代码的实现过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport "fmt" func main () { var n int = 30 fmt.Println("ok1" ) if n > 20 { goto label1 } fmt.Println("ok2" ) fmt.Println("ok3" ) fmt.Println("ok4" ) label1: fmt.Println("ok5" ) fmt.Println("ok6" ) fmt.Println("ok7" ) }
数组 Array(数组)的介绍 数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素 (element),这种类型可以是任意的原始类型,比如 int、string 等,也可以是用户自定义的 类型。
一个数组包含的元素个数被称为数组的长度。在 Golang 中数组是一个长度固定的数 据类型,数组的长度是类型的一部分 ,也就是说 [5]int 和 [10]int 是两个不同的类型 。
Golang 中数组的另一个特点是占用内存的连续性,也就是说数组中的元素是被分配到连续的内存地 址中的,因而索引数组元素的速度非常快。
和数组对应的类型是 Slice(切片),Slice 是可以增长和收缩的动态序列,功能也更灵活,但是想要理解 slice 工作原理的话需要先理解数组
数组的基本用法:
1 2 3 4 5 6 7 var a [3 ]int var b [3 ]int b[0 ] = 80 b[1 ] = 100 b[2 ] = 96
数组定义
比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。**[5]int 和[4]int 是不同的类型**
1 2 3 var a [3 ]int var b [4 ]int a = b
数组可以通过下标进行访问,下标是从 0 开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会 panic(异常)。
数组的初始化 使用初始化列表 1 2 3 4 5 6 7 8 func main () { var testArray [3 ]int var numArray = [3 ]int {1 , 2 } var cityArray = [3 ]string {"北京" , "上海" , "深圳" } fmt.Println(testArray) fmt.Println(numArray) fmt.Println(cityArray) }
根据初始值的个数自行推断数组的长度(不指明数组长度) 1 2 3 4 5 6 7 8 9 10 func main () { var testArray [3 ]int var numArray = [...]int {1 , 2 } var cityArray = [...]string {"北京" , "上海" , "深圳" } fmt.Println(testArray) fmt.Println(numArray) fmt.Printf("type of numArray:%T\n" , numArray) fmt.Println(cityArray) fmt.Printf("type of cityArray:%T\n" , cityArray) }
指定索引值的方式来初始化数组 1 2 3 4 5 func main () { a := [...]int {1 : 1 , 3 : 5 } fmt.Println(a) fmt.Printf("type of a:%T\n" , a) }
数组的遍历 for循环遍历 1 2 3 4 5 6 7 func main () { var a = [...]string {"北京" , "上海" , "深圳" } for i := 0 ; i < len (a); i++ { fmt.Println(a[i]) } }
for range遍历 1 2 3 4 5 6 func main () { var a = [...]string {"北京" , "上海" , "深圳" } for index, value := range a { fmt.Println(index, value) } }
数组是值类型 数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func modifyArray (x [3]int ) { x[0 ] = 100 } func modifyArray2 (x [3][2]int ) { x[2 ][0 ] = 100 } func main () { a := [3 ]int {10 , 20 , 30 } modifyArray(a) fmt.Println(a) b := [3 ][2 ]int {{1 , 1 }, {1 , 1 }, {1 , 1 },} modifyArray2(b) fmt.Println(b) }
注意:
数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的
[n]*T 表示指针数组, [n]*T 表示数组指针
多维数组 Go 语言是支持多维数组的,这里以二维数组为例
1 2 var 数组变量名 [元素数量][元素数量]var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
二维数组的定义 1 2 3 4 5 6 7 func main () { a := [3 ][2 ]string { {"北京" , "上海" }, {"广州" , "深圳" }, {"成都" , "重庆" }, } fmt.Println(a) fmt.Println(a[2 ][1 ]) }
二位数组的遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { a := [3 ][2 ]string { {"北京" , "上海" }, {"广州" , "深圳" }, {"成都" , "重庆" }, } for _, v1 := range a { for _, v2 := range v1 { fmt.Printf("%s\t" , v2) } fmt.Println() } }
注意 : 多维数组只有第一层可以使用…来让编译器推导数组长度。例如
1 2 3 4 5 a := [...][2 ]string { {"北京" , "上海" }, {"广州" , "深圳" }, {"成都" , "重庆" }, } {"北京" , "上海" }, {"广州" , "深圳" }, {"成都" , "重庆" }, }
就是说只有第一个[]内能写…来推导长度
切片 Slice(切片)的介绍 因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainfunc arraySum (x [4]int ) int { sum := 0 for _, v := range x { sum = sum + v } return sum } func main () { a := [4 ]int {1 , 2 , 3 , 4 } println (arraySum(a)) b := [5 ]int {1 , 2 , 3 , 4 , 5 } println (arraySum(b)) }
这个求和函数只能接受[4]int 类型,其他的都不支持。所以传入长度为 5 的数组的时候就会报错
切片的定义 切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。 它非常灵活,支持自动扩容。
切片是一个引用类型 ,它的内部结构包含地址 、长度 和容量 。 声明切片类型的基本语法如下:
1 2 var 切片变量名 []切片中的元素类型var name []T
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { var a []string var b = []int {} var c = []bool {false , true } var d = []bool {false , true } fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(a == nil ) fmt.Println(b == nil ) fmt.Println(c == nil ) fmt.Println(c == d) }
关于nil 当你声明了一个变量 , 但却还并没有赋值时 , golang 中会自动给你的变量赋值一个默认零 值。这是每种类型对应的零值, 而nil则是指针,切片,map,channel,函数,接口 的零值
1 2 3 4 5 6 7 8 bool -> false numbers -> 0 string -> "" pointers -> nil slices -> nil maps -> nil channels -> nil functions -> nil interfaces -> nil
切片的循环遍历 切片的遍历和数组的遍历是一样的,同样有两种方法
for循环遍历 1 2 3 4 5 6 var a = []string {"北京" , "上海" , "深圳" }for i := 0 ; i < len (a); i++ { fmt.Println(a[i]) }
for range遍历 1 2 3 4 5 var a = []string {"北京" , "上海" , "深圳" }for index, value := range a { fmt.Println(index, value) }
基于数组定义切片 由于切片的底层就是一个数组,所以我们可以基于数组定义切片(很类似python)
定义切片 1 2 3 4 5 6 7 8 9 10 11 12 func main () {a := [5 ]int {55 , 56 , 57 , 58 , 59 } b := a[1 :4 ] fmt.Println(b) fmt.Printf("type of b:%T\n" , b) } 还支持如下方式: c := a[1 :] d := a[:4 ] e := a[:]
定义切片的切片 除了基于数组得到切片,我们还可以通过切片来得到切片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () {a := [...]string {"北京" , "上海" , "广州" , "深圳" , "成都" , "重庆" } fmt.Printf("a:%v type:%T len:%d cap:%d\n" , a, a, len (a), cap (a)) b := a[1 :3 ] fmt.Printf("b:%v type:%T len:%d cap:%d\n" , b, b, len (b), cap (b)) c := b[1 :5 ] fmt.Printf("c:%v type:%T len:%d cap:%d\n" , c, c, len (c), cap (c)) }
**注意:**对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误
关于切片的长度len和容量cap 切片拥有自己的长度和容量,我们可以通过使用内置的 len()函数求长度,使用内置的 cap() 函数求切片的容量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 s := []int {2 , 3 , 5 , 7 , 11 , 13 } fmt.Println(s) fmt.Printf("长度:%v 容量 %v\n" , len (s), cap (s)) c := s[:2 ] fmt.Println(c) fmt.Printf("长度:%v 容量 %v\n" , len (c), cap (c)) d := s[1 :3 ] fmt.Println(d) fmt.Printf("长度:%v 容量 %v" , len (d),cap (d))
上面案例中
第一个printf : 底层数组的长度和容量, 均为6
第二个printf : c:=s[:2]后输出:[2 3], 左指针 s[0],右指针 s[2] , 所以长度为 2,容量为 6
第三个printf: d := s[1:3]后输出:[3 5], 左指针 s[1],右指针 s[3] , 所以长度为 2,容量为 5
切片的本质 切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针 、切片的长度 (len) 和切片的容量 (cap)
举个例子,现在有一个数组 a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片 s1 := a[:5],相应示意图如下
切片 s2 := a[3:6],相应示意图如下
使用make()函数构造切片 上面都是基于数组来创建的切片,如果需要动态 的创建一个切片我们就需要使用内置的make()函数,格式如下:
其中:
T:切片的元素类型
size:切片中元素的数量
cap:切片的容量
举个例子
1 2 3 4 5 6 7 func main () { a := make ([]int , 2 , 10 ) fmt.Println(a) fmt.Println(len (a)) fmt.Println(cap (a)) }
上面代码中 a 的内部存储空间已经分配了 10 个,但实际上只用了2个, 容量并不会影响当前元素的个数,所以 len(a)返回 2cap(a)则返回该切片的容量
切片不能直接用于比较 切片之间是不能比较 的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。
切片唯一合法的比较操作是和 nil 比较 。 一个 nil 值的切片并没有底层数组,一个 nil 值的切片的长度和容量都是 0。
但是我们不能说一个长度和容量都是 0 的切片一定是nil, 例如下面的示例
1 2 3 var s1 []int s2 := []int {} s3 := make ([]int , 0 )
所以要判断一个切片是否是空的,要是用 len(s) == 0 来判断,不应该使用 s == nil 来判断
切片是引用数据类型 切片是引用数据类型, 拷贝前后两个变量共享底层数组 ,对一个切片的修改会影响另一个切片的内容 ,这点需要特别注意。
1 2 3 4 5 6 7 func main () { s1 := make ([]int , 3 ) s2 := s1 s2[0 ] = 100 fmt.Println(s1) fmt.Println(s2) }
切片的一些操作 append()方法为切片添加元素 Go 语言的内建函数 append()可以为切片动态添加元素,每个切片会指向一个底层数组 ,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在 append()函数调用时,所以我们通常都需要用原变量接收 append 函数的返回值 (返回值是经过append操作后的原素组)
给切片追加元素的错误 写法:
1 2 3 4 s3 := []int {1 , 2 , 3 , 5 , 6 , 7 } s3[6 ] = 8 fmt.Println(s3)
append()方法为切片追加元素:
1 2 3 4 5 6 7 8 9 func main () { var numSlice []int for i := 0 ; i < 10 ; i++ { numSlice = append (numSlice, i) fmt.Printf("%v len:%d cap:%d ptr:%p\n" , numSlice, len (numSlice), cap (numSlic e), numSlice) } }
上面案例的printf输出:
1 2 3 4 5 6 7 8 9 10 [0 ] len :1 cap :1 ptr:0xc0000a8000 [0 1 ] len :2 cap :2 ptr:0xc0000a8040 [0 1 2 ] len :3 cap :4 ptr:0xc0000b2020 [0 1 2 3 ] len :4 cap :4 ptr:0xc0000b2020 [0 1 2 3 4 ] len :5 cap :8 ptr:0xc0000b6000 [0 1 2 3 4 5 ] len :6 cap :8 ptr:0xc0000b6000 [0 1 2 3 4 5 6 ] len :7 cap :8 ptr:0xc0000b6000 [0 1 2 3 4 5 6 7 ] len :8 cap :8 ptr:0xc0000b6000 [0 1 2 3 4 5 6 7 8 ] len :9 cap :16 ptr:0xc0000b8000 [0 1 2 3 4 5 6 7 8 9 ] len :10 cap :16 ptr:0xc0000b8000
从上面的结果可以看出:
append()函数将元素追加到切片的最后并返回该切片
切片 numSlice 的容量按照 1,2,4,8,16 这样的规则自动进行扩容,每次扩容后都是 扩容前的 2 倍
切片的扩容策略:
首先判断,如果新申请容量(cap)大于 2 倍 的旧容量(old.cap),最终容量(newcap) 就是新申请的容量(cap)
否则判断,如果旧切片的长度小于 1024 ,则最终容量(newcap)就是旧容量(old.cap)的两 倍,即(newcap=doublecap)
否则判断,如果旧切片长度大于等于 1024 ,则最终容量(newcap)从旧容量(old.cap) 开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量 (newcap)大于等于新申请的容量(cap),即(newcap >= cap)
如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如 int 和 string 类型的处理方式就不一样
append()函数支持一次性追加多个元素
1 2 3 4 5 6 7 8 9 var citySlice []string citySlice = append (citySlice, "北京" ) citySlice = append (citySlice, "上海" , "广州" , "深圳" ) a := []string {"成都" , "重庆" } citySlice = append (citySlice, a...) fmt.Println(citySlice)
append()函数支持追加(多个)切片
当追加切片时, 最后一个追加的元素要加**…**
1 2 3 4 s1 := []int {100 , 200 , 300 } s2 := []int {400 , 500 , 600 } s3 := append (s1, s2...) fmt.Println(s3)
使用copy()函数赋值切片 先来看一个问题
1 2 3 4 5 6 7 8 9 func main () { a := []int {1 , 2 , 3 , 4 , 5 } b := a fmt.Println(a) fmt.Println(b) b[0 ] = 1000 fmt.Println(a) fmt.Println(b) }
由于切片是引用类型 ,所以 a 和 b 其实都指向了同一块内存地址。修改 b 的同时 a 的值也会发生变化
Go 语言内建的 copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy() 函数的使用格式如下
1 copy (destSlice, srcSlice []T)
其中:
srcSlice: 数据来源切片
destSlice: 目标切片
T: 切片中的元素类型
代码示例
1 2 3 4 5 6 7 8 9 10 11 func main () { a := []int {1 , 2 , 3 , 4 , 5 } c := make ([]int , 5 , 5 ) copy (c, a) fmt.Println(a) fmt.Println(c) c[0 ] = 1000 fmt.Println(a) fmt.Println(c) }
切片删除元素 Go 语言中
并没有
删除切片元素的专用方法(这是否…),我们可以使用切片本身的”特性”来删除元素。 代码如下
1 2 3 4 5 6 7 func main () { a := []int {30 , 31 , 32 , 33 , 34 , 35 , 36 , 37 } a = append (a[:2 ], a[3 :]...) fmt.Println(a) }
总结一下就是:要从切片 a 中删除索引为 index 的元素,操作方法是 a = append(a[:index], a[index+1:]…)
Map Map(集合)的介绍 map 是一种无序的基于 key-value 的数据结构,Go 语言中的 map 是引用类型,必须初始化才能使用。 Go 语言中 map 的定义语法如下
其中:
KeyType:表示键的类型
ValueType:表示键对应的值的类型
map 类型的变量默认初始值为 nil,需要使用 make()函数来分配内存 。语法为
1 make (map [KeyType]ValueType, [cap ])
其中cap表示map的容量, 该参数不是必须的
注意: :获取 map 的容量不能使用 cap, cap 返回的是数组切片分配的空间大小, 根本不能用于 map。要获取 map 的容量,可以用 len 函数
具体定义过程示例:
1 scoreMap := make (map [string ]int )
map的基本使用 map 中的数据都是成对出现的,map 的基本使用示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { scoreMap := make (map [string ]int , 8 ) scoreMap["张三" ] = 90 scoreMap["小明" ] = 100 fmt.Println(scoreMap) fmt.Println(scoreMap["小明" ]) fmt.Printf("type of a:%T\n" , scoreMap) }
map也支持在声明时填充元素, 例如:
1 2 3 4 5 6 7 func main () { userInfo := map [string ]string { "username" : "crispcookie" , "password" : "123456" , } fmt.Println(userInfo) }
判断某个键是否存在 Go 语言中有个判断 map 中键是否存在的特殊写法(“写法”,这也太随性了),格式如下:
1 value, ok := map 对象[key]
示例:
1 2 3 4 5 6 7 8 9 10 11 12 func main () { scoreMap := make (map [string ]int ) scoreMap["张三" ] = 9 scoreMap["小明" ] = 100 v, ok := scoreMap["张三" ] if ok { fmt.Println(v) } else { fmt.Println("查无此人" ) } }
map的遍历 Go 语言中使用 for range 遍历 map
1 2 3 4 5 6 7 8 9 func main () { scoreMap := make (map [string ]int ) scoreMap["张三" ] = 90 scoreMap["小明" ] = 100 scoreMap["李四" ] = 60 for k, v := range scoreMap { fmt.Println(k, v) } }
但我们只想遍历 key 的时候,可以按下面的写法
1 2 3 4 5 6 7 8 9 func main () { scoreMap := make (map [string ]int ) scoreMap["张三" ] = 90 scoreMap["小明" ] = 100 scoreMap["娜扎" ] = 60 for k := range scoreMap { fmt.Println(k) } }
注意: 遍历 map 时的元素顺序与添加键值对的顺序无关(C++的底层时红黑树,按照键来进行排序,本人暂时还没研究go里的元素顺序是声明规则)
使用delete()函数删除键值对 使用 delete()内建函数从 map 中删除一组键值对,delete()函数的格式如下:
其中:
map 对象:表示要删除键值对的 map 对象
key:表示要删除的键值对的键
示例:
1 2 3 4 5 6 7 8 9 10 func main () { scoreMap := make (map [string ]int ) scoreMap["张三" ] = 90 scoreMap["小明" ] = 100 scoreMap["李四" ] = 60 delete (scoreMap, "小明" ) for k,v := range scoreMap{ fmt.Println(k, v) } }
元素为map类型的切片 下面的代码演示了切片中的元素为 map 类型时的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { var mapSlice = make ([]map [string ]string , 3 ) for index, value := range mapSlice { fmt.Printf("index:%d value:%v\n" , index, value) } fmt.Println("after init" ) mapSlice[0 ] = make (map [string ]string , 10 ) mapSlice[0 ]["name" ] = "小王子" mapSlice[0 ]["password" ] = "123456" mapSlice[0 ]["address" ] = "海淀区" for index, value := range mapSlice { fmt.Printf("index:%d value:%v\n" , index, value) } }
值为切片类型的map 下面的代码演示了 map 中值为切片类型的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { var sliceMap = make (map [string ][]string , 3 ) fmt.Println(sliceMap) fmt.Println("after init" ) key := "中国" value, ok := sliceMap[key] if !ok { value = make ([]string , 0 , 2 ) } value = append (value, "北京" , "上海" ) sliceMap[key] = value fmt.Println(sliceMap) }
函数 函数的定义 函数是组织好的、可重复使用的、用于执行指定任务的代码块。本节介绍了 Go 语言中函数的相关内容
Go 语言中支持:函数 、匿名函数 和闭包 Go 语言中定义函数使用 func 关键字,具体格式如下:
1 2 3 func 函数名(参数) (返回值){ 函数体 }
其中:
函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字 。在同一个包内,函数名也称不能重名(包的概念详见后文)
参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔
返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹, 并用,分隔
函数体:实现指定功能的代码块
示例:
1 2 3 func intSum (x int , y int ) int { return x + y }
函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数
1 2 3 func sayHello () { fmt.Println("Hello crispcookie" ) }
函数的调用 定义了函数之后,我们可以通过函数名()的方式调用函数。 例如我们调用上面定义的两个函数,代码如下
1 2 3 4 5 func main () { sayHello() ret := intSum(10 , 20 ) fmt.Println(ret) }
注意,调用有返回值的函数时,可以不接收其返回值
函数参数 类型简写 函数的参数中如果相邻变量的类型相同,则可以省略类型,例如
1 2 3 func intSum (x, y int ) int { return x + y }
上面的代码中,intSum 函数有两个参数,这两个参数的类型均为 int,因此可以省略 x 的类型,因为 y 后面有类型说明,x 参数也是该类型
可变参数 可变参数是指函数的参数数量不固定 。Go 语言中的可变参数通过在参数名后加…来标识
注意: 可变参数通常要作为函数的最后一个参数
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func intSum2 (x ...int ) int { fmt.Println(x) sum := 0 for _, v := range x { sum = sum + v } return sum } func main () { ret1 := intSum2() ret2 := intSum2(10 ) ret3 := intSum2(10 , 20 ) ret4 := intSum2(10 , 20 , 30 ) fmt.Println(ret1, ret2, ret3, ret4) }
固定参数搭配可变参数使用时,可变参数要放在固定参数的后面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func intSum3 (x int , y ...int ) int { fmt.Println(x, y) sum := x for _, v := range y { sum = sum + v } return sum } func main () { ret5 := intSum3(100 ) ret6 := intSum3(100 , 10 ) ret7 := intSum3(100 , 10 , 20 ) ret8 := intSum3(100 , 10 , 20 , 30 ) fmt.Println(ret5, ret6, ret7, ret8) }
本质上,函数的可变参数是通过切片 来实现的
函数返回值 Go 语言中通过 return 关键字向外输出返回值
函数多返回值 Go 语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来
示例:
1 2 3 4 5 func calc (x, y int ) (int , int ) { sum := x + y sub := x - y return sum, sub }
返回值命名 函数定义时可以给返回值命名,并在函数体中直接使用这些变量 ,最后通过 return 关键字返回
示例:
1 2 3 4 5 func calc (x, y int ) (sum, sub int ) { sum = x + y sub = x - y return }
函数变量作用域 全局变量 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量.
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" var num int64 = 10 func testGlobalVar () { fmt.Printf("num=%d\n" , num) } func main () { testGlobalVar() }
局部变量 局部变量是函数内部定义的变量, 函数内定义的变量无法在该函数外使用
函数内定义的变量无法在该函数外使用
1 2 3 4 5 6 7 8 9 func testLocalVar () { var x int64 = 100 fmt.Printf("x=%d\n" , x) } func main () { testLocalVar() fmt.Println(x) }
如果局部变量和全局变量重名,优先访问局部变量
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" var num int64 = 10 func testNum () { num := 100 fmt.Printf("num=%d\n" , num) } func main () { testNum() }
变量只作用于当前的{}内,在子{}中可以使用,通常我们会在 if 条件判断、for 循环、switch 语句 上使用这种定义变量的方式
1 2 3 4 5 6 7 8 func testLocalVar2 (x, y int ) { fmt.Println(x, y) if x > 0 { z := 100 fmt.Println(z) } }
函数类型与变量 定义函数类型 我们可以使用 type 关键字来定义一个函数类型,具体格式如下:
1 type calculation func (int , int ) int
上面语句定义了一个 calculation 类型,它是一种函数类型,这种函数接收两个 int 类型的参 数并且返回一个 int 类型的返回值。 简单来说,凡是满足这个条件的函数都是 calculation 类型的函数,例如下面的 add 和 sub 是 calculation 类型
1 2 3 4 5 6 7 func add (x, y int ) int { return x + y } func sub (x, y int ) int { return x - y }
add 和 sub 都能赋值给 calculation 类型的变量
1 2 3 var c calculationc=add c=sub
函数类型变量 我们可以声明函数类型的变量并且为该变量赋值, 让函数作为变量值
1 2 3 4 5 6 7 8 9 func main () { var c calculation c = add fmt.Printf("type of c:%T\n" , c) fmt.Println(c(1 , 2 )) f := add fmt.Printf("type of f:%T\n" , f) fmt.Println(f(10 , 20 )) }
高阶函数 高阶函数分为函数作为参数 和函数作为返回值 两部分
函数可以作为参数 示例:
1 2 3 4 5 6 7 8 9 10 11 12 func add (x, y int ) int { return x + y } func calc (x, y int , op func (int , int ) int ) int { return op(x, y) } func main () { ret2 := calc(10 , 20 , add) fmt.Println(ret2) }
让人两眼一黑两眼一亮的语法
2. 函数作为返回值 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import ( "fmt" ) func add (x, y int ) int { return x + y } func sub (x, y int ) int { return x - y } func do (s string ) func (int , int ) int { switch s { case "+" : return add case "-" : return sub default : return nil } } func main () { var a = do("+" ) fmt.Println(a(10 , 20 )) }
匿名函数和闭包 匿名函数 函数当然还可以作为返回值,但是在 Go 语言中函数内部 不能再像之前那样定义函数了,只能定义匿名函数。匿名函数, 就是没有函数名的函数,匿名函数的定义格式如下
匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量 或者作为立即执行函数
1 2 3 4 5 6 7 8 9 10 11 12 func main () { add := func (x, y) int { fmt.Println(x + y) } add(10 , 20 ) func (x, y int ) { fmt.Println(x + y) }(10 , 20 ) }
闭包 闭包可以理解成“定义在一个函数内部的函数“ 。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func adder () func (int ) int { var x int return func (y int ) int { x += y return x } } func main () { var f = adder() fmt.Println(f(10 )) fmt.Println(f(20 )) fmt.Println(f(30 )) f1 := adder() fmt.Println(f1(40 )) fmt.Println(f1(50 )) }
变量 f 是一个函数并且它引用了其外部作用域中的 x 变量 ,此时 f 就是一个闭包。 在 f 的生命周期 内,变量 x 也一直有效
闭包进阶示例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func adder2 (x int ) func (int ) int { return func (y int ) int { x += y return x } } func main () { var f = adder2(10 ) fmt.Println(f(10 )) fmt.Println(f(20 )) fmt.Println(f(30 )) f1 := adder2(20 ) fmt.Println(f1(40 )) fmt.Println(f1(50 )) }
闭包进阶示例 2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func makeSuffixFunc (suffix string ) func (string ) string { return func (name string ) string { if !strings.HasSuffix(name, suffix) { return name + suffix } return name } } func main () { jpgFunc := makeSuffixFunc(".jpg" ) txtFunc := makeSuffixFunc(".txt" ) fmt.Println(jpgFunc("test" )) fmt.Println(txtFunc("test" )) }
闭包进阶示例 3:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func calc (base int ) (func (int ) int , func (int ) int ) { add := func (i int ) int { base += i return base } sub := func (i int ) int { base -= i return base } return add, sub } func main () { f1, f2 := calc(10 ) fmt.Println(f1(1 ), f2(2 )) fmt.Println(f1(3 ), f2(4 )) fmt.Println(f1(5 ), f2(6 )) }
使用闭包要牢记: 闭包=函数+引用环境
defer语句 defer使用 Go 语言中的 defer 语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按 defer 定义的逆序进行执行,也就是说,先被 defer的语句最后被执行,最后被defer的语句,最先被执行
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () {fmt.Println("start" ) defer fmt.Println(1 )defer fmt.Println(2 )defer fmt.Println(3 )fmt.Println("end" ) }
由于 defer 语句延迟调用的特性,所以 defer 语句能非常方便的处理资源释放问题。比如: 资源清理、文件关闭、解锁及记录时间等
defer的执行时机 在 Go 语言的函数中 return 语句在底层并不是原子操作,它分为给返回值赋值和 RET 指令两 步。而 defer 语句执行的时机就在返回值赋值操作后,RET 指令执行前。具体如下图所示:
defer使用案例 案例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 func f1 () int { x := 5 defer func () { x++ }() return x } func f2 () (x int ) { defer func () { x++ }() return 5 } func f3 () (y int ) { x := 5 defer func () { x++ }() return x } func f4 () (x int ) { defer func (x int ) { x++ }(x) return 5 } func main () { fmt.Println(f1()) fmt.Println(f2()) fmt.Println(f3()) fmt.Println(f4()) }
案例2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func calc (index string , a, b int ) int { ret := a + b fmt.Println(index, a, b, ret) return ret } func main () { x := 1 y := 2 defer calc("AA" , x, calc("A" , x, y)) x = 10 defer calc("BB" , x, calc("B" , x, y)) y = 20 }
内置函数 内置函数
Go 语言中目前(Go1.12)是没有异常机制的 ,但是使用 panic/recover 模式来处理错误。 panic 可以在任何地方引发,但 recover 只有在 defer 调用的函数中有效 。 首先来看一个例子:
panic/recover 的基本使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func funcA () { fmt.Println("func A" ) } func funcB () { panic ("panic in B" ) } func funcC () { fmt.Println("func C" ) } func main () { funcA() funcB() funcC() }
程序运行期间 funcB 中引发了 panic 导致程序崩溃,异常退出了。这个时候我们就可以通过 recover 将程序恢复回来,继续往后执行
改进方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func funcA () { fmt.Println("func A" ) } func funcB () { defer func () { err := recover () if err != nil { fmt.Println("recover in B" ) } }() panic ("panic in B" ) } func funcC () { fmt.Println("func C" ) } func main () { funcA() funcB() funcC() }
注意:
recover()必须搭配 defer 使用
defer 一定要在可能引发 panic 之前定义
defer /recover 实现异常处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func fn2 () { defer func () { err := recover () if err != nil { fmt.Println("抛出异常给管理员发送邮件" ) fmt.Println(err) } }() num1 := 10 num2 := 0 res := num1 / num2 fmt.Println("res=" , res) }
defer /panic/recover 抛出异常 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func readFile (fileName string ) error { if fileName == "main.go" { return nil } return errors.New("读取文件错误" ) } func fn3 () { defer func () { err := recover () if err != nil { fmt.Println("抛出异常给管理员发送邮件" ) } }() var err = readFile("xxx.go" ) if err != nil { panic (err) } fmt.Println("继续执行" ) } func main () { fn3() }
后记: ….这个异常处理, 两眼一黑
指针 关于指针 指针也是一个变量,但它是一种特殊的变量,它存储的数据不是一个普通的值,而是另 一个变量的内存地址
要搞明白 Go 语言中的指针需要先知道 3 个概念:指针地址、指针类型和指针取值
Go 语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和 *(根据地址取值)
指针地址和指针类型 每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。
Go 语言中使用& 字符放在变量前面对变量进行取地址操作。
Go 语言中的值类型(int、float、bool、string、 array、struct)都有对应的指针类型,如:*int、*int64、*string 等
取变量指针的语法如下
其中:
v : 代表被取地址的变量,类型为 T
ptr : 用于接收地址的变量,ptr 的类型就为T,称做 T 的指针类型。 代表指针。
示例:
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { var a = 10 var b = &a fmt.Printf("a:%d ptr:%p\n" , a, &a) fmt.Printf("b:%v type:%T\n" , b, b) fmt.Println("取 b 的地址:" , &b) }
b := &a 的图示:
指针取值 在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作, 也就是指针取值,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { a := 10 b := &a fmt.Printf("type of b:%T\n" , b) c := *b fmt.Printf("type of c:%T\n" , c) fmt.Printf("value of c:%v\n" , c) }
总结 : 取地址操作符&和取值操作符是一对互补操作符,&取出地址, 根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
对变量进行取地址(&)操作,可以获得这个变量的指针变量
指针变量的值是指针地址
对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值
指针传值示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func modify1 (x int ) { x = 100 } func modify2 (x *int ) { *x = 100 } func main () { a := 10 modify1(a) fmt.Println(a) modify2(&a) fmt.Println(a) }
new和make 先看一个例子
1 2 3 4 5 func main () { var userinfo map [string ]string userinfo["username" ] = "张三" fmt.Println(userinfo) }
1 2 3 4 5 func main () { var a *int *a = 100 fmt.Println(*a) }
执行上面的代码会引发 panic,为什么呢?
在 Go 语言中对于引用类型的变量,我们在使用 的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。
而对于值类型 的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配 内存,就引出来今天的 new 和 make。 Go 语言中 new 和 make 是内建的两个函数,主要用来分配内存
new函数分配内存 new 是一个内置的函数,它的函数签名如下
其中:
Type 表示类型,new 函数只接受一个参数,这个参数是一个类型
*Type 表示类型指针,new 函数返回 一个指向该类型内存地址的指针 。
实际开发中 new 函数不太常用
,使用 new 函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子
1 2 3 4 5 6 7 8 func main () { a := new (int ) b := new (bool ) fmt.Printf("%T\n" , a) fmt.Printf("%T\n" , b) fmt.Println(*a) fmt.Println(*b) }
本节开始的示例代码中 var a *int 只是声明了一个指针变量 a 但是没有初始化,
指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new 函数对 a 进行初始化之后就可以正常对其赋值了
1 2 3 4 5 6 7 func main () { var a *int a = new (int ) *a = 10 fmt.Println(*a) }
make函数分配内存 make 也是用于内存分配的,区别于 new,它只用于 slice、map 以及 channel 的内存创建, 而且它返回的类型就是这三个类型本身 ,而不是他们的指针类型 ,因为这三种类型就是引用类型,所以就没有必要返回他们的指针,make 函数的函数签名如下:
1 func make (t Type, size ...IntegerType) Type
make 函数是无可替代的,在使用 slice、map 以及 channel 的时候,都需要使用 make 进 行初始化,然后才可以对它们进行操作。这个在前面中都有说明,关于 channel 在后续的章节详细说明。
本节开始的示例中 var b map[string]int 只是声明变量 b 是一个 map 类型的变量,需要像下面 的示例代码一样使用 make 函数进行初始化操作之后,才能对其进行键值对赋值
1 2 3 4 5 6 func main () { var userinfo map [string ]string userinfo = make (map [string ]string ) userinfo["username" ] = "张三" fmt.Println(userinfo) }
make和new的区别
二者都是用来做内存分配的
make 只用于 slice、map 以及 channel 的初始化,返回的还是这三个引用类型本身
而 new 用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针
结构体 关于Golang结构体 Golang 中没有“类”的概念 两眼一黑,Golang 中的结构体和其他语言中的类有点相似。和其他面向对 象语言中的类相比,Golang 中的结构体具有更高的扩展性和灵活性。
Golang 中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型就无法满足需求了,Golang 提供了一种 自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称 struct。 也就是我们可以通过 struct 来定义自己的类型了。
Golang type 关键词自定义类型和类型别名 Golang 中通过 type 关键词定义一个结构体
自定义类型 在 Go 语言中有一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型, Go 语 言中可以使用 type 关键字来定义自定义类型
上面代码表示:将 myInt 定义为 int 类型,通过 type 关键字的定义,myInt 就是一种新的类型,它具有 int 的特性
类型别名 Golang1.9 版本以后添加的新功能
类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像 一个孩子小时候有大名、小名、英文名,但这些名字都指的是他本人
我们之前见过的 rune 和 byte 就是类型别名 ,他们的底层定义如下
1 2 type byte = uint8 type rune = int32
自定义类型和类型别名的区别 类型别名与自定义类型表面上看只有一个等号的差异,通过下面的这段代码来理解它们 之间的区别
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" type newInt int type myInt = int func main () { var a newInt var b myInt fmt.Printf("type of a:%T\n" , a) fmt.Printf("type of b:%T\n" , b) }
结果显示 a 的类型是 main.newInt,表示 main 包下定义的 newInt 类型。b 的类型是 int 类型
结构体定义初始化的几种方法 结构体的定义 使用 type 和 struct 关键字来定义结构体,具体代码格式如下:
1 2 3 4 5 type 类型名 struct {字段名 字段类型 字段名 字段类型 … }
其中:
类型名:表示自定义结构体的名称,在同一个包内不能重复
字段名:表示结构体字段名。结构体中的字段名必须唯一
字段类型:表示结构体字段的具体类型
举个例子,我们定义一个 person(人)结构体,代码如下:
1 2 3 4 5 type person struct { name string city string age int8 }
同样类型的字段也可以写在一行,
1 2 3 4 type person struct { name, city string age int8 }
注意: 结构体首字母可以大写也可以小写,大写表示这个结构体是公有的,在其他的包里面 可以使用。小写表示这个结构体是私有的,只有这个包里面才能使用
结构体实例化(第一种方法) 只有当结构体实例化时 ,才会真正地分配内存 。也就是必须实例化后才能使用结构体的字段。 结构体本身也是一种类型,我们可以像声明内置类型一样使用 var 关键字声明结构体类型
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" type person struct { name string city string age int } func main () { var p1 person p1.name = "张三" p1.city = "北京" p1.age = 18 fmt.Printf("p1=%v\n" , p1) fmt.Printf("p1=%#v\n" , p1) }
结构体实例化(第二种方法) 还可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址。 格式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" type person struct { name string city string age int } func main () { var p2 = new (person) p2.name = "张三" p2.age = 20 p2.city = "北京" fmt.Printf("%T\n" , p2) fmt.Printf("p2=%#v\n" , p2) }
从打印的结果中我们可以看出 p2 是一个结构体指针
注意 :在 Golang 中支持对结构体指针直接使用 .来访问结构体的成员。p2.name = “张三” ,其实在底层是(*p2).name = “张三”
结构体实例化(第三种方法) 使用&对结构体进行取地址操作相当于对该结构体类型进行了一次 new 实例化操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" type person struct { name string city string age int } func main () { p3 := &person{} fmt.Printf("%T\n" , p3) fmt.Printf("p3=%#v\n" , p3) p3.name = "zhangsan" p3.age = 30 p3.city = "深圳" (*p3).age = 40 fmt.Printf("p3=%#v\n" , p3) }
结构体实例化(第四种方式) 通过键值对初始化进行结构体实例化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" type person struct { name string city string age int } func main () { p4 := person{ name: "zhangsan" , city: "北京" , age: 18 , } fmt.Printf("p4=%#v\n" , p4) }
结构体实例化(第五种方式) 结构体指针进行键值对初始化
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" type person struct { name string city string age int } func main () { p5 := &person{ name: "zhangsan" , city: "上海" , age: 28 , } fmt.Printf("p5=%#v\n" , p5) }
当某些字段没有初始值的时候,这个字段可以不写。此时,没有指定初始值的字段的值为零值
1 2 3 4 5 6 7 8 9 10 type person struct { name string city string age int } func main () { p6 := &person{ city: "北京" , } fmt.Printf("p6=%#v\n" , p6) }
结构体实例化(第六种方式) 使用值的列表初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" type person struct { name string city string age int } func main () { p7 := &person{ "zhangsan" , "北京" , 28 } fmt.Printf("p7=%#v\n" , p7) }
使用这种格式初始化时,需要注意
1.必须初始化结构体的所有字段。
2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3.该方式不能和键值初始化方式混用
结构体方法和接收者 在 go 语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法 就是定义了接收者的函数。接收者的概念就类似于java中的 this
方法的定义格式如下
1 2 3 func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 }
其中:
接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小 写字母,而不是 self、this 之类的命名。例如,Person 类型的接收者变量应该命名为 p, Connector 类型的接收者变量应该命名为 c 等
接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
方法名、参数列表、返回参数:具体格式与函数定义相同
示例1: 给结构体 Person 定义一个方法打印 Person 的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" type Person struct { name string age int8 } func (p Person) printInfo() { fmt.Printf("姓名:%v 年龄:%v" , p.name, p.age) } func main () { p1 := Person{ name: "小王子" , age: 25 , } p1.printInfo() }
值类型的接收者 当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身
指针类型的接收者 指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针 的任意成员变量,在方法结束后,修改都是有效的,这种方式就十分接近于其他语言中面向 对象中的 this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport "fmt" type Person struct { name string age int } func (p Person) printInfo() { fmt.Printf("姓名:%v 年龄:%v\n" , p.name, p.age) } func (p *Person) setInfo(name string , age int ) { p.name = name p.age = age } func main () { p1 := Person{ name: "小王子" , age: 25 , } p1.printInfo() p1.setInfo("张三" , 20 ) p1.printInfo() }
给任意类型添加方法 在 Go 语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的 int 类型使用 type 关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" type myInt int func (m myInt) SayHello() { fmt.Println("Hello, 我是一个 int。" ) } func main () { var m1 myInt m1.SayHello() m1 = 100 fmt.Printf("%#v %T\n" , m1, m1) }
注意: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法
结构体匿名字段 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段
1 2 3 4 5 6 7 8 9 10 type Person struct { string int } func main () { p1 := Person{ "小王子" , 18 ,} fmt.Printf("%#v\n" , p1) fmt.Println(p1.string , p1.int ) }
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型 的匿名字段只能有一个
嵌套结构体 一个结构体中可以嵌套包含另一个结构体或结构体指针 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" type Address struct { Province string City string } type User struct { Name string Gender string Address Address } func main () { user1 := User{ Name: "张三" , Gender: "男" , Address: Address{ Province: "广东" , City: "深圳" , }, } fmt.Printf("user1=%#v\n" , user1) }
关于嵌套结构体的字段名冲突 嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体 的字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package maintype Address struct { Province string City string CreateTime string } type Email struct { Account string CreateTime string } type User struct { Name string Gender string Address Email } func main () {var user3 Useruser3.Name = "张三" user3.Gender = "男" user3.Address.CreateTime = "2020" user3.Email.CreateTime = "2021" }
嵌套匿名结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" type Address struct { Province string City string } type User struct { Name string Gender string Address } func main () { var user2 User user2.Name = "张三" user2.Gender = "男" user2.Address.Province = "广东" user2.City = "深圳" fmt.Printf("user2=%#v\n" , user2) }
注意 :当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找
结构体的继承 Go 语言中使用结构体也可以实现其他编程语言中的继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport "fmt" type Animal struct { name string } func (a *Animal) run() { fmt.Printf("%s 会运动!\n" , a.name) } type Dog struct { Age int8 *Animal } func (d *Dog) wang() { fmt.Printf("%s 会汪汪汪~\n" , d.name) } func main () { d1 := &Dog{ Age: 4 , Animal: &Animal{ name: "阿奇" , }, } d1.wang() }
看到这继承我两眼一黑
JSON序列化 结构体与 JSON 序列 给前端提供 Api 接口数据,就需要涉及到结构体和 Json 之间的相互转换
Golang JSON 序列化 是指把结构体数据转化成 JSON 格式的字符串
Golang JSON 的反序列化 是指把 JSON 数据转化成 Golang 中的结构体对象
Golang中的序列化和反序列化主要通过 “encoding/json” 包 中 的 json.Marshal() 和 json.Unmarshal()方法实现
json序列化(结构体对象->json) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int Gender string name string Sno string } func main () { var s1 = Student{ ID: 1 , Gender: "男" , name: "李四" , Sno: "s0001" , } fmt.Printf("%#v\n" , s1) var s, _ = json.Marshal(s1) jsonStr := string (s) fmt.Println(jsonStr) }
json反序列化(json->结构体对象) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int Gender string Name string Sno string } func main () { var jsonStr = `{"ID":1,"Gender":"男","Name":"李四","Sno":"s0001"}` var student Student err := json.Unmarshal([]byte (jsonStr), &student) if err != nil { fmt.Printf("unmarshal err=%v\n" , err) } fmt.Printf("反序列化后 student=%#v student.Name=%v \n" , student, student.Name) }
结构体标签tag Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag 在结构体字段的 后方定义,由一对反引号包裹起来,具体的格式如下:
1 `key1: "value1" key2: "value2" `
结构体 tag 由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结 构体字段可以设置多个键值对 tag,不同的键值对之间使用空格分隔
注意: 为结构体编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差 ,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取 值。例如不要在 key 和 value 之间添加空格
序列化示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int `json:"id"` Gender string `json:"gender"` Name string Sno string } func main () { var s1 = Student{ ID: 1 , Gender: "男" , name: "李四" , Sno: "s0001" , } fmt.Printf("%#v\n" , s1) var s, _ = json.Marshal(s1) jsonStr := string (s) fmt.Println(jsonStr) }
反序列化示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int `json:"id"` Gender string `json:"gender"` Name string Sno string } func main () { var s2 Student var str = "{\"id\":1,\"gender\":\"男\",\"Name\":\"李四\",\"Sno\":\"s0001\"}" err := json.Unmarshal([]byte (str), &s2) if err != nil { fmt.Println(err) } fmt.Printf("%#v" , s2) }
五. 嵌套结构体和 JSON 序列化反序列化 序列化示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int Gender string Name string } type Class struct { Title string Students []Student } func main () { c := &Class{ Title: "001" , Students: make ([]Student, 0 , 200 ), } for i := 0 ; i < 10 ; i++ { stu := Student{ Name: fmt.Sprintf("stu%02d" , i), Gender: "男" , ID: i, } c.Students = append (c.Students, stu) } data, err := json.Marshal(c) if err != nil { fmt.Println("json marshal failed" ) return } fmt.Printf("json:%s\n" , data) }
反序列化示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int Gender string Name string } type Class struct { Title string Students []Student } func main () { str := `{ "Title":"001", "Students":[ {"ID":0,"Gender":"男","Name":"stu00"}, {"ID":1,"Gender":"男","Name":"stu01"}, {"ID":2,"Gender":"男","Name":"stu02"}, {"ID":3,"Gender":"男","Name":"stu03"}, {"ID":4,"Gender":"男","Name":"stu04"}, {"ID":5,"Gender":"男","Name":"stu05"}, {"ID":6,"Gender":"男","Name":"stu06"}, {"ID":7,"Gender":"男","Name":"stu07"}, {"ID":8,"Gender":"男","Name":"stu08"}, {"ID":9,"Gender":"男","Name":"stu09"} ] }` c1 := &Class{} err := json.Unmarshal([]byte (str), c1) if err != nil { fmt.Println("json unmarshal failed!" ) return } fmt.Printf("%#v\n" , c1) }
Package包 Golang 中包的介绍和定义 包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,Go 提供了很多内置包,如 fmt、strconv、strings、sort、errors、time、encoding/json、os、io 等。
Golang 中的包可以分为三种:1、系统内置包 2、自定义包 3、第三方
系统内置包: Golang 语言给我们提供的内置包,引入后可以直接使用,如 fmt、strconv、strings、 sort、errors、time、encoding/json、os、io 等
自定义包:开发者自己写的包
第三方包:属于自定义包的一种,需要下载安装到本地后才可以使用,如前面介绍的 “github.com/shopspring/decimal”包解决 float 精度丢失问题
Golang 包管理工具 go mod 在 Golang1.11 版本之前如果我们要自定义包的话必须把项目放在 GOPATH 目录。Go1.11 版 本之后无需手动配置环境变量,使用 go mod 管理项目,也不需要非得把项目放到 GOPATH 指定目录下,你可以在你磁盘的任何位置新建一个项目 , Go1.13 以后可以彻底不要 GOPATH 了1.
go mod init初始化项目 实际项目开发中我们首先要在我们项目目录中用 go mod 命令生成一个 go.mod 文件管理我 们项目的依赖。 比如我们的 golang 项目文件要放在了 crisp 这个文件夹,这个时候我们需要在 crisp 文件夹 里面使用 go mod 命令生成一个 go.mod 文件
使用go mod init 项目文件夹名字生成go.mod文件
当然如果你和我一样比较懒, 你也可以下载Goland, 对着项目文件夹右键点击新建->Go模块文件
也可以创建一样的go.mod文件
go mod 其他命令 使用 go mod [arguments] 执行以下go mod 命令
Golang 中自定义包 包(package)是多个 Go 源码的集合,一个包可以简单理解为一个存放多个.go 文件的文件 夹。该文件夹下面的所有 go 文件都要在代码的第一行添加如下代码,声明该文件归属的包
注意:
一个文件夹下面直接包含的文件只能归属一个 package,同样一个 package 的文件不能 在多个文件夹下
包名可以不和文件夹的名字一样,包名不能包含 - 符号
包名为 main 的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译 不包含 main 包的源代码则不会得到可执行文件
定义一个包 如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识 符必须是对外可见的(public)。在 Go 语言中只需要 将标识符的首字母大写 就可以让标识 符对外可见了
定义一个包名为 calc 的包
1 2 3 4 5 6 7 8 9 10 11 package calc var a = 100 var Age = 20 func Add (x, y int ) int { return x + y } func Sum (x, y int ) int { return x - y }
**2. main.go 中引入这个包 **
访问一个包里面的公有属性方法的时候需要通过包名称.去访问
1 2 3 4 5 6 7 8 9 10 package mainimport ( "fmt" "crisp/calc" ) func main () { c := calc.Add(10 , 20 ) fmt.Println(c) }
导入一个包 单行导入
1 2 import "包 1" import "包 2
多行导入
1 2 3 4 import ( "包 1" "包 2" )
匿名导入
如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。具体的格式如下
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中
自定义包名
在导入包名的时候,我们还可以为导入的包设置别名。通常用于导入的包名太长或者导入的包名冲突的情况。具体语法格式如下
Golang中使用第三方包 可以在 https://pkg.go.dev/ 查找看常见的 golang 第三方包
找到需要下载安装的第三方包的地址 以前面的的解决 float 精度损失的包 decimal 为例
https://github.com/shopspring/decimal
安装这个包 第一种方法: go get 包名称 (全局)
1 go get github.com/shopspring/decimal
第二种方法:go mod download (全局)
依赖包会自动下载到**$GOPATH/pkg/mod**,多个项目可以共享缓存的 mod,注意使用 go mod download 的时候首先需要在你的项目里面引入第三方包
第三种方法: go mod vendor 将依赖复制到当前项目的 vendor
将依赖复制到当前项目的 vendor 下
注意: 使用 go mod vendor 的时候, $GOPATH/pkg/mod 内已经引入了第三方包
3. 查阅文档使用包 ctrl c+v时间
接口 接口的介绍 Golang 中的接口是一种抽象数据类型 ,Golang 中接口定义了对象的行为规范,只定义规范 不实现。接口中定义的规范由具体的对象来实现
通俗的讲接口就一个标准,它是对一个对象的行为和规范进行约定,约定实现接口的对象必 须得按照接口的规范
Golang接口的定义 在 Golang 中接口(interface)是一种类型,一种抽象的类型。接口(interface)是一组函数method 的集合,Golang 中的接口不能包含任何变量
在 Golang 中接口中的所有方法都没有方法体,接口定义了一个对象的行为规范,只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想
Golang 中的接口也是一种数据类型 ,不需要显示实现 。只需要 一个变量含有接口类型中的所有方法 ,那么这个变量就实现了这个接口
Golang 中每个接口由数个方法组成,接口的定义格式如下
1 2 3 4 type 接口名 interface { 方法名 1 ( 参数列表 1 ) 返回值列表 1 方法名 2 ( 参数列表 2 ) 返回值列表 2 … }
其中:
接口名 :使用 type 将接口定义为自定义的类型名。Go 语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer 等。接口名最好要能突出该接口的类型含义
方法名: 当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被 接口所在的包(package)之外的代码访问
参数列表、返回值列表: 参数列表和返回值列表中的参数变量名可以省略
示例: 定义一个 Usber 接口让 Phone 和 Camera 结构体实现这个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package mainimport "fmt" type Usber interface { Start() Stop() } type Phone struct { Name string } func (p Phone) Start() { fmt.Println(p.Name, "开始工作" ) } func (p Phone) Stop() { fmt.Println("phone 停止" ) } type Camera struct {} func (c Camera) Start() { fmt.Println("相机 开始工作" ) } func (c Camera) Stop() { fmt.Println("相机 停止工作" ) } func main () { phone := Phone{Name: "小米手机" ,} var p Usber = phone p.Start() camera := Camera{} var c Usber = camera c.Start() }
示例: Computer 结构体中的 Work 方法必须传入一个 Usb 的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainimport "fmt" type Usber interface { Start() Stop() } type Phone struct { Name string } func (p Phone) Start() { fmt.Println(p.Name, "开始工作" ) } func (p Phone) Stop() { fmt.Println("phone 停止" ) } type Camera struct {} func (c Camera) Start() { fmt.Println("相机 开始工作" ) } func (c Camera) Stop() { fmt.Println("相机 停止工作" ) } type Computer struct { Name string } func (c Computer) Work(usb Usber) { usb.Start() usb.Stop() } func main () { phone := Phone{Name: "小米手机" , } camera := Camera{} computer := Computer{} computer.Work(phone) computer.Work(camera) }
空接口 Golang 中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。空接口表示没有任何约束,因此任何类型变量都可以实现空接口
空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型 (和JAVA的泛型差不多)
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { var x interface {} s := "你好 golang" x = s fmt.Printf("type:%T value:%v\n" , x, x) i := 100 x = i fmt.Printf("type:%T value:%v\n" , x, x) b := true x = b fmt.Printf("type:%T value:%v\n" , x, x) }
空接口作为函数的参数 使用空接口实现可以接收任意类型的函数参数
1 2 3 4 func show (a interface {}) { fmt.Printf("type:%T value:%v\n" , a, a) }
map 的值实现空接口 使用空接口实现可以保存任意值的字典
1 2 3 4 5 6 var studentInfo = make (map [string ]interface {})studentInfo["name" ] = "张三" studentInfo["age" ] = 18 studentInfo["married" ] = false fmt.Println(studentInfo)
切片实现空接口 1 2 var slice = []interface {}{"张三" , 20 , true , 32.2 }fmt.Println(slice)
类型断言 一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型 和动态值 。 如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言,其语法格式
其中:
x : 表示类型为 interface{}的变量
T : 表示断言 x 可能是的类型
该语法返回两个参数,第一个参数是 x 转化为 T 类型后的变量 ,第二个值是一个布尔值 ,若 为 true 则表示断言成功,为 false 则表示断言失败
示例:
1 2 3 4 5 6 7 8 9 10 func main () { var x interface {} x = "Hello golnag" v, ok := x.(string ) if ok { fmt.Println(v) } else { fmt.Println("类型断言失败" ) } }
上面的示例中如果要断言多次就需要写多个 if 判断,这个时候我们可以使用 switch 语句来 实现
注意: 类型.(type)只能结合 switch 语句使用
1 2 3 4 5 6 7 8 9 10 11 12 func justifyType (x interface {}) { switch v := x.(type ) { case string : fmt.Printf("x is a string,value is %v\n" , v) case int : fmt.Printf("x is a int is %v\n" , v) case bool : fmt.Printf("x is a bool is %v\n" , v) default : fmt.Println("unsupport type!" ) } }
因为空接口可以存储任意类型值的特点,所以空接口在 Go 语言中的使用十分广泛
**注意:**只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗
结构体值接收者和指针接收者实现接口的区别 值接收者 如果结构体中的方法是值接收者,那么实例化后的结构体值类型 和结构体指针类型 都可以赋值给接口变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" type Usb interface { Start() Stop() } type Phone struct { Name string } func (p Phone) Start() { fmt.Println(p.Name, "开始工作" ) } func (p Phone) Stop() { fmt.Println("phone 停止" ) } func main () { phone1 := Phone{Name: "小米手机" , } var p1 Usb = phone1 p1.Start() phone2 := &Phone{Name: "苹果手机" , } var p2 Usb = phone2 p2.Start() }
指针接受者 如果结构体中的方法是指针接收者,那么实例化后结构体指针类型 都可以赋值给接口变量, 结构体值类型 没法赋值给接口变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport "fmt" type Usb interface { Start() Stop() } type Phone struct { Name string } func (p *Phone) Start() { fmt.Println(p.Name, "开始工作" ) } func (p *Phone) Stop() { fmt.Println("phone 停止" ) } func main () { phone2 := &Phone{Name: "苹果手机" , } var p2 Usb = phone2 p2.Start() }
一个结构体实现多个接口 Golang 中一个结构体也可以实现多个接口
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport "fmt" type AInterface interface { GetInfo() string } type BInterface interface { SetInfo(string , int ) } type People struct { Name string Age int } func (p People) GetInfo() string { return fmt.Sprintf("姓名:%v 年龄:%d" , p.Name, p.Age) } func (p *People) SetInfo(name string , age int ) { p.Name = name p.Age = age } func main () { var people = &People{Name: "张三" , Age: 20 , } var p1 AInterface = people var p2 BInterface = people fmt.Println(p1.GetInfo()) p2.SetInfo("李四" , 30 ) fmt.Println(p1.GetInfo()) }
接口嵌套 接口与接口间可以通过嵌套创造出新的接口
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport "fmt" type SayInterface interface { say() } type MoveInterface interface { move() } type Animal interface { SayInterface MoveInterface } type Cat struct { name string } func (c Cat) say() { fmt.Println("喵喵喵" ) } func (c Cat) move() { fmt.Println("猫会动" ) } func main () { var x Animal x = Cat{name: "花花" } x.move() x.say() }
多协程 进程、线程以及并行、并发 关于进程和线程 进程(Process) 就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基 本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进 程都有一个自己的地址空间。一个进程至少有 5 种基本状态,它们是:初始态,执行态, 等待状态,就绪状态,终止状态
通俗的讲进程就是一个正在执行的程序
线程 是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
一个进程可以创建多个线程,同一个进程中的多个线程可以并发执行,一个程序要运行的话 至少有一个进程
关于并行和并发 并发: 多个线程同时竞争一个位置,竞争到的才可以执行,每一个时间段只有一个线程在执行。
并行: 多个线程可以同时执行,每一个时间段,可以有多个线程同时执行
通俗的讲多线程程序在单核 CPU 上面运行就是并发,多线程程序在多核 CUP 上运行就是并行,如果线程数大于 CPU 核数, 则多线程程序在多个 CPU 上面运行既有并行又有并发
单核cpu
多核cpu
Golang的协程(goroutine) golang 中的主线程: (可以理解为线程/也可以理解为进程),在一个 Golang 程序的主线程 上可以起多个协程 。Golang 中多协程 可以实现并行或者并发
协程 :可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在 ,是 完全由用户自己的程序进行调度的。Golang 的一大特色就是从语言层面原生支持协程,在 函数或者方法前面加 go 关键字就可创建一个协程。可以说 Golang 中的协程就是 goroutine
Golang 中的多协程类似其他语言中的多线程
多协程和多线程: :Golang 中每个 goroutine (协程) 默认占用内存 远比 Java 、C 的线程少。 OS 线程(操作系统线程)一般都有固定的栈内存(通常为 2MB 左右),一个 goroutine (协 程) 占用内存非常小,只有 2KB 左右,多协程 goroutine 切换调度开销方面远比线程要少。 这也是为什么越来越多的大公司使用 Golang 的原因之一
Goroutine 的使用以及 sync.WaitGroup 并行执行需求
在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 50 毫秒秒输出 “你好 golang” 在主线程中也每隔 50 毫秒输出”你好 golang”, 输出10次后,退出程序,要求主线程和 goroutine 同时执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "strconv" "time" ) func test () { for i := 1 ; i <= 10 ; i++ { fmt.Println("tesst () hello,world " + strconv.Itoa(i)) time.Sleep(time.Second) } } func main () { go test() for i := 1 ; i <= 10 ; i++ { fmt.Println(" main() hello,golang" + strconv.Itoa(i)) time.Sleep(time.Second) } }
上面代码看上去没有问题,但是要注意主线程执行完毕后即使协程没有执行完毕,程序 会退出,所以我们需要对上面代码进行改造
sync.WaitGroup 可以实现主线程等待协程执行完毕
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "strconv" "sync" "time" ) var wg sync.WaitGroup func test () { for i := 1 ; i <= 10 ; i++ { fmt.Println("test () 你好 golang time.Sleep(time.Millisecond * 50) } wg.Done() // 4、goroutine 结束就登记-1 } func main() { wg.Add(1) //2、启动一个 goroutine 就登记+1 go test() for i := 1; i <= 2; i++ { fmt.Println(" main() 你好 golang" + strconv.Itoa(i)) time.Sleep(time.Millisecond * 50) } wg.Wait() // 3、等待所有登记的 goroutine 都结束 }
Goroutine并发 Go 语言中实现并发非常简单,我们还可以启动多个 goroutine。
示例: (这里使用了 sync.WaitGroup 来实现等待 goroutine 执行完毕)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "strconv" "sync" "time" ) var wg sync.WaitGroupfunc hello (i int ) { defer wg.Done() fmt.Println("Hello Goroutine!" , i) } func main () { for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go hello(i) } wg.Wait() }
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为 10 个 goroutine 是并发执行的,而 goroutine 的调度是随机的
设置 Golang 并行运行的时候占用的 cup 数量 Go 运行时的调度器使用 GOMAXPROCS 参数来确定需要使用多少个 OS 线程来同时执行 Go 代码, 默认值是机器上的 CPU 核心数
例如在一个 8 核心的机器上,调度器会把 Go 代码同 时调度到 8 个 OS 线程上。 Go 语言中可以通过 runtime.GOMAXPROCS()函数设置当前程序并发时占用的 CPU 逻辑核心数
Go1.5 版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的 CPU 逻辑核心数
1 2 3 4 5 6 7 8 9 10 11 package mainimport ( "fmt" "runtime" ) func main () { cpuNum := runtime.NumCPU() fmt.Println("cpuNum=" , cpuNum) runtime.GOMAXPROCS(cpuNum - 1 ) }
Goroutine并发案例 通过传统for循环来统计素数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { start := time.Now().Unix() for num := 1 ; num <= 120000 ; num++ { flag := true for i := 2 ; i < num; i++ { if num%i == 0 { flag = false break } } if flag { } } end := time.Now().Unix() fmt.Println("普通的方法耗时=" , end-start) }
多个协程统计统计素数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroupfunc fn1 (n int ) { for num := (n-1 )*30000 + 1 ; num <= n*30000 ; num++ { flag := true for i := 2 ; i < num; i++ { if num%i == 0 { flag = false break } } if flag { } } wg.Done() } func main () { start := time.Now().Unix() for i := 1 ; i <= 4 ; i++ { wg.Add(1 ) go fn1(i) } wg.Wait() end := time.Now().Unix() fmt.Println("采用多协程的方法耗时=" , end-start) }
对比两种方法的执行的结果可以发现, goroutine 已经能大大的提升性能
如果我们想统计数据和打印数据同时进行,这个时候我们就可以使用管道(Channel)
Golang并发安全和锁 需求: 现在要计算 1-60 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。要求使用 goroutine 完成
思路
编写一个函数,来计算各个数的阶乘,并放入到 map 中.
启动多个协程,将统计的将结果放入到 map 中
只使用 Goroutine 实现,运行的时候可能会出现资源争夺问题 concurrent map writes
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport ( "fmt" "sync" _"time" ) var ( myMap = make (map [int ]int ) wg sync.WaitGroup ) func test (n int ) { res := 1 for i := 1 ; i <= n; i++ { res *= i } myMap[n] = res wg.Done() } func main () { for i := 1 ; i <= 60 ; i++ { wg.Add(1 ) go test(i) } wg.Wait() for i, v := range myMap { fmt.Printf("map[%d]=%d\n" , i, v) } }
互斥锁 互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 可以访问共享资源 。
Go 语言中使用 sync 包的 Mutex 类型来实现互斥锁。使用互斥锁来修复上面 代码的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package mainimport ( "fmt" "sync" _"time" ) var ( myMap = make (map [int ]int ) wg sync.WaitGroup lock sync.Mutex ) func test (n int ) { res := 1 for i := 1 ; i <= n; i++ { res *= i } lock.Lock() myMap[n] = res lock.Unlock() wg.Done() } func main () { for i := 1 ; i <= 60 ; i++ { wg.Add(1 ) go test(i) } wg.Wait() for i, v := range myMap { fmt.Printf("map[%d]=%d\n" , i, v) } }
注意:
使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等 锁;
当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区
多个 goroutine 同时等待一个锁时,唤醒的策略是随机的
虽然使用互斥锁能解决资源争夺问题,但是并不完美,通过全局变量加锁同步来实现通讯, 并不利于多个协程对全局变量的读写操作。这个时候我们也可以通过另一种方式来实现上面的功能管道(Channel)
读写互斥锁 互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资 源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。
读写锁在 Go 语言中使用 sync 包中的 RWMutex 类型。
读写锁分为两种:读锁 和写锁 。
当一个 goroutine 获取读锁之后,其他的 goroutine 如果是获 取读锁会继续获得锁,如果是获取写锁就会等待;当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 var ( x int64 wg sync.WaitGroup lock sync.Mutex rwlock sync.RWMutex ) func write () { rwlock.Lock() x = x + 1 time.Sleep(10 * time.Millisecond) rwlock.Unlock() wg.Done() } func read () { rwlock.RLock() time.Sleep(time.Millisecond) rwlock.RUnlock() wg.Done() } func main () { start := time.Now() for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go write() } for i := 0 ; i < 1000 ; i++ { wg.Add(1 ) go read() } wg.Wait() end := time.Now() fmt.Println(end.Sub(start)) }
需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来
Goroutine Recover 解决协程中出现的 Panic 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport ( "fmt" "time" ) func sayHello () { for i := 0 ; i < 10 ; i++ { time.Sleep(time.Second) fmt.Println("hello,world" ) } } func test () { defer func () { if err := recover (); err != nil { fmt.Println("test() 发生错误" , err) } }() var myMap map [int ]string myMap[0 ] = "golang" } func main () { go sayHello() go test() for i := 0 ; i < 10 ; i++ { fmt.Println("main() ok=" , i) time.Sleep(time.Second) } }
Channel Channel(管道)的介绍 管道是 Golang 在语言级别上提供的 goroutine 间的通讯方式,我们可以使用 channel 在 多个 goroutine 之间传递消息。如果说 goroutine 是 Go 程序并发的执行体,channel 就是它们之间的连接。channel 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制, 管道是协程之间的通信方式
Golang 的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内存而 不是通过共享内存而实现通信
Go 语言中的管道(channel)是一种特殊的类型。管道像一个传送带或者队列,总是遵循**先入先出**(First In First Out)的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,也就是声明 channel 的时候需要为其指定元素类型
Channel的定义 channel 是一种类型,一种引用类型。声明管道类型的格式如下
1 2 3 4 5 var 变量 chan 元素类型举几个例子: var ch1 chan int var ch2 chan bool var ch3 chan []int
声明的管道后需要使用 make 函数初始化之后才能使用
示例:
1 2 3 4 5 6 ch1 := make (chan int , 10 ) ch2 := make (chan bool , 4 ) ch3 := = make (chan []int , 3 )
channel相关操作 管道有发送(send)、接收(receive)和关闭(close)三种操作
发送和接收都使用**<-**符号
先使用以下语句定义一个管道
发送(将数据放在管道内) 将一个值发送到管道中
接收(从管道内取值) 从一个管道中接收值
关闭管道 通过调用内置的 close 函数来关闭管道
关于关闭管道需要注意的事情是,只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭管道。管道是可以被垃圾回收机制(GC)回收的 ,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭管道不是必须的
关闭后的管道有以下特点:
对一个关闭的管道再发送值就会导致 panic
对一个关闭的管道进行接收会一直获取值直到管道为空
对一个关闭的并且没有值的管道执行接收操作会得到对应类型的零值
关闭一个已经关闭的管道会导致 panic。
管道阻塞 无缓冲的管道:
如果创建管道的时候没有指定容量,那么我们可以叫这个管道为无缓冲的管道, 无缓冲的管道又称为阻塞的管道 。
示例:
1 2 3 4 5 func main () { ch := make (chan int ) ch <- 10 fmt.Println("发送成功" ) }
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
1 2 3 4 5 fatal error : all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() D:/goroutine/main.go :10 +0x5b exit status 2
有缓冲的管道:
解决上面问题的方法还有一种就是使用有缓冲区的管道。我们可以在使用 make 函数初始化管道的时候为其指定管道的容量
示例:
1 2 3 4 5 func main () { ch := make (chan int , 1 ) ch <- 10 fmt.Println("发送成功" ) }
只要管道的容量大于零,那么该管道就是有缓冲的管道,管道的容量表示管道中能存放元素的数量。
管道阻塞示例:
1 2 3 4 5 6 func main () { ch := make (chan int , 1 ) ch <- 10 ch <- 12 fmt.Println("发送成功" ) }
解决方法示例:
1 2 3 4 5 6 7 8 9 func main () { ch := make (chan int , 1 ) ch <- 10 <-ch ch <- 12 <-ch ch <- 17 fmt.Println("发送成功" ) }
for range从管道循环取值 当向管道中发送完数据时,我们可以通过 close 函数来关闭管道。 当管道被关闭时,再往该管道发送值会引发 panic,从该管道取值的操作会先取完管道中的值,再然后取到的值一直都是对应类型的零值。
那如何判断一个管道是否被关闭了呢 ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func main () { var ch1 = make (chan int , 5 ) for i := 0 ; i < 5 ; i++ { ch1 <- i + 1 } close (ch1) for val := range ch1 { fmt.Println(val) } }
从上面的例子中可以看到有两种方式在接收值的时候判断该管道是否被关闭,不过通常情况使用的是 for range 的方式。使用 for range 遍历管道,当管道被关闭的时候就会退出 for range。
channel结合goroutine使用 示例1: 定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行
要求:
开启一个 fn1 的的协程给向管道 inChan 中写入 100 条数据
开启一个 fn2 的协程读取 inChan 中写入的数据
fn1 和 fn2 要同时操作一个管道
主线程必须等待操作完成后才可以退出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroupfunc fn1 (intChan chan int ) { for i := 0 ; i < 100 ; i++ { intChan <- i + 1 fmt.Println("writeData 写入数据-" , i+1 ) time.Sleep(time.Millisecond * 100 ) } close (intChan) wg.Done() } func fn2 (intChan chan int ) { for v := range intChan { fmt.Printf("readData 读到数据=%v\n" , v) time.Sleep(time.Millisecond * 50 ) } wg.Done() } func main () { allChan := make (chan int , 100 ) wg.Add(1 ) go fn1(allChan) wg.Add(1 ) go fn2(allChan) wg.Wait() fmt.Println("读取完毕..." ) }
示例2 goroutine 结合 channel 实现统计 1-120000 的数字中那些是素数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroupfunc putNum (intChan chan int ) { for i := 1 ; i <= 1000 ; i++ { intChan <- i } close (intChan) wg.Done() } func primeNum (intChan chan int , primeChan chan int , exitChan chan bool ) { for num := range intChan { var flag bool = true for i :=2 ; i < num; i++ { if num%i == 0 { flag = false break } } if flag { primeChan <- num } } exitChan <- true wg.Done() } func printPrime (primeChan chan int ) { for v := range primeChan { fmt.Println(v) } wg.Done() } func main () { start := time.Now().Unix() intChan := make (chan int , 1000 ) primeChan := make (chan int , 20000 ) exitChan := make (chan bool , 8 ) wg.Add(1 ) go putNum(intChan) for i := 0 ; i < 8 ; i++ { wg.Add(1 ) go primeNum(intChan, primeChan, exitChan) } wg.Add(1 ) go printPrime(primeChan) wg.Add(1 ) go func () { for i := 0 ; i < 8 ; i++ { <-exitChan } close (primeChan) wg.Done() }() wg.Wait() end := time.Now().Unix() fmt.Println(end - start) fmt.Println("main 线程退出" ) }
单向管道 有的时候我们会将管道作为参数在多个任务函数间传递 ,很多时候我们在不同的任务函数中使用管道都会对其进行限制,比如限制管道在函数中只能发送或只能接收
声明channel为只写 在声明的时候在chan右边加上指向管道的单箭头
1 2 var chanWriter chan <- int chanWriter = make (chan int , 3 )
声明channel为只读 在声明的时候在chan左边加上指出管道的单箭头
1 var chanReader <-chan int
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 var chan2 chan <- int chan2 = make (chan int , 3 ) chan2<- 20 fmt.Println("chan2=" , chan2) var chan3 <-chan int num2 := <-chan3 fmt.Println("num2" , num2)
select多路复用 传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock,在实际开发中,可能我们不好确定什么关闭该管道
你也许会写出如下代码使用遍历的方式来实现:
1 2 3 4 5 6 7 for { data, ok := <-ch1 data, ok := <-ch2 … }
这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go 内置了 select 关键字,可以同时响应多个管道的操作
select 的使用类似于 switch 语句,它有一系列 case 分支和一个默认的分支。每个 case 会对应一个管道的通信(接收或发送)过程。select 会一直等待,直到某个 case 的通信操作完成 时,就会执行 case 分支对应的语句。具体格式如下
1 2 3 4 5 6 7 8 9 10 select { case <-ch1: ... case data := <-ch2: ... case ch3<-data: ... default : 默认操作 }
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "fmt" "time" ) func main () { intChan := make (chan int , 10 ) for i := 0 ; i < 10 ; i++ { intChan <- i } stringChan := make (chan string , 5 ) for i := 0 ; i < 5 ; i++ { stringChan <- "hello" + fmt.Sprintf("%d" , i) } for { select { case v := <-intChan: fmt.Printf("从 intChan 读取的数据%d\n" , v) time.Sleep(time.Second) case v := <-stringChan: fmt.Printf("从 stringChan 读取的数据%s\n" , v) time.Sleep(time.Second) default : fmt.Printf("都取不到了\n" ) time.Sleep(time.Second) return } } }
使用 select 语句能提高代码的可读性
可处理一个或多个 channel 的发送/接收操作
如果多个 case 同时满足,select 会随机选择一个
对于没有 case 的 select{}会一直等待,可用于阻塞 main 函数
反射 反射的引入 有时我们需要写一个函数,这个函数有能力统一处理各种值类型,而这些类型可能无法共享同一个接口,也可能布局未知,也有可能这个类型在我们设计函数时还不存在,这个时候我们就可以用到反射
空接口可以存储任意类型的变量,那我们如何知道这个空接口保存数据的类型是什么? 值是什么呢?
方法一: 使用类型断言 X.(T)
方法二: 使用反射实现,也就是在程序运行时动态的获取一个变量的类型信息和值信息
把结构体序列化成 json 字符串,自定义结构体 Tag 标签的时候就用到了反射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "encoding/json" "fmt" ) type Student struct { ID int `json:"id"` Gender string `json:"gender"` Name string `json:"name"` Sno string `json:"sno"` } func main () { var s1 = Student{ ID: 1 , Gender: "男" , Name: "李四" , Sno: "s0001" , } var s, _ = json.Marshal(s1) jsonStr := string (s) fmt.Println(jsonStr) }
ORM框架也用到了反射技术, Mybatis就是半自动的ORM框架
ORM:对象关系映射 (Object Relational Mapping,简称 ORM)是通过使用描述对象和数据库 之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中
反射的介绍 反射是指在程序运行期间对程序本身进行访问和修改的能力。正常情况 程序在编译时, 变量被转换为内存地址, 变量名不会被编译器写入到可执行部分。在运行程序时, 程序无法获取自身的信息。
支持反射的语言 可以在程序编译期将变量的反射信息,如字段名称、类型信息、 结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运 行期获取类型的反射信息,并且有能力修改它们
Golang中反射可以实现的功能
反射可以在程序运行期间动态的获取变量的各种信息,比如变量的类型 类别
如果是结构体,通过反射还可以获取结构体本身的信息,比如结构体的字段、结构体的方法、结构体的 tag
通过反射,可以修改变量的值,可以调用关联的方法
Golang的变量分为两个部分
类型信息 :预先定义好的元信息
值信息 :程序运行过程中可动态变化的
在 GoLang 的反射机制中,任何接口值都由是一个具体类型 和具体类型的值 两部分组成的。
在 GoLang 中,反射的相关功能由内置的 reflect 包 提供,任意接口值在反射中都可以理解为 由 reflect.Type 和 reflect.Value 两部分组成, 并且reflect包提供了reflect.TypeOf 和 reflect.ValueOf 两个重要函数来获取任意对象的 Value 和 Type
reflect.TypeOf()获取任意值的类型对象 在 Go 语言中,使用 reflect.TypeOf()函数可以接收任意 interface{}参数,可以获得任意值的类型对象 (reflect.Type),程序通过类型对象可以访问任意值的类型信息
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" "reflect" ) func reflectType (x interface {}) { v := reflect.TypeOf(x) fmt.Printf("type:%v\n" , v) } func main () { var a float32 = 12.5 reflectType(a) var b int64 = 100 reflectType(b) }
type Name 和 type Kind 在反射中关于类型还划分为两种 :类型(Type)和种类(Kind)
因为在 Go 语言中我们可 以使用 type 关键字构造很多自定义类型,而种类(Kind)就是指底层的类型 ,
Go 语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空
在反射中, 当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind) . 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package mainimport ( "fmt" "reflect" ) type myInt int64 type Person struct { Name string Age int } type Animal struct { Name string } func reflectType (x interface {}) { t := reflect.TypeOf(x) fmt.Printf("TypeOf:%v Name:%v Kind:%v\n" , t, t.Name(), t.Kind()) } func main () { var a *float32 var b myInt var c rune reflectType(a) reflectType(b) reflectType(c) var d = Person{ Name: "crisp" , Age: 20 , } var e = Animal{ Name: "小花" , } reflectType(d) reflectType(e) var f = []int {1 , 2 , 3 , 4 , 5 } reflectType(f) }
在reflect包中定义的Kind类型如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 type Kind uint const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Ptr Slice String Struct UnsafePointer )
reflect.ValueOf() reflect.ValueOf()返回的是 reflect.Value 类型 ,其中包含了原始值的值信息。reflect.Value 与原 始值之间可以互相转换
reflect.Value 类型提供的获取原始值的方法如下:
方法
转换后的类型
说明
Interface()
interface {}
将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int()
int64
将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint()
uint64
将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float()
float64
将值以双精度(float64)类型返回,所有浮点数(float32、float64)均 可以此方式返回
Bool()
bool
将值以 bool 类型返回
Bytes()
[]bytes
将值以字节数组 []bytes
String()
string
将值以字符串类型返回
…
…
…
通过反射获取原始值示例 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "reflect" ) func reflectValue (x interface {}) { v := reflect.ValueOf(x) var c = v.Int() + 6 fmt.Println(c) } func main () { var a int64 = 100 reflectValue(a) }
通过反射获取原始值示例 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "fmt" "reflect" ) func reflectValue (x interface {}) { v := reflect.ValueOf(x) k := v.Kind() switch k { case reflect.Int64: fmt.Printf("type is int64, value is %d\n" , v.Int()) case reflect.Float32: fmt.Printf("type is float32, value is %f\n" , v.Float()) case reflect.Float64: fmt.Printf("type is float64, value is %f\n" , v.Float()) } } func main () { var a float32 = 3.14 var b int64 = 100 reflectValue(a) reflectValue(b) c := reflect.ValueOf(10 ) fmt.Printf("type c :%T\n" , c) }
通过反射设置变量的值
想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝 ,
必须传递变量地址才能修改变量值。而反射中使用专有的 Elem()方法来获取指针对应的值
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "reflect" ) func reflectSetValue1 (x interface {}) { v := reflect.ValueOf(x) if v.Kind() == reflect.Int64 { v.SetInt(200 ) } } func reflectSetValue2 (x interface {}) { v := reflect.ValueOf(x) if v.Elem().Kind() == reflect.Int64 { v.Elem().SetInt(200 ) } } func main () { var a int64 = 100 reflectSetValue2(&a) fmt.Println(a) }
结构体反射 与结构体相关的方法 任意值通过 reflect.TypeOf()获得反射对象信息后
如果它的类型是结构体,可以通过反射值对象(reflect.Type)的 **NumField()**和 Field()方法 获得结构体成员的详细信息
reflect.Type 中与获取结构体成员相关的的方法
方法
返回值
说明
Field(i int)
StructField
根据索引,返回索引对应的结构体字段的信息
NumField()
int
返回结构体成员字段数量
FieldByName(name string)
(StructField, bool)
根据给定字符串返回字符串对应的结构体字段的信息
FieldByIndex(index []int)
StructField
多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息
FieldByNameFunc(match func(string) bool)
(StructField,bool)
根据传入的匹配函数匹配需要的字段
NumMethod()
int
返回该类型的方法集中方法的数目
Method(int)
Method
返回该类型方法集中的第 i 个方法
MethodByName(string)
(Method,bool)
根据方法名返回该类型方法集中的方法
StructField 类型 StructField 类型用来描述结构体中的一个字段的信息。StructField 的定义如下:
1 2 3 4 5 6 7 8 9 type StructField struct { Name string PkgPath string Tag StructTag Offset uintptr Index []int Anonymous bool }
结构体反射示例 当我们使用反射得到一个结构体数据之后可以通过索引 依次获取其字段信息,也可以通过字段名 去获取指定的字段信息
获取结构体属性,获取执行结构体方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 package mainimport ( "fmt" "reflect" ) type Student struct { Name string `json:"name"` Age int `json:"age"` Score int `json:"score"` } func (s Student) GetInfo() string { var str = fmt.Sprintf("姓名:%v 年龄:%v 成绩:%v" , s.Name, s.Age, s.Score) fmt.Println(str) return str } func (s *Student) SetInfo(name string , age int , score int ) { s.Name = name s.Age = age s.Score = score } func (s *Student) Print() { fmt.Println("打印方法..." ) } func PrintStructField (s interface {}) { t := reflect.TypeOf(s) kind := t.Kind() if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct { fmt.Println("传入的不是结构体" ) return } field0 := t.Field(0 ) fmt.Println(field0.Name) fmt.Println(field0.Type) fmt.Println(field0.Tag.Get("json" )) field1, _ := t.FieldByName("Age" ) fmt.Println(field1.Name) fmt.Println(field1.Type) fmt.Println(field1.Tag.Get("json" )) num := t.NumField() fmt.Println("字段数量:" , num) } func PrintStructFn (s interface {}) { t := reflect.TypeOf(s) v := reflect.ValueOf(s) if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct { fmt.Println("传入的不是结构体" ) return } var tMethod = t.Method(0 ) fmt.Println(tMethod.Name) fmt.Println(tMethod.Type) fmt.Println(t.NumMethod()) v.MethodByName("Print" ).Call(nil ) var params []reflect.Value params = append (params, reflect.ValueOf("张三" )) params = append (params, reflect.ValueOf(22 )) params = append (params, reflect.ValueOf(100 )) v.MethodByName("SetInfo" ).Call(params) lue info := v.MethodByName("GetInfo" ).Call(nil ) fmt.Println(info) } func main () { stu1 := Student{Name: "小明" , Age: 15 , Score: 98 , } PrintStructFn(&stu1) }
修改结构体方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "fmt" "reflect" ) type Student struct { Name string `json:"name"` Age int `json:"age"` Score int `json:"score"` } func (s Student) GetInfo() string { var str = fmt.Sprintf("姓名:%v 年龄:%v 成绩:%v" , s.Name, s.Age, s.Score) return str } func reflectChangeStruct (s interface {}) { t := reflect.TypeOf(s) v := reflect.ValueOf(s) if t.Elem().Kind() != reflect.Struct { fmt.Println("传入的不是结构体指针类型" ) return } name := v.Elem().FieldByName("Name" ) name.SetString("李四" ) age := v.Elem().FieldByName("Age" ) age.SetInt(20 ) } func main () { stu1 := Student{ Name: "小明" , Age: 15 , Score: 98 , } reflectChangeStruct(&stu1) fmt.Println(stu1.GetInfo()) }
文件操作 打开和关闭文件 os.Open()函数能够打开一个文件,返回一个*File(文件指针) 和一个 err(错误信息)
这种方式打开文件是只读 的
操作完成文件对象以后一定要记得关闭文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" "os" ) func main () { file, err := os.Open("./main.go" ) if err != nil { fmt.Println("open file failed!, err:" , err) return } fmt.Println(file) defer file.Close() }
为了防止文件忘记关闭,我们通常使用 defer 注册文件关闭语句
file.Read()读取文件 Read 方法定义如下:
1 func (f *File) Read(b []byte ) (n int , err error )
它接收一个字节切片 ,返回读取的字节数 和可能的具体错误 ,读到文件末尾时会返回 0 和 io.EOF
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "fmt" "io" "os" ) func main () { file, err := os.Open("./main.go" ) if err != nil { fmt.Println("open file failed!, err:" , err) return } defer file.Close() var tmp = make ([]byte , 128 ) n, err := file.Read(tmp) if err == io.EOF { fmt.Println("文件读完了" ) return } if err != nil { fmt.Println("read file failed, err:" , err) return } fmt.Printf("读取了%d 字节数据\n" , n) fmt.Println(string (tmp[:n])) }
循环读取 使用 for 循环读取文件中的所有数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func main () { file, err := os.Open("./main.go" ) if err != nil { fmt.Println("open file failed!, err:" , err) return } defer file.Close() var content []byte var tmp = make ([]byte , 128 ) for { n, err := file.Read(tmp) if err == io.EOF { fmt.Println("文件读完了" ) break } if err != nil { fmt.Println("read file failed, err:" , err) return } content = append (content, tmp[:n]...) } fmt.Println(string (content)) }
bufio读取文件 bufio 是在 file 的基础上封装了一层 API,支持更多的功能, 支持按行读取
buf有缓冲的意思(buffer)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport ( "bufio" "fmt" "io" "os" ) func main () { file, err := os.Open("C:/test.txt" ) if err != nil { fmt.Println("open file failed, err:" , err) return } defer file.Close() reader := bufio.NewReader(file) for { line, err := reader.ReadString('\n' ) if err == io.EOF { if len (line) != 0 { fmt.Println(line) } fmt.Println("文件读完了" ) break } if err != nil { fmt.Println("read file failed, err:" , err) return } fmt.Print(line) } }
ioutil读取整个文件 io/ioutil 包的 ReadFile 方法能够读取完整的文件,只需要将文件名作为参数传入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "io/ioutil" ) func main () { content, err := ioutil.ReadFile("./main.go" ) if err != nil { fmt.Println("read file failed, err:" , err) return } fmt.Println(string (content)) }
文件写入操作 os.OpenFile()函数能够以指定模式 打开文件,从而实现文件写入相关功能
其中:
name :要打开的文件名 flag:打开文件的模式
perm: 文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01
模式有以下几种
模式
含义
os.O_WRONLY
只写
os.O_CREATE
创建文件
os.O_RDONLY
只读
os.O_RDWR
读写
os.O_TRUNC
清空
os.O_APPEND
追加
Write 和 WriteString 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "os" ) func main () { file, err := os.OpenFile("C:/test.txt" , os.O_CREATE|os.O_RDWR, 0666 ) if err != nil { fmt.Println("open file failed, err:" , err) return } defer file.Close() str := "你好 golang" file.Write([]byte (str)) file.WriteString("直接写入的字符串数据" ) }
bufio.NewWriter 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "bufio" "fmt" "os" ) func main () { file, err := os.OpenFile("C:/test.txt" , os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666 ) if err != nil { fmt.Println("open file failed, err:" , err) return } defer file.Close() writer := bufio.NewWriter(file) for i := 0 ; i < 10 ; i++ { writer.WriteString("你好 golang\r\n" ) } writer.Flush() }
ioutil.WriteFile 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" "io/ioutil" ) func main () { str := "hello golang" err := ioutil.WriteFile("C:/test.txt" , []byte (str), 0666 ) if err != nil { fmt.Println("write file failed, err:" , err) return } }
文件重命名 1 2 3 4 err := os.Rename("C:/test1.txt" , "D:/test1.txt" ) if err != nil { fmt.Println(err) }
复制文件 1. ioutil 进行复制 其实就是获取源文件的数据, 写入新的文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "fmt" "io/ioutil" ) func CopyFile (dstFileName string , srcFileName string ) (err error ) { input, err := ioutil.ReadFile(srcFileName) if err != nil { fmt.Println(err) return err } err = ioutil.WriteFile(dstFileName, input, 0644 ) if err != nil { fmt.Println("Error creating" , dstFileName) fmt.Println(err) return err } return nil } func main () { srcFile := "c:/test1.zip" dstFile := "D:/test1.zip" err := CopyFile(dstFile, srcFile) if err == nil { fmt.Printf("拷贝完成\n" ) } else { fmt.Printf("拷贝错误 err=%v\n" , err) } }
2. 方法流的方式复制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "io" "os" ) func CopyFile (dstFileName string , srcFileName string ) (err error ) { source, _ := os.Open(srcFileName) destination, _ := os.OpenFile(dstFileName, os.O_CREATE|os.O_WRONLY, 0666 ) buf := make ([]byte , 128 ) for { n, err := source.Read(buf) if err != nil && err != io.EOF { return err } if n == 0 { break } if _, err := destination.Write(buf[:n]); err != nil { return err } } } func main () { srcFile := "c:/000.avi" dstFile := "D:/000.avi" err := CopyFile(dstFile, srcFile) if err == nil { fmt.Printf("拷贝完成\n" ) } else { fmt.Printf("拷贝错误 err=%v\n" , err) } }
创建目录 创建一个目录 1 2 3 4 err := os.Mkdir("./abc" , 0666 ) if err != nil { fmt.Println(err) }
创建多级目录 1 2 3 4 err := os.MkdirAll("dir1/dir2/dir3" , 0666 ) if err != nil { fmt.Println(err) }
删除目录和文件 删除一个目录或文件 1 2 3 4 err := os.Remove("t.txt" ) if err != nil { fmt.Println(err) }
一次删除多个目录或文件 1 2 3 4 err := os.RemoveAll("aaa" ) if err != nil { fmt.Println(err) }