Golang基础知识笔记
# 一、GO变量 ## 1.1 变量的介绍 **变量概念** 变量相当于内存中的一个数据存储空间,可以将变量看做是一个房间的门牌号,通过门牌号我们可以找到房间,同样的道理,通过变量名可以访问到变量(值) **变量使用步骤** - 声明变量 (也叫定义变量) - 非变量赋值 - 使用变量 ## 1.2 变量快速入门案例 案例: 声明变量i,类型为int类型,并给变量i赋值数据10,最后打印量变量i ``` package main import "fmt" func main() { // 定义变量/声明变量 var i int //给 i赋值 i = 10 //使用变量 fmt.Println("i=",i) } ``` 结果 ![image.png](https://img.ukx.cn/abcdocker/2021/03/30/d40a9bbc8ddd5/d40a9bbc8ddd5.png) ## 1.3 变量的声明初始化和赋值 **声明变量** 基本语法: var 变量名 数据类型 `var a int` 声明了一个变量,变量名是a `var num1 float32` 声明了一个变量,表示一个单精度类型的小数,变量名是num1 **初始化变量** 在声明变量的时候就给值 `var a int =10` 初始化变量a并赋值10 如果使用类型推导,可以省略数据类型 `var b =100` **变量赋值** 比如先声明了变量: `var abc int` //默认为0 然后,在给值`abc = 100`,这就是给变量赋值 ## 1.4 变量使用注意事项 1. 变量表示内存中的一个存储区域 1. 该区域有自己的名称 (变量名)和类型(数据类型) 示意图: ![image.png](https://img.ukx.cn/abcdocker/2021/03/30/a33a5bdea0e43/a33a5bdea0e43.png) **Golang变量使用的三种方式** - 第一种: 指定变量类型,`声明后若不赋值,使用默认值` (默认值为0) ``` func main() { var i int fmt.Println("i=",i) } ``` - 第二种: 根据值自行判定变量类型 (**类型推导**) ``` func main() { var num = 10.11 fmt.Println("num=",num) } ``` - 第三种: 省略var,`:=`左侧的变量不应该是已经声明过的,否则编译报错 ``` func main() { name := "abcdocker" fmt.Println("name=", name) } ``` **1) 多变量声明** 在编写过程中,我们有时需要一次性声明多个变量,Golang也提供这样的语法 案例如下: ``` //演示Go如何一次性声明多个变量 func main() { var n1,n2,n3 int fmt.Println("n1=",n1,"n2=",n2,"n3=",n3) } ``` //一次性声明多个变量并赋值 ``` func main() { var n1,name,n3=100,"tom",888 fmt.Println("n1=",n1,"name=",name,"n3=",n3) } 结果如下: n1= 100 name= tom n3= 888 ``` //一次性声明多个变量,使用类型推导 ``` func main() { n1,name,n3:=100,"tomcat~",888 fmt.Println("n1=",n1,"name=",name,"n3=",n3) } 结果如下: n1= 100 name= tomcat~ n3= 888 ``` **2) 一次性声明全局变量 [在GO中函数外部定义变量就是去全局变量]** ``` package main import "fmt" var n1 =100 var n2 =200 var name = "abcdocker" func main() { fmt.Println("n1=",n1,"n2=",n2,"name=",name) } 结果如下: n1= 100 n2= 200 name= abcdocker ``` **3) 该区域的数据值可以在同一类型范围内不断变化,但是不可以改变数据类型** ``` package main import "fmt" func main() { //该区域的数据值可以在同一类型范围内不断变化 //正确案例 var i int = 10 i = 30 i = 40 fmt.Println("i=",i) //错误案例: 不可以改变数据类型 //1.10属于浮点数,不可以使用int整数类型 i = 1.2 //错误 } ``` **4) 变量在同一个作用域 (一个函数或者代码块)内不能重名** ``` package main import "fmt" func main() { var i int = 100 fmt.Println("i=", i) i := 20 ////变量名不允许重复 var i bool //变量名不允许重复 } ``` >变量=变量名+值+数据类型 Golang的变量如果没有赋值,编译器会使用默认值,比如int默认值为0,string默认值为空船,小数默认值为0 ## 1.5 程序中 + 号使用 1. 当左右两边都是数值型时,择做加法运算 1. 当左右两边都是字符串,则做字符串拼接 ``` package main import "fmt" func main() { var i = 1 var a = 2 var r = i + a //做加法运算 fmt.Println("r=",r) //string类型 var str1 = "hello " var str2 = "abcdocker" var res = str1 + str2 //做拼接操作 fmt.Println("res=", res) } ``` ![image.png](https://img.ukx.cn/abcdocker/2021/03/30/129d04ba2a98b/129d04ba2a98b.png) # 二、数据类型基本介绍 ![image.png](https://img.ukx.cn/abcdocker/2021/03/30/5e196c903fc92/5e196c903fc92.png) ## 2.1 整数类型 简单的来说,就是用于存放整数值的,比如0,-1,23456等等 | 序号 | 类型和描述 | | ------------ | ------------ | | 1 | **uint8** 无符号8位整型(0到255)| | 2 | ** unit16** 无符号16位整型(0到65534) | | 3 | ** uint32** 无符号32位整型 (0到4294967295 )| | 4 | **uint64** 无符号 64 位整型 (0 到 18446744073709551615) | | 5 | **int8** 有符号 8 位整型 (-128 到 127) | | 6 | **int16** 有符号 16 位整型 (-32768 到 32767)| | 7 | **int32** 有符号 32 位整型 (-2147483648 到 2147483647)| | 8 | **int64** 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)| // int ,uint,rune,byte使用 ``` package main import "fmt" func main() { var a int = 9000 fmt.Println("a=", a) var b uint = 1 var c byte = 255 fmt.Println("b=", b, "c=", c) } ``` **整数使用的细节** - golang各整数类型分: 有符号和无符号, int uint的大小和系统有关 - golang的整型默认声明为int 型 - golang程序中整型变量在使用时,遵守保大不保小的原则,即: 在保证程序正确运行下,尽量使用占用空间小的数据类型 `var age byte = 30` - bit: 计算机中的最小存储单位。1byte=8 bit ## 2.2 小数类型 小数类型就是用于存放有小数点的数字,比如`11.1 , 3.14149` **案例演示** ``` func main() { var price float32 = 11.19 fmt.Println("price=", price) } ``` **小数类型分类** | 序号 | 类型和描述 | | ------------ | ------------ | | 1 | **float32 IEEE-754** 32位浮点型数 | | 2 | **float64 IEEE-754** 64位浮点型数 | | 3 | **complex64** 32 位实数和虚数 | | 4 | **complex128** 64 位实数和虚数 | - 关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数为+尾数为 说明: 浮点数都是有符号的 - float64的精度比float32的要准确 - 如果我们要存储一个精度高的数(比如3.1415926)则应该选用float64 **小数类型使用细节** 1) golang浮点类型有固定的范围和长度,不受操作系统的影响 2) golang的浮点型默认声明为float64类型 ``` func main() { var num1 = 1.1 fmt.Printf("num1的数据类型是 %T \n",num1) } //输出结果 num1的数据类型是 float64 ``` ## 2.3 字符类型 字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。也就是说对于传统的字符串是由字符组成,而go的字符串不同,它是由字节组成的。 **案例演示** ``` func main() { var n1 byte = 'a' var n2 byte = '0' //字符0 //当我们直接输出byte值,就是输出了对应的字符串的码值 // 'a' ==> fmt.Println("n1=", n1) fmt.Println("n2=", n2) //如果我们希望输出对应字符,需要使用格式化输出 fmt.Printf("n1=%c n2=%c \n",n1,n2) } //输出结果 n1= 97 n2= 48 n1=a n2=0 ``` 对于上面代码说明 1)如果我们保存在字符的ASCII表的,比如[0-1,a-z,A-Z..] 可以直接保存在byte 2) 如果我们保存的字符对应码值大于255,这时我们考虑使用int类型保存 **字符类型使用细节** - 字符常量是用单引号(")括起来的单个字符。 - go中允许使用转义字符`\`来将其后的字符转变为特殊字符型常量 - go语音的字符使用UTF-8 (英文字符占用一个字节,汉字占用3个字节) - 在go中,字符的本质是一个证书,直接输出时,是该字符对应的UTF-8编码的值 - 可以直接给某个变量赋一个数字,然后按照格式化输出时%c,会输出该数字对应的unicode字符 - 字符类型是可以进行计算的,相当于一个证书,因为它都有对应的Unicode码 ``` func main() { //字符类型是可以进行运算的,相当于一个证书,运算时是按照码值进行运算 var h1 = 10 + 'a' //a的码值为97,相当于10+97 fmt.Println("h1=",h1) } ``` **字符类型本质探讨** - [x] 字符型存储到计算机中,需要将字符对应的码值(整数)找出来 存储: 字符--->对应码值--->二进制--->存储 读取: 二进制--->码值--->字符--->读取 - [x] 字符和码值的对应关系是通过字符编码表决定的 - [x] go语言的编码都统一成了utf-8 ## 2.4 布尔类型 **基本介绍** 1) 布尔类型也叫bool类型,bool类型数据只允许取true和false 2) bool类型占用1个字节 3) bool类型适用于`逻辑运算`,一般用于程序流程控制 **演示golang中bool类型使用** ``` func main() { var b = false fmt.Println("b=", b) } //输出结果为false ``` > bool类型只能取true 或者false (默认为false) ## 2.5 基本数据类型默认值 在go中,数据类型都有一个默认值,当变量没有赋值时,就会保留默认值 | 数据类型 | 默认值 | | ------------ | ------------ | | 整型 | 0 | | 浮点型 | 0 | | 字符串 | " " (空) | | 布尔类型 | false | ## 2.6 基本数据类型相互转换 Golang和java/c不同,go在不同类型的变量之间赋值时需要显式转换。也就是golang中数据类型不能自动转换 **基本语法** 表达式T(v)将值v转换为类型T `T`: 代表数据类型,比如int32,int64,float32等等 `v`:代表需要转换的变量 //基本数据类型转换案例 ``` package main import "fmt" func main() { var i int32 = 100 //将i转变成float var n1 float32 = float32(i) var n2 int8 = int8(i) var n3 int64 = int64(i) fmt.Printf("i=%v n1=%v n2=%v n3=%v \n", i, n1, n2, n3) } //输出结果: i=100 n1=100 n2=100 n3=100 ``` **基本数据类型相互转换的注意事项** 1) Go中,数据类型的转换可以是从 表示范围小-->表示范围大,也可以范围大-->范围小 2) 被转换的是变量存储的数据(即值),变量本身没有变化 3) 在转换中,比如讲int64转成in8 [`-128---127`],编译时不会报错,只是转换结果是按**溢出处理**,和我们希望的结果不一样。因此在转换时需要考虑范围 ``` func main() { var num1 int64 = 99999 var num2 int8 = int8(num1) fmt.Println("num2=", num2) } //输出结果 num2= -97 //不符合预期 ``` ## 2.7 package fmt mt包实现了类似C语言printf和scanf的格式化I/O。格式化动作('verb')源自C语言但更简单。 为了以后对格式化输出有更详细的了解,这里整理了一些fmt通用参数 - **通用:** | 序号 | 参数 | 说明 | | ------------ | ------------ | ------------ | | 1 | %v | 值的默认格式表示 | | 2 | %+v | 类似%v,但输出结构体时会添加字段名 | | 3 | %#v | 值的Go语法表示 | | 4 | %T | 值的类型的Go语法表示 | | 5 | %% | 百分号 | - **布尔值:** ``` %t 单词true或false ``` - **整数:** | 序号 | 参数 |说明 | | ------------ | ------------ | ------------ | | 1 | %b | 表示为二进制 | | 2 | %c | 该值对应的unicode码值 | | 3 | %d | 表示为十进制 | | 4 | %o | 表示为八进制 | | 5 | %q | 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示 | | 6 | %x | 表示为十六进制,使用a-f | | 7 | %X |表示为十六进制,使用A-F| | 8 | %U | 表示为Unicode格式:U+1234,等价于"U+%04X" | - **浮点数与复数:** | 序号 | 参数 | 说明 | | ------------ | ------------ | ------------ | | 1 | %b | 无小数部分、二进制指数的科学计数法,如-123456p-78; | | 2 | %e | 科学计数法,如-1234.456e+78 | | 3 | %E | 科学计数法,如-1234.456E+78 | | 4 | %f | 有小数部分但无指数部分,如123.456| | 5 | %F | 等价于%f | | 6 | %g | 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出) | | 7 | %G | 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出) | - **字符串和[]byte:** | 序号 | 参数 | 说明 | | ------------ | ------------ | ------------ | | 1 | %s | 直接输出字符串或者[]byte | | 2 | %q | 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示 | | 3 | %x | 每个字节用两字符十六进制数表示(使用a-f) | | 4 | %X | 每个字节用两字符十六进制数表示(使用A-F) | 转载: https://studygolang.com/pkgdoc ## 2.8 基本数据类型和string转换 方式1: `fmt.Sprintf("%参数",表达式)` ``` func Sprintf(format string, a ... interface{}) string Sprintf根据format参数生成格式化的字符串并返回该字符串,参数需要和表达式的数据类型匹配 ``` fmt.Sprintf案例演示 ``` package main import "fmt" func main() { //定义数据类型 var num1 int64 = 99999 var num2 int8 = int8(num1) var b bool = true var mychar byte = 'a' fmt.Println("num2=", num2) //定义空值 var str string str = fmt.Sprintf("%d", num1) fmt.Printf("str type %T str=%q \n", str, str) str = fmt.Sprintf("%f", num2) fmt.Printf("str type %T str=%q \n", str, str) str = fmt.Sprintf("%t", b) fmt.Printf("srr type %T str=%q \n", str, str) str = fmt.Sprintf("%c", mychar) fmt.Printf("str type %T str=%q \n", str, str) } //输出结果如下 ➜ 01 go run main.go num2= -97 str type string str="99999" str type string str="%!f(int8=-97)" srr type string str="true" str type string str="a" ``` 在将String类型转换成基本数据类型时,要确保string类型能够转成有效的数据,例如可以将`123`转成一个整数,但是不能把`hello`转成一个证书,如果强制转换,golang直接将其结果转成0 ## 2.9 指针 1) 基本数据类型,变量存的就是值,也叫值类型 2) 获取变量的地址用`&`,例如:`var num int`获取num地址:`&num` ![image.png](https://img.ukx.cn/abcdocker/2021/03/31/a781dd55cbab9/a781dd55cbab9.png) 3) 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值 ``` var ptr *int = &num ``` ![image.png](https://img.ukx.cn/abcdocker/2021/03/31/b75ddc151a273/b75ddc151a273.png) 4) 获取指针类型所指向的值,使用: * ``` package main import "fmt" func main() { //基本数据类型在内存布局 var i int = 10 fmt.Println("i的地址=", &i) //1. ptr是一个指针变量 //2. ptr的类型 *int //3. ptr 本身的值&i var ptr *int = &i fmt.Printf("ptr=%v \n",ptr) fmt.Printf("ptr 的地址=%v \n", &ptr) fmt.Printf("ptr 指向的值=%v \n", *ptr) } //输出结果 ➜ 01 go run main.go i的地址= 0xc0000b4008 ptr=0xc0000b4008 ptr 的地址=0xc0000ae020 ptr 指向的值=10 ``` ![image.png](https://img.ukx.cn/abcdocker/2021/03/31/e9696bcc1d547/e9696bcc1d547.png) **案例演示** 1) 写一个程序,获取一个int变量num的地址,并显示到终端 2) 将num的地址赋给指针ptr,并通过ptr去修改num的值 ``` package main import "fmt" func main() { var num int = 10 fmt.Printf("num address=%v \n", &num) //ptr变量 var ptr *int ptr = &num *ptr = 10 //这里修改,会到num值的变化 fmt.Println("num =", num) fmt.Println("ptr =", &ptr) } //输出结果 ➜ 01 go run main.go num address=0xc0000b4008 num = 10 ptr = 0xc0000ae020 ``` **指针使用细节** 1) 值类型,都有对应的指针类型,形式为***数据类型**,比如int的对应的指针就是 \*int,float32对应的指针类型就是 \*float32,以此类推 2)值类型包括: 基本数据类型**int系列,float系列,bool,string、数组和结构体struct** ## 2.10 值类型和引用类型 - 值类型: 基本数据类型int系列,float系列,bool,string、数组和结构体struct - 引用类型: 指针、slice切片、map、管道chan、interface都是引用类型 **值类型和引用类型的使用特点** 1) 值类型: 变量直接存储值,内存通常在栈中分配 2) 引用类型: 变量存储的是一个地址,这个地址对应的空间才是真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就称为一个垃圾,由GC来回收 3) 内存占和堆区示意图 ![image.png](https://img.ukx.cn/abcdocker/2021/03/31/9c5949b7670b6/9c5949b7670b6.png) ## 2.11 标示符的命名规则 1) 由26个英文字母大小写,0-9,\_组成 2) 数据不可以开头。`var 3num int ` ❌ 3) Golang中严格区分大小写 4) 标示符不能包含空格 5) 下划线`_`本身在Go中是一个特殊的标示符,称为`空标示符`。可以代表任何其他的标示符,但是它对应的值会被忽略。所以**仅能被作为占位符使用,不能作为标示符** 6) 不能以系统保留关键字作为标示符(一共有25个),比如`if`、`break`、`for`等 # 三、运算符 运算符是一种特殊的符号,用以表示数据的运算、复制和比较 运算符有以下分类 - 算术运算符 - 赋值运算符 - 比较运算符/关系运算符 - 逻辑运算符 - 位运算符 - 其它运算符 ## 3.1 算术运算符 算术运算符是对数值类型的变量进行计算的,比如: 加减乘除 |运算符 | 运算 | 范例 | 结果 | | ------------ | ------------ | ------------ | ------------ | | + | 正号 | +3 | 3 | | - | 负号 | -4 | 4 | | + | 加 | 5+5 | 10 | | - | 减 | 10-5 | 5 | | * | 乘 | 3*4 | 12 | | / | 除 | 5/5 | 1 | | % | 取模(取余) |7% 5 | 2 | | ++ | 自增 | a=2 a++ | a=3 | | -- | 自减 | a=2 a-- | a=1 | | + | 字符串相加 | "hello" + "abcdocker" | "hello abcdocker" | **案例演示** //演示/的使用特点 ``` //演示/的使用特点 package main import "fmt" func main() { //如果运算符都是整数,那么除后,去掉小数部分,保留整数部分 fmt.Println(10 / 4) var n1 float32 = 10 / 4 fmt.Println(n1) //如果希望保留小数部分,则需要有浮点数参与运算 var n2 float32 = 10.0 / 4 fmt.Println(n2) } //输出结果 ➜ 01 go run main.go 2 2 2.5 ``` //演示%的使用特点 ``` package main import "fmt" func main() { //%公式 a%b = a-a / b * b fmt.Println("10%3=", 10%3) // = 1 fmt.Println("-10%3=", 10%3) //= -10 -(-10)/ 3*3 = -10-(-9) = -1 fmt.Println("10%-3=", 10%-3) } //输出结果 ➜ 01 go run main.go 10%3= 1 -10%3= 1 10%-3= 1 ``` // ++ -- 的使用 ``` package main import "fmt" func main() { var i int = 10 i++ //等价 i = i+1 fmt.Println("i=", i) i-- //等价i = i -1 fmt.Println("i=", i) } //输出结果 ➜ 01 go run main.go i= 11 i= 10 ``` **算术运算符使用注意事项** 1) 对于除号`/`,它的整数除和小数除是有区别的: 整数之间做除法时,只保留整数部分而舍弃小数部分。例如:`x:=19/5`,结果是3 2) 当对一个数取模时,可以等价`a%b=a-a/b*b` 3) Golang中的自增和自建不可以以下使用方式 ` a = i++`❌ `a=i--`❌ `if i++ >0`❌ 4) Golang中i++和i--只能写在变量后面,不能写在变量前面 `++i`❌ `--i`❌ ### 3.1.1 算术运算符练习题 - 假如还有97天放假,问: xxx个星期零xx天 ``` package main import "fmt" func main() { var days int = 97 var week int = days / 7 var day int = days % 7 fmt.Printf("%d个星期零%d天\n", week, day) } //输出结果如下 ➜ 01 go run main.go 13个星期零6天 ``` - 定义一个变量保存华氏温度,华氏温度转换摄氏温度的公式为: 5/9*(华氏摄氏度-100),请求出华氏温度对应的摄氏温度 ``` package main import "fmt" func main() { var huashi float32 = 134.2 var sheshi float32 = 5.0 / 9 * (huashi - 100) fmt.Printf("%v 对应的摄氏温度%v \n" , huashi,sheshi) } //输出结果 ➜ 01 go run main.go 134.2 对应的摄氏温度19 ``` ## 3.2 关系运算符 1. 关系运算符的结果都是bool型,也就是要么是true,要么是false 1. 关系表达式 通常在 **if结构**的条件中或**循环结构**的条件中 ![image.png](https://img.ukx.cn/abcdocker/2021/03/31/c896d51d4a476/c896d51d4a476.png) >1) 关系运算符的结果都是bool型,也就是要么是true,要么是false >2) 关系运算符组成的表达式,我们称之为关系表达式 : a >b >3) 比较运算符 "==" 不能误写"=" ## 3.3 逻辑运算符 用于连接多个条件(一般来讲就是关系表达式),最终的结果也是一个bool值 > 假定A值为True,B值为False | 运算符 | 描述 | 实例 | | ------------ | ------------ | ------------ | | && | 逻辑`与`运算符。如果两边的操作数都是True,则为True,否则为False | (A && B)为False | | 丨丨 | 逻辑`或`运算符。如果两边的操作数有一个True,则为True,否则False | (a 丨丨 b)为true | | ! | 逻辑`非`运算符。如果条件为True,则逻辑为False,否则为true | !(a && b)为true | **案例演示** //演示逻辑运算符`&&`使用 ``` package main import "fmt" func main() { var age int = 40 if age > 30 && age < 50 { fmt.Println("OK") } } ``` //演示逻辑运算符`||`使用 ``` package main import "fmt" func main() { var age int = 40 if age > 30 || age < 50 { fmt.Println("OK") } } ``` **注意事项** (1) && 也叫短路与: 如果第一个条件为false,则第二个条件不会判断,最终结果为false (2) || 也叫短路或: 如果第一个条件为true,则第二个条件不会判断,最终结果为true ## 3.4 赋值运算符 赋值运算符就是讲某个运算后的值,赋给指定的变量 | 运算符 | 描述 | 实例 | | ------------ | ------------ | ------------ | | = | 简单的赋值运算符,将一个表达式的值赋给一个左值 | C = A +B 将A +B表达式结果赋值给C| | += | 相加后在赋值 | C +=A 等于C = C+A | | -= | 相减后在赋值 | C-=A 等于C = C -A | | *= | 相乘后在赋值 | C *=A 等于C =C * A | | /= | 相除后在赋值 | C /=A 等于C = C/A | | %= | 求余后在赋值 | C %=A 等于 C= C % A | ## 3.5 位运算符 | 描述符 | 描述 | | ------------ | ------------ | | & |按位与运算符"&"是双目运算符。其功能是参与运算符的两数各对应的二进制位相与 。运算规则是:同时为1,结果为1,否则为0 | | 丨 | 按位或运算符"丨"是双目运算符。其功能是参与运算的两数各自对应的二进制相或。 运算规则是: 有一个为1,结果为1,否则为0 | | ^ | 按位异或运算符"^"是双目运算符。其功能是参与运算的两数各对应的二进制位相异或。运算规则是: 当二进位不同时,结果为1,否则为0 | | << | 左移运算符"<<" 是双目运算符。 其功能是把"<<"左边的运算符的各二进制位全部左移若干位,高位丢弃,低位补0。左移n位就是乘以2的n次方| | >> | 右移运算符">>" 是双目运算符。 其功能是把">>" 运算的运算数的各二进制全部右移若干位,右移n位就是除以2的n次方 | ## 3.6 其他运算符说明 | 运算符 | 描述 | 实例 | | ------------ | ------------ | ------------ | | & | 返回变量存储地址 | &a;将给出变量的实际地址 | | * | 指针变量 | *a;是一个指针变量 | **案例演示** ``` package main import "fmt" func main() { a := 100 fmt.Println("a的地址=", &a) var ptr *int = &a fmt.Println("ptr指向的值是=", *ptr) } //输出结果 ➜ 01 go run main.go a的地址= 0xc00001a090 ptr指向的值是= 100 ``` ## 3.7 运算符优先级 由高至低 ![image.png](https://img.ukx.cn/abcdocker/2021/03/31/1a415f5145de0/1a415f5145de0.png) ## 3.8 键盘输入语句 在开发过程中,需要接受用户输入语句,可以使用`Scanln`进行引用 **案例演示** 要求: 可以从前台接受用户信息, [姓名,年龄,薪水,是否通过考试] 1)使用`fmt.Scanln()`获取 ``` package main import "fmt" func main() { var name string var age byte var sal float32 var isPass bool fmt.Println("请输入姓名 ") fmt.Scanln(&name) fmt.Println("请输入年龄") fmt.Scanln(&age) fmt.Println("请输入薪水") fmt.Scanln(&sal) fmt.Println("请输入是否通过考试[true & false]") fmt.Scanln(&isPass) fmt.Printf("您的名字是:%v\n 您的年龄是:%v \n 您的薪水是:%v \n 是否通过考试:%v \n", name, age, sal, isPass) } //输入内容 请输入姓名 abcdocker 请输入年龄 20 请输入薪水 10000 请输入是否通过考试[true & false] true //输出内容 您的名字是:abcdocker 您的年龄是:20 您的薪水是:10000 是否通过考试:true ``` 2)使用`fmt.Scanf()`获取 ``` package main import "fmt" func main() { var name string var age byte var sal float32 var isPass bool fmt.Println("请输入您的姓名,年龄,薪水,是否通过考试,请使用空格隔开") fmt.Scanf("%s %d %f %t", &name, &age, &sal, &isPass) fmt.Printf("名字是%v \n年龄是%v\n薪水是%v\n是否通过考试%v\n", name, age, sal, isPass) } //输入内容 ➜ 01 go run main.go 请输入您的姓名,年龄,薪水,是否通过考试,请使用空格隔开 abcdocker 19 10000 false //输出内容 名字是abcdocker 年龄是19 薪水是10000 是否通过考试false ``` # 四、进制 对于整数,有四种表示方式。 1) 二进制: 0,1,满2进1 在go中,不能直接使用二进制来表示一个证书,它沿用了c的特点 2) 十进制: 0-9,满10进1 3) 八进制: 0-7,满8进1,以数字0开头表示 4) 十六进制: 0-9及A-F,满16进1,以0x或者0X开头表示 >此处A-F不区分大小写 ## 4.1 进制转换 ## 4.2 其他进制转十进制 ## 4.3 二进制如何转十进制 ## 4.4 位运算 # 五、程序流程控制 在程序中,程序运行的流程控制决定程序是如何执行的,主要有三大流程控制语句 - [x] 顺序控制 - [x] 分支控制 - [x] 循环控制 ## 5.1 顺序控制 程序从上到下逐行地执行,中间没有任何判断和跳转 ``` //案例,顺序执行 package main import "fmt" func main() { var name string var age byte var sal float32 var isPass bool fmt.Println("请输入姓名 ") fmt.Scanln(&name) fmt.Println("请输入年龄") fmt.Scanln(&age) fmt.Println("请输入薪水") fmt.Scanln(&sal) fmt.Println("请输入是否通过考试[true & false]") fmt.Scanln(&isPass) fmt.Printf("您的名字是:%v\n 您的年龄是:%v \n 您的薪水是:%v \n 是否通过考试:%v \n", name, age, sal, isPass) } ``` ![image.png](https://img.ukx.cn/abcdocker/2021/04/01/30c5f5a63ea97/30c5f5a63ea97.png) Go中定义变量时采用合法的前向引用 ``` //正确操作✔️ package main import "fmt" func main() { var num1 int = 10 //声明了num1 var num2 int = num1 + 20 //使用了num1 fmt.Println(num2) } //错误方式❎ package main import "fmt" func main() { var num2 int = num1 + 20 //使用了num1 var num1 int = 10 //声明了num1 fmt.Println(num2) } ``` ## 5.2 分支控制 分支控制就是让程序有选择执行,有三种形式 1) 单分支 2) 双分支 3) 多分支 **单分支控制** ``` //基本语法 if 条件表达式 { 执行代码块 } //当条件表达式为true时,就会执行{}的代码。注意{}是必须要有 ``` >案例: 编写一个程序,可以输入年了,如果年龄大于18,则输出"你的年了大于18" ``` package main import "fmt" func main() { var age byte fmt.Println("请输入您的年龄") fmt.Scanln(&age) if age > 18 { fmt.Println("你的年龄大于18") } } //输出结果 ➜ 01 go run main.go 请输入您的年龄 20 你的年龄大于18 ``` 单分支流程图 ![image.png](https://img.ukx.cn/abcdocker/2021/04/01/e2c316204810a/e2c316204810a.png) **双分支控制** 基本语法 ``` if 条件表达式{ 执行代码块1 }else { 执行代码块2 } //说明: 当条件表达式成立,即执行代码块1,否则执行代码块2. {}必须要有的 ``` >案例: 编写一个程序,可以输入年了,如果大于18岁则输出"您已经大于18岁",否则输出"未成年" ``` package main import "fmt" func main() { var age byte fmt.Println("请输入您的年龄") fmt.Scanln(&age) if age > 18 { fmt.Println("你的年龄大于18") } else { fmt.Println("未成年") } } //输出结果 ➜ 01 go run main.go 请输入您的年龄 12 未成年 ➜ 01 go run main.go 请输入您的年龄 20 你的年龄大于18 ``` >双分支只会执行其中一个分支 **多分支控制** 基本语法 ``` if 条件表达式1 { 执行代码块1 }else if 条件表达式2{ 执行代码块2 } ... else { 执行代码块n } ``` 对于基本语法的说明 (1) 多分支的判断流程如下 - 1.1 先判断条件表达式1是否成立,如果为真,就执行代码1 - 1.2 如果条件表达式1为假,就去判断条件表达式2是否成立,如果条件表达式2位真,就执行代码块2 - 1.3 以此类推 - 1.4如果所有的条件表达式不成立,则执行else的语句块 (2) else 不是必须的 (3) 多分支只能有一个执行入口 **多分支案例** > 小明参考考试当成绩是以下结果是进行奖励 > 成绩为100分时,奖励BMW >成绩为(80,99)时,奖励一台iPhone X >成绩为(60,80时),奖励iPad 其他时没有奖励 请从键盘输入成绩,并加以判断 ``` package main import "fmt" func main() { var fenshu int fmt.Println("请输入查询分数") fmt.Scanln(&fenshu) if fenshu >= 100 { fmt.Println("100分,牛逼") } else if fenshu >= 80 && fenshu <= 90 { fmt.Println("该加油了") } else if fenshu >= 60 && fenshu <= 80 { fmt.Println("等着挨揍吧") } else { fmt.Println("重开吧") } } //输出结果 ➜ exec1 go run main.go 请输入查询分数 50 重开吧 ``` >案例2: 参加百米运动会,用时8秒内进入决赛,否则提示淘汰。根据性别提示男子组或女子组 ``` package main import "fmt" func main() { var miao float64 fmt.Println("请输入秒数") fmt.Scanln(&miao) var nannv string fmt.Println("请输入性别") fmt.Scanln(&nannv) if miao < 8 { if nannv == "nan" { fmt.Println("男子组") } else { fmt.Println("女子组") } fmt.Println("恭喜入围,用时", miao, "秒") } else { fmt.Println("您已淘汰") } } //输出结果 ➜ exec3 go run main.go 请输入秒数 6 请输入性别 nan 男子组 恭喜入围,用时 6 秒 ➜ exec3 go run main.go 请输入秒数 9 请输入性别 nv 您已淘汰 ``` >案例,根据淡旺季的月份和年龄打印票价 > 4—10 旺季 成人(18-60) :60 儿童(<18) :半价 老人(>60): 1/3 淡季: 成人40 其他20 ``` package main import "fmt" func main() { var age int var month int var price float64 = 60.0 fmt.Println("请输入月份") fmt.Scanln(&month) fmt.Println("请输入年龄") fmt.Scanln(&age) if month >= 4 && month <= 10 { if age > 60 { fmt.Printf("%v月 票价%v 年龄%v \n", month, price/3, age) } else if age >= 18 { fmt.Printf("%v月 票价%v 年龄%v \n", month, price, age) } else { fmt.Printf("%v月 票价%v 年龄%v \n", month, price/2, age) } } else { if age >= 18 && age < 60 { fmt.Println("淡季成人票价40") } else { fmt.Println("淡季儿童和老人 票价20") } } } //输出结果 ➜ exec4 go run main.go 请输入月份 8 请输入年龄 20 8月 票价60 年龄20 ➜ exec4 go run main.go 请输入月份 1 请输入年龄 60 淡季儿童和老人 票价20 ``` ## 5.3 SWITCH 分支控制 switch 语句用于基于不同条件执行不同动作,每一个case分支都是唯一的,从上到下逐一测试,直到匹配为止。 匹配项后面也不需要再加**break** ``` //基本语法 switch 表达式{ case 表达式1,表达式2,....: 语句块 case 表达式3,表达式4, ....: 语句块 default: 语句块 } ``` - [x] switch的执行流程是,先执行表达式,得到值,然后和case的表达式进行比较,如果相等,就匹配,然后执行对应case的语句块,最后退出switch控制 - [x] 如果switch的表达式的值没有和任何的case的表达式匹配成功,则执行default的语句块。 - [x] golang的case后的表达式可以有多个,使用逗号间隔 - [x] golang中的case语句块不需要写break,因为默认会有,即在默认情况下,当程序执行完case语句后,就直接退出该switch控制结构 >switch 案例: 请编写一个程序,该程序可以接受一个字符,比如:a,b,c,d,f a表示星期一,b表示星期二 ... 根据用户的呼入显示相依的信息,要求使用switch局域完成 ``` package main import "fmt" func main() { var zm string fmt.Println("请输入a,b,c,d") fmt.Scanln(&zm) switch zm { case "a": fmt.Println("周一") case "b": fmt.Println("周二") case "c": fmt.Println("周三") default: fmt.Println("请重新输入") } } //输出结果 ➜ 01 go run main.go 请输入a,b,c,d a 周一 ➜ 01 go run main.go 请输入a,b,c,d b 周二 ``` switch使用细节 1)case/switch后是一个表达式(即: 常量值、变量、一个有返回的函数都可以) 2)case 后的各个表达式的值的数据类型,必须和switch的表达式数据类型一致 3)case后面可以带多个表达式,使用逗号间隔。比如case 表达式1,表达式2 4)case后面的表达式如果是常量值,则要求不能重复 5)case后面不需要带break,程序匹配到一个case后就会执行对应的代码块,然后退出switch,如果一个都匹配不到,则执行default 6)default语句不是必须的 7)switch后也可以不带表达式,类似if --else分支来使用 `case age > 90:` 8)switch后也可以直接声明/定义一个变量,分号结束`switch age := 90;` 9)switch穿透`-fallthrough`,如果在case语句块后增加allthrough,则会继续执行下一个case > 案例: 对学生成绩大于60分,输出合格。低于60分的,输出不合格 (输入成绩不能大于100) ``` //第一种方式 package main import "fmt" func main() { var cj byte fmt.Println("请输入分数") fmt.Scanln(&cj) switch { case cj > 60 && cj < 101: fmt.Println("成绩合格") case cj < 60: fmt.Println("成绩不合格") default: fmt.Println("请重新输入") } } //输出结果 ➜ 02 go run main.go 请输入分数 100 请重新输入 ➜ 02 go run main.go 请输入分数 59 成绩不合格 ➜ 02 ``` > 案例: 根据用户指定月份,打印该月份所属的集结。 3,4,5为春季,6,7,8位夏季,9,10,11 秋季,12,1,2位冬季 ``` package main import "fmt" func main() { var ji byte fmt.Println("请输入月份 [1..12]") fmt.Scanln(&ji) switch ji { case 3, 4, 5: fmt.Println("春季") case 6, 7, 8: fmt.Println("夏季") case 9, 10, 11: fmt.Println("秋季") case 12, 1, 2: fmt.Println("冬季") default: fmt.Println("请重新输入") } } //输出结果 ➜ 03 go run main.go 请输入月份 [1..12] 4 春季 ``` **switch和if的比较** 1. 如果判断的具体数值不多,而且符合整数、浮点数、字符、字符串这几种类型。建议使用switch语句,简洁高效 1. 其他情况,对区间判断和结果为bool类型的判断,使用if,if的适用范围更广。 ## 5.4 FOR 循环控制 for循环语法格式 ``` for 循环变量初始化; 循环条件;循环变量迭代{ 循环操作(语句) } ``` >案例: for 循环打印10行 ``` package main import "fmt" func main() { for i := 1; i <= 10; i++ { fmt.Println("hellow") } } //输出结果 ➜ 01 go run main.go hellow hellow ... //省略 hellow hellow ``` 语法格式说明 - 循环遍历初始值 - 循环条件 - 循环操作(语句),有人也叫循环体 - 循环变量迭代 for循环执行的顺序说明: - 执行循环变量初始化,`i :=1` - 执行循环条件,`i <=10` - 如果循环条件为真,就执行循环操作 ,`fmt.Println("abcdocker")` - 执行循环变量迭代,`i++`可以解析为(i = i +1) - 反复执行 2,3,4 步骤,执行循环为Fakse,就退出循环 ![image.png](https://img.ukx.cn/abcdocker/2021/04/01/32d67146efc55/32d67146efc55.png) **for循环使用的注意事项** 1)循环条件是返回一个布尔值的表达式 2)for循环的第三种使用方式 ``` for 循环判断条件{ //循环执行语句 } //将变量初始化和变量迭代写到其他位置 ``` >案例演示 ``` package main import "fmt" func main() { j := 1 //循环变量初始化 for j <= 10 { fmt.Println("您好,abcdocker", j) j++ } } //输出结果 ➜ 01 go run main.go 您好,abcdocker 1 您好,abcdocker 2 您好,abcdocker 3 您好,abcdocker 4 您好,abcdocker 5 您好,abcdocker 6 您好,abcdocker 7 您好,abcdocker 8 您好,abcdocker 9 您好,abcdocker 10 ``` **for循环的第三方使用方式** ``` for { //循环执行语句 } ``` 上面的写法等价for ;;{} 是一个无线循环,通常需要配置break语句使用 ``` package main import "fmt" func main() { k := 1 //循环变量初始化 for { if k <= 10 { fmt.Println("ok~", k) } else { break } k++ } } //输出结果 ➜ 01 go run main.go ok~ 1 ok~ 2 ok~ 3 ok~ 4 ok~ 5 ok~ 6 ok~ 7 ok~ 8 ok~ 9 ok~ 10 ``` >案例: 打印机1 ~ 100直接所有是9的倍数的整数的个数及总和 ``` package main import "fmt" func main() { // 思路分析 // 1. 使用for 循环对max进行遍历 // 2. 当一个数是9% ==0 就是9的倍数 // 3. 我们需要声明两个变量count和sum来保存个数和总和 var max uint64 = 100 var count uint64 = 0 var sum uint64 = 0 var i uint64 = 1 for ; i <= max; i++ { if i%9 == 0 { count++ sum += i } } fmt.Printf("count=%v sum=%v \n", count, sum) } //输出结果 ➜ 01 go run main.go count=11 sum=594 ``` >案例: 完成下面表达式的输出 > 0 + 6 = 6 > 1 + 5 = 6 > 2 + 4 = 6 > 3 + 3 = 6 > 4 + 2 = 6 > 5 + 1 = 6 > 6 + 0 = 6 ``` package main import "fmt" func main() { var n int = 6 for i := 0; i <= n; i++ { fmt.Printf("%v + %v = %v \n", i, n-i, n) } } //输出结果 ➜ 01 go run main.go 0 + 6 = 6 1 + 5 = 6 2 + 4 = 6 3 + 3 = 6 4 + 2 = 6 5 + 1 = 6 6 + 0 = 6 ``` ## 5.5 WHILE和DO..WHILE的实现 Go语言没有while和do..while语法,可以通过for循环来实现其使用效果。 ``` //循环变量初始化 for { if 循环条件表达式{ break //跳出for循环.. } 循环操作(语句) 循环变量迭代 } ``` >案例演示 使用while实现输出10句 " Hello ,word" ``` package main import "fmt" func main() { var i int = 1 for { if i >10 { //循环条件 break //跳出for循环,结束for循环 } fmt.Println("hello word",i) i++ //循环变量迭代 } } ``` **使用do..while完成输出10句 ok** ``` package main import ( "fmt" ) func main() { var a int = 1 for { fmt.Println("hello ok", a) a++ //循环变量迭代 if a > 10 { break //a >10之后就跳出for循环 } } } ``` **do..while说明** ``` 循环变量初始化 for { 循环操作(语句) 循环变量迭代 if 循环条件表达式{ break //跳出for循环 } } ``` ## 5.6 多重循环控制 1) 将一个循环放在另一个循环体内,就形成了嵌套循环。在外面的for称为外层循环在里面的for循环称为内层循环。 2) 实际上,嵌套循环就是把内循环当成外循环的循环体。当只有内层循环的循环条件为false时,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次的循环 3) 外层循环次数为m次,内层为n此,则内循环体实际上需要执行m*n次 >应用案例一 ``` # 统计3个班学习情况,每个班有5名同学,求出各个班的平均分和所有班级的平均分[学生成绩从键盘输入] //第一步: 分析思路 // 1.先统计一个班的数据,学生有5名 // 2.要求从键盘输入 // 3.sum要计算出总分和平均分 package main import "fmt" func main() { var sum float64 = 0.0 //定义总数 for i := 1; i <= 5; i++ { var fen float64 = 0.0 //每个学生的分数 fmt.Printf("请输出 %v 学生的分数 \n", i) fmt.Scanln(&fen) sum += fen } fmt.Printf("全班的总分为 %v 平均分为%v \n", sum, sum/5) } //第一步已经完成一个班的总数和平均分 //输出结果如下 ➜ 01 go run main.go 请输出 1 学生的分数 10 请输出 2 学生的分数 10 请输出 3 学生的分数 10 请输出 4 学生的分数 10 请输出 5 学生的分数 10 全班的总分为 50 平均分为10 // 第二步: 统计3个班的数据 // 1.统计3个班的数据,每个班有5名同学,求出每个班的平均分 // 2. j表示第几个班级 // 3.定义一个变量存放总成绩 package main import "fmt" func main() { // totalSum 所有班总成绩 var totalSum float64 = 0.0 // j表示第几个班级 for j := 1; j <= 3; j++ { var sum float64 = 0.0 //定义总数 for i := 1; i <= 5; i++ { var fen float64 = 0.0 //每个学生的分数 fmt.Printf("请输入第%d班,第 %d 个学生的分数 \n", j, i) fmt.Scanln(&fen) // sum为总数,fen是每个学生的分数,加在一起就是这个班级的总分 sum += fen } // 计算所有班级的总成绩到totalSum totalSum += sum fmt.Printf("第%d 班级,平均分为%v \n", j, sum/5) } fmt.Printf("各个班的总成绩是%v 各个班级的平均分是%v \n", totalSum, totalSum/(5*3)) } //输出结果如下 ➜ 01 go run main.go 请输入第1班,第 1 个学生的分数 10 请输入第1班,第 2 个学生的分数 10 请输入第1班,第 3 个学生的分数 10 请输入第1班,第 4 个学生的分数 10 请输入第1班,第 5 个学生的分数 10 第1 班级,平均分为10 请输入第2班,第 1 个学生的分数 20 请输入第2班,第 2 个学生的分数 20 请输入第2班,第 3 个学生的分数 20 请输入第2班,第 4 个学生的分数 20 请输入第2班,第 5 个学生的分数 20 第2 班级,平均分为20 请输入第3班,第 1 个学生的分数 30 请输入第3班,第 2 个学生的分数 30 请输入第3班,第 3 个学生的分数 30 请输入第3班,第 4 个学生的分数 30 请输入第3班,第 5 个学生的分数 30 第3 班级,平均分为30 各个班的总成绩是300 各个班级的平均分是20 //3.优化 // 目前已经实现功能了,但是后期如果班级增加,几百个几千人修改起来就比较麻烦。接下来我们将代码优化 // 1) 定义2个变量,表示班级的个数,和学生的个数 package main import "fmt" func main() { // totalSum 所有班总成绩 var totalSum float64 = 0.0 // 优化: 定义班级变量 var classNum int = 3 var stuNum int = 5 // j表示第几个班级 for j := 1; j <= classNum; j++ { var sum float64 = 0.0 //定义总数 for i := 1; i <= stuNum; i++ { var fen float64 = 0.0 //每个学生的分数 fmt.Printf("请输入第%d班,第 %d 个学生的分数 \n", j, i) fmt.Scanln(&fen) // sum为总数,fen是每个学生的分数,加在一起就是这个班级的总分 sum += fen } // 计算所有班级的总成绩到totalSum totalSum += sum fmt.Printf("第%d 班级,平均分为%v \n", j, sum/float64(stuNum)) } fmt.Printf("各个班的总成绩是%v 各个班级的平均分是%v \n", totalSum, totalSum/float64(classNum*stuNum)) } //输出结果 ➜ 01 go run main.go 请输入第1班,第 1 个学生的分数 10 请输入第1班,第 2 个学生的分数 10 请输入第1班,第 3 个学生的分数 10 请输入第1班,第 4 个学生的分数 10 请输入第1班,第 5 个学生的分数 10 第1 班级,平均分为10 请输入第2班,第 1 个学生的分数 10 请输入第2班,第 2 个学生的分数 10 请输入第2班,第 3 个学生的分数 10 请输入第2班,第 4 个学生的分数 10 请输入第2班,第 5 个学生的分数 10 第2 班级,平均分为10 请输入第3班,第 1 个学生的分数 10 请输入第3班,第 2 个学生的分数 10 请输入第3班,第 3 个学生的分数 10 请输入第3班,第 4 个学生的分数 10 请输入第3班,第 5 个学生的分数 10 第3 班级,平均分为10 各个班的总成绩是150 各个班级的平均分是10 ➜ 01 ``` >应用案例二 基于上面的脚本,统计每个班级的合格人数 ``` // 首先,我们需要定义一个变量,用于保存及格人数 // 第二,在每个班级中的for循环添加if 判断,当大于60分标记为几个 package main import "fmt" func main() { // totalSum 所有班总成绩 var totalSum float64 = 0.0 // 定义保存及格人数变量 var countOK int = 0 // 优化: 定义班级变量 var classNum int = 2 var stuNum int = 5 // j表示第几个班级 for j := 1; j <= classNum; j++ { var sum float64 = 0.0 //定义总数 for i := 1; i <= stuNum; i++ { var fen float64 = 0.0 //每个学生的分数 fmt.Printf("请输入第%d班,第 %d 个学生的分数 \n", j, i) fmt.Scanln(&fen) // if 判断学生是否及格 if fen >= 60 { countOK++ } // sum为总数,fen是每个学生的分数,加在一起就是这个班级的总分 sum += fen } // 计算所有班级的总成绩到totalSum totalSum += sum fmt.Printf("第%d 班级,平均分为%v \n", j, sum/float64(stuNum)) } fmt.Printf("各个班的总成绩是%v 各个班级的平均分是%v \n", totalSum, totalSum/float64(classNum*stuNum)) //打印及格人数 fmt.Printf("及格人数为%v \n", countOK) } //输出结果 ➜ 01 go run main.go 请输入第1班,第 1 个学生的分数 100 请输入第1班,第 2 个学生的分数 100 请输入第1班,第 3 个学生的分数 100 请输入第1班,第 4 个学生的分数 100 请输入第1班,第 5 个学生的分数 100 第1 班级,平均分为100 请输入第2班,第 1 个学生的分数 80 请输入第2班,第 2 个学生的分数 70 请输入第2班,第 3 个学生的分数 1 请输入第2班,第 4 个学生的分数 2 请输入第2班,第 5 个学生的分数 1 第2 班级,平均分为30.8 各个班的总成绩是654 各个班级的平均分是65.4 及格人数为7 ➜ 01 ``` >应用案例三 打印金字塔 使用for循环打印金字塔,并且可以接受一个整数表示层数,打印出金字塔 (空心金字塔) ``` // 第一步,打印一个矩形 /* *** *** *** */ package main import "fmt" func main() { // i 表示层数 for i := 1; i <= 3; i++ { //j表示每层打印多少个 for j := 1; j <= 3; j++ { fmt.Print("*") } fmt.Println() } } //输出结果 ➜ 02 go run main.go *** *** *** // 第二步,打印半个金字塔 /* * ** *** */ package main import "fmt" func main() { // i 表示层数 for i := 1; i <= 3; i++ { //j表示每层打印多少个 for j := 1; j <= i; j++ { fmt.Print("*") } fmt.Println() } } //输出结果 ➜ 02 go run main.go * ** *** //第三步: 打印整个金字塔 //规律 * 1层1个* 规则: 2 * 层数 - 1 *** 2层3个* 规则: 2 * 层数 - 1 **** 3层5个* 规则: 2 * 层数 - 1 package main import "fmt" func main() { // i 表示层数 for i := 1; i <= 3; i++ { //j表示每层打印多少个 for j := 1; j <= 2 * i -1 ; j++ { fmt.Print("*") } fmt.Println() } } //输出结果 ➜ 02 go run main.go * *** ***** //目前输出还是有问题,我们需要解决空格的问题 //调整空格 //规律 * 1层1个* 规则: 2 * 层数 - 1 空格规律 总层数-当前层数 *** 2层3个* 规则: 2 * 层数 - 1 空格规律 总层数-当前层数 **** 3层5个* 规则: 2 * 层数 - 1 空格规律 总层数-当前层数 package main import "fmt" func main() { // i 表示层数 for i := 1; i <= 3; i++ { //在打印*号前打印空格 //k<= 总层数-当前层数 for k := 1; k <= 3-i; k++ { fmt.Printf(" ") } //j表示每层打印多少个 for j := 1; j <= 2*i-1; j++ { fmt.Print("*") } fmt.Println() } } //输出结果 ➜ 02 go run main.go * *** ***** //第四步: 优化代码扩展性 //将3(层数)设置为一个变量,每次修改只需要修改这一个变量 package main import "fmt" func main() { //设置层数变量 var totalLevel int = 5 // i 表示层数 for i := 1; i <= totalLevel; i++ { //在打印*号前打印空格 //k<= 总层数-当前层数 for k := 1; k <= totalLevel-i; k++ { fmt.Printf(" ") } //j表示每层打印多少个 for j := 1; j <= 2*i-1; j++ { fmt.Print("*") } fmt.Println() } } //输出结果 ➜ 02 go run main.go * *** ***** ******* ********* // 第五步: 打印空心金字塔 //分析: 在我们给每行大银行*号时,需要考虑是打印*号还是打印空格 //我们分析的结果是,每层的一个和最后一个是打印*,其他就应该是空的,即输出空格 package main import "fmt" func main() { //设置层数变量 var totalLevel int = 9 // i 表示层数 for i := 1; i <= totalLevel; i++ { //在打印*号前打印空格 //k<= 总层数-当前层数 for k := 1; k <= totalLevel-i; k++ { fmt.Printf(" ") } //j表示每层打印多少个 for j := 1; j <= 2*i-1; j++ { //开头的第一个和最后一个打印*号,其他的打印空格 //最后一行都打*号 if j == 1 || j == 2*i-1 || i == totalLevel { fmt.Print("*") } else { fmt.Print(" ") } } fmt.Println() } } //输出结果 ➜ 02 go run main.go * * * * * * * * * * * * * * * ***************** ``` >应用案例四 打印九九乘法表 ``` package main import "fmt" func main() { //打印九九乘法表 //表示行数 for i := 1; i <= 9; i++ { for j := 1; j <= i; j++ { fmt.Printf("%v * %v = %v \t", j, i, i*j) // \t制表符 } fmt.Println() } } //输出结果 ➜ 03 go run main.go 1 * 1 = 1 1 * 2 = 2 2 * 2 = 4 1 * 3 = 3 2 * 3 = 6 3 * 3 = 9 1 * 4 = 4 2 * 4 = 8 3 * 4 = 12 4 * 4 = 16 1 * 5 = 5 2 * 5 = 10 3 * 5 = 15 4 * 5 = 20 5 * 5 = 25 1 * 6 = 6 2 * 6 = 12 3 * 6 = 18 4 * 6 = 24 5 * 6 = 30 6 * 6 = 36 1 * 7 = 7 2 * 7 = 14 3 * 7 = 21 4 * 7 = 28 5 * 7 = 35 6 * 7 = 42 7 * 7 = 49 1 * 8 = 8 2 * 8 = 16 3 * 8 = 24 4 * 8 = 32 5 * 8 = 40 6 * 8 = 48 7 * 8 = 56 8 * 8 = 64 1 * 9 = 9 2 * 9 = 18 3 * 9 = 27 4 * 9 = 36 5 * 9 = 45 6 * 9 = 54 7 * 9 = 63 8 * 9 = 72 9 * 9 = 81 ``` ## 5.7 跳转控制语句-BREAK break语句用于终止某个**语句块的执行**,用于中断当前for循环或跳出switch语句 基本语法: ``` { ... break } ``` **break快速入门案例** > 随机生成1-100的一个数,直到生成99这个数,查看一共生成了多少次 ``` package main import ( "fmt" "math/rand" "time" ) func main() { var conut int = 0 for { //并且由于rand生成的数值是不变的,所以需要给rand设置一个总值,并且生成随机数 rand.Seed(time.Now().UnixNano()) n := rand.Intn(100) + 1 //默认rand生成的数为0-99,不包含100.所以我们需要加1 | 光是用这一行是不满足随机数的,所以需要上面的函数来同时使用 // fmt.Println(n) conut++ if n == 99 { break //跳出循环 } } fmt.Println("生成99一共循环了 ", conut) } //输出结果 ➜ 05 go run main.go 生成99一共循环了 202 ``` **label标签使用** 1.break默认会跳出最近的for循环 2.break后面可以指定标签,跳到标签对应的循环 案例 ``` package main import ( "fmt" ) func main() { //break 指定标签来使用 label: //定义标签,注意需要添加冒号 for i := 0; i < 4; i++ { for j := 0; j < 10; j++ { if j == 2 { break label //标签名称可以修改 } fmt.Println("j=", j) } // 当j==2,break 直接跳转到外层,所以只会打印0和1,外层for循环便退出 } } //输出结果 ➜ 06 go run main.go j= 0 j= 1 ``` >练习题: 实现登陆验证,有三次机会,如果用户为"张无忌",密码"888"提示成功,否则提示剩余多少次机会 ``` package main import ( "fmt" ) func main() { var user string var passwd string var login = 3 fmt.Println("欢迎登陆abcdocker运维系统") fmt.Println() for i := 1; i <= 3; i++ { fmt.Println("请输入登陆用户名") fmt.Scanln(&user) fmt.Println("请输入登陆密码") fmt.Scanln(&passwd) if user == "张无忌" && passwd == "888" { fmt.Println("恭喜您登陆成功!") break } else { login-- fmt.Printf("你还有%v次登陆机会 \n", login) } } if login == 0 { fmt.Println("机会用完,没有登陆成功") } } //输出结果 ➜ 07 go run main.go 欢迎登陆abcdocker运维系统 请输入登陆用户名 abc 请输入登陆密码 abc 你还有2次登陆机会 请输入登陆用户名 张无忌 请输入登陆密码 888 恭喜您登陆成功! ``` ## 5.8 跳转控制语句-CONTINUE continue语句用于结束`本次`循环,继续执行下一次循环。 continue语句出现在多层嵌套的循环语句体重,可以通过标签指明要跳转的那一层循环 基本语法 ``` { ... continue; ... } ``` ![image.png](http://img.ukx.cn/abcdocker/2021/04/07/621e4f0c6fa18/621e4f0c6fa18.png) >案例演示 ``` package main import ( "fmt" ) func main() { for i := 0; i < 3; i++ { fmt.Println() for j := 0; j < 3; j++ { if j == 0 { continue } fmt.Println("j=", j) } } } //输出结果 ➜ 08 go run main.go j= 1 j= 2 j= 1 j= 2 j= 1 //永远不会输出0 ``` >案例演示:continue 实现打印1-100之内的奇数[要求使用for循环+continue] ``` package main import ( "fmt" ) func main() { //continue 实现打印1-100之内的奇数[要求使用for循环+continue] var conut byte for i := 1; i <= 100; i++ { if i%2 == 0 { continue } fmt.Println("奇数是", i) conut++ } fmt.Printf("奇数一共有%v 个\n", conut) } ``` > 从键盘输入个数不确定的证书,并判断读入的正数和负数的个数,输入为0时程序结束 ``` package main import ( "fmt" ) func main() { //从键盘输入个数不确定的证书,并判断读入的正数和负数的个数,输入为0时程序结束 var positiveCount int var negativeCount int var num int for { fmt.Println("请输入一个整数") fmt.Scanln(&num) if num == 0 { break //退出循环 } if num > 0 { positiveCount++ continue //结束本次循环,进行下次循环 } negativeCount++ } fmt.Printf("正数个数是%v 负数的个数是%v \n", positiveCount, negativeCount) } //输出结果 ➜ 08 go run main.go 请输入一个整数 1 请输入一个整数 2 请输入一个整数 3 请输入一个整数 -1 请输入一个整数 -2 请输入一个整数 0 正数个数是3 负数的个数是2 ``` ## 5.9 跳转控制语句-GOTO 1)Go语音的goto语句可以无条件地转移到程序中指定的行。 2)goto语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能。 3)在go程序设计中**一般不主张使用goto语句**,以免造成程序流程的混乱 基本语法 ``` goto label ... label: statement ``` ![image.png](https://img.ukx.cn/abcdocker/2021/04/08/fe4ee5e245c69/fe4ee5e245c69.png) **案例演示** ``` package main import ( "fmt" ) func main() { var n int = 30 //演示goto的使用,通常配合if使用 fmt.Println("ok1") if n > 20 { goto label1 //当n 大于20,就goto跳转到下面label1 } fmt.Println("ok2") //此处为不执行 fmt.Println("ok3") //此处为不执行 fmt.Println("ok4") //此处为不执行 label1: //标签名,可变更 fmt.Println("ok5") fmt.Println("ok6") fmt.Println("ok7") fmt.Println("ok8") } //输出结果 ➜ 08 go run main.go ok1 ok5 ok6 ok7 ok8 ``` ## 5.10 跳转控制语句-RETURN return使用在方法或函数中,表示跳出所在的函数或方法 ``` package main import ( "fmt" ) func main() { for i := 1; i <= 10; i++ { if i == 3 { return } fmt.Println("abcdocker", i) } fmt.Println("hello word") } ``` >说明 1. 如果return是在普通的函数,则表示跳出该函数,即不在执行函数中的return后面代码,也可以理解成终止函数 1. 如果return是在main函数,表示终止main函数,也就是说终止程序。 **return语句基本语法** ``` func 函数名 (形参列表) (返回值类型列表) { 语句 return 返回值列表 } ``` 1) 如果返回多个值时,在接受时希望忽略某个返回值,则使用`_`符号表示占位忽略 2) 如果返回值只有一个,`返回值类型列表` 括号可以不写 **`_`案例演示: 忽略和的结果,包保留差的结果** ``` package main import "fmt" //定义函数 func getSumAndSub(n1 int, n2 int) (int, int) { //(int, int)表示返回2个结果 sum := n1 + n2 sub := n1 - n2 return sum, sub //返回sum和sub给调用者 } func main() { //调用变量 _, sub := getSumAndSub(1, 10) fmt.Println("sub =", sub) } //运行结果 ➜ exec4 go run main.go sub = -9 ``` >案例演示 请编写一个函数,可以计算两个数的和和差,并返回计算结果 ``` package main import "fmt" //定义函数 func getSumAndSub(n1 int, n2 int) (int, int) { //(int, int)表示返回2个结果 sum := n1 + n2 sub := n1 - n2 return sum, sub //返回sum和sub给调用者 } func main() { //调用变量 res1, res2 := getSumAndSub(10, 20) //res1 = sum = 10+20 && res2 = sub = 10-20 fmt.Println("rest1 =", res1) fmt.Println("rest2 =", res2) } //输出结果 ➜ exec4 go run main.go rest1 = 30 rest2 = -10 ``` 案例演示 ``` package main import ( "fmt" _ "go_code/package_demo/utils" ) func sumNum(n1 int, n2 int) int { sum := n1 + n2 fmt.Println("get sum =", sum) //当函数中有return语句时,就是将结果返回给调用者 return sum } func main() { sum := sumNum(10, 20) fmt.Println("main sum =", sum) //输出结果为30 // fmt.Println("utils.go = ", utils.Abcdocker) //utils为包名 Abcdocker为变量名,变量需要大写 // var num1 float64 = 1.1 // var num2 float64 = 1.2 // var operator byte = '+' // //引用utils包中的函数 // result := utils.Cal(num1, num2, operator) //utils代表包名,Cal代表函数名称,如果是小写cal无法包调用 // fmt.Println("result=", result) } //输出结果 ➜ main go run main.go get sum = 30 main sum = 30 ``` # 六、函数 为完成某一功能程序指令(语句)的集合,称为函数。 在Golang中,函数分为: `自定义函数`、`系统函数` ## 6.1 函数的基本概念及使用 基本语法 ``` func 函数名 (形参列表) (返回值列表){ 执行语句.. return 返回值列表 } ``` 1)形参列表: 表示函数的输入 2)函数中的语句: 表示为了实现某一功能代码块 3)函数可以有返回值,也可以没有 **案例演示** 使用函数解决计算问题 ``` package main import "fmt" func main() { //输入两个数,在输入一个运算符得到结果 var num1 float64 = 10.0 var num2 float64 = 2.1 var operator byte = '+' var res float64 switch operator { case '+': res = num1 + num2 case '-': res = num1 - num2 case '*': res = num1 * num2 case '/': res = num1 / num2 default: fmt.Println("请重新输入操作符号") } fmt.Println("res= ", res) } //输出结果 ➜ 08 go run main.go res= 12.1 ``` 但是如果我们代码中有多个需要计算的,switch就需要写入多行。这里就需要使用函数来解决 ``` package main import "fmt" func cal(num1 float64, num2 float64, operator byte) float64 { //num1和num2类型为float64,并且返回的类型也是float64 var res float64 switch operator { case '+': res = num1 + num2 case '-': res = num1 - num2 case '*': res = num1 * num2 case '/': res = num1 / num2 default: fmt.Println("请重新输入操作符号") } return res //将res结果返回 } func main() { //引用函数 //1.首先定义一个变量, 用于接受返回值 result := cal(10.0, 2.0, '+') //2.cal为函数名称,后面的10.0 == num1、2.0 ==num2,其中➕ == operator fmt.Println("result=", result) //3.除了直接输入数字的方式外,我们还可以把10.0和2.0以及'+'设置为变量 var n1 float64 = 1.1 var n2 float64 = 1.2 var operator byte = '+' result1 := cal(n1, n2, operator) fmt.Println("result1=", result1) } ``` ## 6.2 包的引用 1) 在实际上开发中,需要在不同的文件调用函数, 2) 项目开发过程需,需要很多人参与,相关函数写在一起容易造成提及庞大,理解困难。所以有了包的概念 ## 6.3 包的原理图 包的本质实际上就是创建不同的文件夹,来存放程序文件。 ``` oa db/db.go utils/utils.go main/main.go ``` ## 6.4 包的基本概念 在go语言中,go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录的 ## 6.5 包的三大作用 - 区分相同名字的函数、变量等标示符 - 当程序文件很多时,可以很好的管理项目 - 控制函数、变量等访问范围,即作用域 ## 6.6 包的相关说明 **打包基本语法** ``` package 包名 例如package main,这里就相当于我们打包了一个main包 ``` **引入包的基本语法** ``` import "包的路径" ``` ## 6.7 包使用的快速入门 首先我们在vscode中创建一个单独的项目 ``` 目录结构如下 src go_code package_demo main main.go utils utils.go ``` ![image.png](https://img.ukx.cn/abcdocker/2021/04/12/5ebe8c984e82a/5ebe8c984e82a.png) 我们将func Cal定义到`utils.go`,将`utils.go`放到一个包中,当其他文件需要使用utils.go方法时,可以import该包 **utili.go文件内容如下** ``` package utils //打包指令,utils代表包名 import "fmt" //将计算的功能,放到一个函数中,然后在使用调用 func Cal(num1 float64, num2 float64, operator byte) float64 { //最后一个float64将结果输出为float64格式,前面的是定义变量类型 //为了让其他的包使用cal,需要将这里的Cal函数名称需要大写 //在go语言中将函数大写,表示该函数可导出 var res float64 switch operator { case '+': res = num1 + num2 case '-': res = num1 - num2 case '*': res = num1 * num2 case '/': res = num1 / num2 default: fmt.Println("操作符号错误...") } return res //将res结果返回 } ``` **main.go文件内容如下** ``` package main import ( "fmt" "go_code/package_demo/utils" //src后面开始写,src前面的路径不需要写 ) func main() { var num1 float64 = 1.1 var num2 float64 = 1.2 var operator byte = '+' //引用utils包中的函数 result := utils.Cal(num1, num2, operator) //utils代表包名,Cal代表函数名称,如果是小写cal无法包调用 fmt.Println("result=", result) } ``` 输出结果如下: ``` ➜ package_demo ls go.mod main utils ➜ package_demo go run main/main.go result= 2.3 ``` ## 6.8 包使用的注意事项 * 在给一个包打包时,该包对应一个文件夹,比如这里的utils文件夹对应的包名就是utils。文件的包名`通常`和文件所在文件夹名一致 (一般为小写) ![image.png](https://img.ukx.cn/abcdocker/2021/04/12/48fb2781046f1/48fb2781046f1.png) * 当一个文件要使用其它函数或变量时,需要先引入对应的包 1)引入方式1: `import "包名"` 2)引入方式2: ``` import ( "包名" ) ``` * package指令在文件第一行,然后是import指令 * 在import包时,路径从`$GOPATH`的src下开始,不需要带src,编译器会自动从src下开始引入 * 为了其它包文件可以访问到本地的函数,则该函数名的首字母需要大写 ![image.png](https://image.abcdocker.com/abcdocker/2021/04/12/48116d63a8b15/48116d63a8b15.png) * 同样我们如果要定义一个变量名,也是相同的使用方式 ![image.png](https://image.abcdocker.com/abcdocker/2021/04/12/f505e12f16743/f505e12f16743.png) ![image.png](https://image.abcdocker.com/abcdocker/2021/04/12/ae294763f7020/ae294763f7020.png) * 在访问其它包函数或变量时,其语法是`包名.函数名` ![image.png](https://image.abcdocker.com/abcdocker/2021/04/12/d40ead315ac55/d40ead315ac55.png) * 如果包名较长,GO支持给包取别名,注意: 取别名后,**原来的包名就不能使用了** 演示 ![image.png](https://image.abcdocker.com/abcdocker/2021/04/12/3515c730308e3/3515c730308e3.png) * 在同一个包下,不能有相同的函数名和全局变量名,否则报重复定义 * 如果我们要编译一个可执行程序文件, 就需要将包声明为main,即package main。 演示一个案例,模拟多个目录,多个包如何编译打包 ``` 目录结构如下 package_demo main main.go utils utils.go ``` 编译时只需要编译main包即可 ``` ➜ main go build main.go ➜ main ./main utils.go = 100 result= 2.3 #编译时也可以指定打包名称和路径 ➜ main go build -o abcdocker main.go ➜ main ls abcdocker main.go ➜ main ./abcdocker utils.go = 100 result= 2.3 ``` 打包exe文件 ``` ➜ main CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go ➜ main ls main.exe main.go ``` ![image.png](https://image.abcdocker.com/abcdocker/2021/04/12/a3426e35a48ca/a3426e35a48ca.png) >下面是在linux打包不同平台的包 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go ## 6.9 函数的调用机制 ![image.png](https://image.abcdocker.com/abcdocker/2021/04/13/68fd39023e3f6/68fd39023e3f6.png) 在go中,最先使用的函数为main函数,当`n1 :=10`定义变量后,main中会执行下一步,通过调用`test`函数,将`n1`传给`test`函数。接下来就到test函数进行执行。 在test函数中`n1= n1+1`基本是相当于10+1。所以在test函数中会输出`11`数字 当test函数将结果输出后,返回到main函数中,继续执行下一条,test函数执行完毕后就会在内存中释放,也就是目前只保留了`n1 := 10`,那么下面打印出来的就是10 >编辑器会回收不使用的栈区,当test栈区执行完毕,编辑器会进行回收 ![image.png](https://image.abcdocker.com/abcdocker/2021/04/13/8474b5883cb51/8474b5883cb51.png) ## 6.10 函数的递归调用 一个函数在**函数体内**又**调用了自己**,我们称为递归调用 ``` //案例演示 package main import "fmt" //定义函数 func test(n int) { if n > 2 { n-- test(n) } fmt.Println("n=", n) } func main() { //调用变量 test(4) } //输出结果 ➜ exec4 go run main.go n= 2 n= 2 n= 3 ``` **分析图** ![image.png](https://image.abcdocker.com/abcdocker/2021/04/14/e234fb926e413/e234fb926e413.png) >案例演示2 ``` package main import "fmt" //定义函数 func test(n int) { if n > 2 { n-- test(n) } else { fmt.Println("n=", n) } } func main() { //调用变量 test(4) } //输出结果 ➜ exec4 go run main.go n= 2 ``` 跟上面的步骤参数解释一样,当`n >`2时,会再次创建test函数,直到`n = 2 >2`,当n不大于2之后,if判断下的内容不执行,反而执行else打印语句。其他函数中的else语句不会执行,因为他们已经在入口处执行了`n --`,并且在当时n是大于`2`的,所以执行`n -- `在调用新的test函数。最后上面的语句只会输出一个`2` **函数递归需要遵守的重要原则** * 执行一个函数时,就创建一个新的受保护的独立空间 (新函数栈) * 函数的局部变量是独立的,不会相互影响 [例如上面的test函数中的n变量,在多个函数中不受影响,互相独立] * 递归必须向退出递归的条件逼近,否则就是无限递归 (例如上面案例的n--) * 当一个函数执行完毕,或者遇到`return`,就会返回,遵守谁调用,就将结果返回给谁。同时当函数执行完毕或者返回时,该函数本身也会被销毁 >递归函数练习题1 请使用递归的方式,求出斐波那契数1,1,2,3,5,7,13 给你一个整数n,求出它的斐波那契数是多少? 思路分析 1.当n == 1 && n ==2 ,返回1 2.当n >=2,返回前面两个数的和 `f(n-1)+f(n-2)` ``` package main import "fmt" func fbn(n int) int { if n == 1 || n == 2 { return 1 } else { return fbn(n-1) + fbn(n-2) } } func main() { //定义接收函数 res := fbn(3) //引用函数 fmt.Println("rest=", res) //2 fmt.Println("rest=", fbn(4)) //3 fmt.Println("rest=", fbn(5)) //5 fmt.Println("rest=", fbn(6)) //8 } //输出结果 ➜ exec4 go run main.go rest= 2 rest= 3 rest= 5 rest= 8 ``` >递归函数练习题2 求函数的值 已知`f(1)=3;f(n)= 2*f(n-1)+1;` 请使用递归的编程思想,求出`f(n)`的值 ``` package main import "fmt" func f(n int) int { if n == 1{ return 3 }else{ return 2* f (n-1) + 1 } } func main() { //使用 fmt.Println("f(1)=", f(1)) fmt.Println("f(5)=", f(5)) } //输出结果 ➜ exec4 go run main.go f(1)= 3 f(5)= 63 ``` > 递归函数练习题3 有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个。以后每天猴子都吃其中的一半,然后再多吃一个。当到第十天时,想再吃时(还没吃),发现只有一个桃子,请问最初一共有多少个桃子 分析: 1.第十天只有一个桃子了 2.第九天桃子= (第十天桃子数量 + 1) 2 3.规律: 第n天的桃子数量` peach(n) =(peach(n+1)+1) x 2` ``` package main import "fmt" func peach(n int) int { if n > 10 || n < 1 { fmt.Println("输入的天数不对") return 0 //返回0 表示没有得到正确数量 } if n == 10 { return 1 } else { return (peach(n+1) + 1) * 2 } } func main() { //使用 fmt.Println("第一天桃子的数量是", peach(1)) fmt.Println("第十天桃子的数量是", peach(10)) fmt.Println("第⑨天桃子的数量是", peach(9)) fmt.Println() fmt.Println("测试输入非1-10的数量", peach(15)) } //输出结果 ➜ exec4 go run main.go 第一天桃子的数量是 1534 第十天桃子的数量是 1 第⑨天桃子的数量是 4 输入的天数不对 测试输入非1-10的数量 0 ``` ## 6.11 函数注意事项 1) 函数的形参列表可以是多个,返回值列表也可以是多个 2) 形参列表和返回值列表的数据类型可以是值和引用类型 3) 函数的命名遵循标示符命名规范,首字母不能是数字,**首字母大写该函数可以被本包或者其他包文件使用**,首字母小写,只能被本包文件使用,其他包文件不能使用 4) 函数中的变量是局部的,函数外不生效 5) 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响原来的值。例如main函数中的n1和`test`函数中的n1修改`test`函数中的n1不会影响`main`函数中的n1 6) 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址`&`,函数内指针的方式操作变量。从效果上看类似引用 ``` func test(n1 *int) { *n1 = *n1 + 10 fmt.Println("test() n1 ==", *n1) } func main() { num := 20 test(&num) fmt.Println("main() num ==", num) //当时用了指针的方式,会修改num :=20这个值 } //输出结果 ➜ exec4 go run main.go test() n1 == 30 main() num == 30 ➜ exec4 ``` 取值会直接修改num的变量,所以`*n1 = *n1+10` 可以理解为`num == 20 +10 `(`*n1`是取值,而不是复制的关系) 7) GO函数不支持传统的函数重载 8) 在GO中,**函数也是一种数据类型**,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用 ``` package main import "fmt" func getSum(n1 int, n2 int) int { return n1 + n2 } func main() { a := getSum //它们的值相同,因为a将代码变量指向了getSum函数 (代码空间) fmt.Printf("a的类型为%T,getSum类型是%T\n", a, getSum) res := a(10, 20) //等价 res := getSum(10,20) fmt.Println("res=", res) } //输出结果 PS C:\Users\Administrator\Desktop\GOstudy\day2> go run .\main.go a的类型为func(int, int) int,getSum类型是func(int, int) int res= 30 ``` 分析: 在函数中可以将函数给交一个变量,并且可以通过变量进行完成调用 9) 函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用 ``` package main import "fmt" func getsum(n1 int, n2 int) int { return n1 + n2 } //将函数当成形参传给其他函数 func myfun(funvar func(int, int) int, num1 int, num2 int) int { //myfun函数名 //funvar 变量名,将funvar变量变成函数类型 //func (int,int)int 形参类型,定义了2个int类型,并且返回了一个int类型 //num1 int,num2 int 传2个类型给getsum //int 返回类型 return funvar(num1, num2) //本函数也可以调用其他函数 //传入num1和num2 //上面说到可以把一个函数交给一个变量,同时也可以把一个变量交给一个形参 } func main() { res2 := myfun(getsum, 50, 60) //调用getsum函数,将myfun函数中的形参传给getsum函数进行调用 //res2 := 类型推导 //myfun调用函数 //getsum,50,60 通过myfun函数调用getsum,将50和60调用给num1和num2 fmt.Println("rest2=", res2) } //输出结果 ➜ exec4 go run main.go rest2= 110 ``` 10) 为了简化数据类型定义,go支持自定义数据类型 基本语法:` type 自定义数据类型名称 数据类型` //相当于一个别名 例如`type myint int` //这时myint就等价int类型 例如`type mysum func(int,int)int` 这时mysum就等价一个函数类型`func (int,int)int` **举例子说明自定义数据类型的使用** ``` package main import "fmt" func main() { type myInt int // 给int取了别名,在go语言中,myInt和int虽然都是int类型,但是go在语法中认为myInt和int是两个类型 var num1 myInt num1 = 10 fmt.Println("num1=", num1) //但是num1不等价于num2 var num2 int //num2 = num1 ❌调用 num2 = int(num1) // ✅正确,虽然myInt和int都是int类型,但是go语法不识别,如果想引用需要强制将myInt类型转换为int类型 //重点! go语法认为myInt和int是不同的类型,所以需要强制转换 fmt.Println("num2=", num2) } //输出结果 ➜ exec4 go run main.go num1= 10 num2= 10 ``` **举例子说明自定义数据类型在函数内的使用** 给函数取一个别名,将来我们函数作为形参就可以简化流程 ``` package main import "fmt" //定义计算函数模板 func getSum(n1 int, n2 int) int { return n1 + n2 } //添加函数类型 type myFunType func(int, int) int //添加新函数调用myFunType func getFunType(abc myFunType, num1 int, num2 int) int { //getFunType为新的函数名 //abc 为变量名 //myFunType 为调用函数的类型 // num1 返回值1,num2返回值2 return abc(num1, num2) } func main() { //调用myFunType res1 := getFunType(getSum, 10, 20) fmt.Println("res1=", res1) } //输出结果 ➜ exec4 go run main.go res1= 30 ``` 11) go语言中支持对返回值命名 ``` package main import "fmt" func getSumAndSub(n1 int, n2 int) (sum int, sub int) { sub = n1 - n2 sum = n1 + n2 //最先计算的是加法,是根据上面sum int写在前面进行定义,而并非此行 return //这里sum和sub无需在定义变量,直接引用即可 } func main() { a, b := getSumAndSub(10, 10) fmt.Printf("a=%v b=%v\n", a, b) //sum和sub是按照函数的计算方式进行调用,最先进行计算的是加法,是在函数中定义的,并非是sum = n1 +2 } //输出结果 ➜ exec4 go run main.go a=20 b=0 ``` 12) 使用`_`占位符,来忽略返回值 ![image.png](https://image.abcdocker.com/abcdocker/2021/04/18/dac685b6922d2/dac685b6922d2.png) 13) go支持可变参数 //支持0到多个参数 ``` func sum(agrs ...int)sum int{ } ``` //支持1到多个参数 ``` func sum(n1 int,agrs ..int)sum int{ } ``` 说明: args是slice切片,通过args[index]可以访问到各个值 //如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后 >案例演示,编写一个函数,可以求出 1到多个int的和 ``` package main import "fmt" //案例演示,编写一个函数,可以求出 1到多个int的和 func sum(n1 int, args ...int) int { //args的值可以变 sum := n1 for i := 0; i < len(args); i++ { sum += args[i] //args[0]表示取出args切片的第一个元素值,其他依次类推 } return sum } func main() { res := sum(10, 0, -1) fmt.Println("res=", res) } //输出结果 ➜ exec4 go run main.go res= 9 ``` ## 6.12 函数练习 > 练习题1,判断代码有无错误,输出什么? ``` package main import "fmt" func sum(n1, n2 float32) float32 { fmt.Printf("n1 type = %T\n", n1) return n1+n2 } func main() { fmt.Println("sum=", sum(1, 2)) } //答案✅,无错误,输出以下内容 ➜ exec4 go run main.go n1 type = float32 sum= 3 ``` > 练习题2,判断代码有无错误,输出什么? ``` package main import "fmt" type mySum func(int, int) int func sum(n1 int, n2 int) int { return n2 + n2 } func sum2(n1, n2, n3 int) int { return n1 + n2 } //使用type自定义数据类型来简化定义 func myFunc(funVar mySum,num1 int,num2 int)int{ return funVar(num1,num2) } func main() { a:=sum b:=sum2 fmt.Println(myFunc(a,1,2)) //✅ 正确 fmt.Println(myFunc(b,1,2)) //❌ 由于sum2的类型是3个int,类型不匹配。因为不能将sum2(n1, n2, n3 int) int函数类型,赋给func(int, int) int类型 } ``` > 练习题3,请编写一个函数swap(n1 \*int, n2 \*int)可以交换n1和n2的值 ``` package main import "fmt" func swap(n1 *int, n2 *int) { //*int代表指针变量 //定义t来接受临时变量 t := *n1 //t来临时接受n1指针变量 *n1 = *n2 //n1赋值为n2 *n2 = t //n2赋值为t,t指针变量已经为n1的数值 } func main() { a := 10 //定义变量a = 10 b := 20 //定义变量b = 20 fmt.Printf("默认结果: a=%v , b=%v \n", a, b) //默认结果 swap(&a, &b) //指针变量传入需要添加&符 fmt.Printf("替换后结果: a=%v , b=%v \n", a, b) //替换后输出结果 } //输出结果 ➜ exec4 go run main.go 默认结果: a=10 , b=20 替换后结果: a=20 , b=10 ``` ## 6.13 函数参数的传递方式 值类型参数默认就是值传递,引用类型参数默认就是引用传递 **两种传递方式** - 值传递 - 引用传递 其实,不管是值传递还是引用类型传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝。 **地址拷贝**效率高,因为数据量小,而值拷贝决定的拷贝的数据大小,数据越大,效率越低 **值传递相关的数据类型** 基本数据类型(int、float、bool、string)、数组、结构体 **引用传递相关的数据类型** 指针、slice切片、map、管道chan、interface **传递方式** ![abc.png](https://img.ukx.cn/abcdocker/2021/03/31/9c5949b7670b6/9c5949b7670b6.png) **值类型默认是值传递:** 变量直接存储值,内存通常在栈中分配 ![image.png](https://img.ukx.cn/abcdocker/2021/04/23/ad5e766dcb10b/ad5e766dcb10b.png) **引用类型默认是引用地址:** 变量存储的是一个地址,这个地址对应的空间才正常存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就称为一个垃圾,由GC来回收 ![image.png](https://img.ukx.cn/abcdocker/2021/04/23/1775c84edab65/1775c84edab65.png) >如果我们希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。 ``` func test(n1 *int) { *n1 = *n1 + 10 fmt.Println("test() n1 ==", *n1) } func main() { num := 20 test(&num) fmt.Println("main() num ==", num) //当时用了指针的方式,会修改num :=20这个值 } //输出结果 ➜ exec4 go run main.go test() n1 == 30 main() num == 30 ➜ exec4 取值会直接修改num的变量,所以*n1 = *n1+10 可以理解为num == 20 +10(*n1是取值,而不是复制的关系) ``` ## 6.14 INIT函数 **基本介绍** **每一个源文件**都可以包含一个int函数,该函数会在main函数执行前,被GO运行框架调用,也就是说int会在main函数前被掉用。int函数一般用于处理初始化前的工作 **案例说明** ``` package main import "fmt" func init() { fmt.Println("int()...") } func main() { fmt.Println("main()...") } //输出结果 ➜ exec4 go run main.go int()... main()... ``` **如果一个文件同时包含了全局变量定义,int函数和main函数,则执行的流程为: `全局变量定义`-->`init函数` --> `main函数`** ``` package main import "fmt" var day = test() //全局变量,调用test函数 func test() int { //定义一个函数 fmt.Println("test()...") //执行顺序1 return 90 } func init() { fmt.Println("int()...") //执行顺序2 } func main() { fmt.Println("main()...day=", day) //执行顺序3 } //输出结果 ➜ exec4 go run main.go test()... int()... main()...day= 90 ``` > **包引用init案例演示** init初始化工作案例演示 ![image.png](https://image.abcdocker.com/abcdocker/2021/04/19/8ef370c2e6488/8ef370c2e6488.png) main包进行引用 ``` package main import ( "fmt" "go_code/project01/01/exec4/utils" ) var day = test() //全局变量,调用test函数 func test() int { //定义一个函数 fmt.Println("test()...") //执行顺序2 return 90 } func init() { fmt.Println("int()...") //执行顺序3 } func main() { fmt.Println("main()...day=", day) //执行顺序4 fmt.Println("Age=", utils.Age, "Name=", utils.Name)//执行顺序1 } //输出结果 [优先输出utils包中的init函数] ➜ exec4 go run main.go utils包的init()... test()... int()... main()...day= 90 Age= 100 Name= abcdocker ``` **init优先级** * 首先先执行被引入文件的变量定义 [1] * 其次执行被引入文件的init函数 [2] * 后面开始执行main.go文件的变量定义 [3] * 其次是main.go文件的init函数 [4] * 最后执行main.go文件中的main主函数 [5] 例如main.go文件引入了utils.go,utils.go又引入了ok.go。那么执行顺序就是ok.go(变量定义-->init函数)然后执行utils.go,最后执行main.go (每个文件中的执行顺序依旧保持`变量定义`、`init函数`、`main.go`) ## 6.15 匿名函数 所谓匿名函数,就是没有名字的函数。一般情况下,我们函数都是有名称的。 GO支持匿名函数,如果我们某个函数只是希望执行一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用 * **匿名函数使用方式一** 在定义匿名函数时直接调用 (这种方式匿名函数只可以调用一次,因为没有将匿名函数给名字或者是交给一个变量。) ``` package main import ( "fmt" ) func main() { res1 := func(n1 int, n2 int) int { //定义匿名函数,res1定义变量来接收匿名函数 return n1 + n2 }(10, 20) //调用匿名函数,输入值即可 //数字10赋值给n1 //数字20赋值给n2 //int返回值给res1 fmt.Println("res1=", res1) } //输出结果 ➜ exec4 go run main.go res1= 30 ``` * **匿名函数使用方式二** 将匿名函数给一个变量(函数变量),再通过该变量来调用匿名函数 下面的匿名函数可以写在main函数中 ``` package main import ( "fmt" ) func main() { //将匿名函数func(n1 int, n2 int) int赋值给a变量 //此时a的数据类型为函数类型,此时我们可以通过A完成调用 a := func(n1 int, n2 int) int { return n1 - n2 } fmt.Println("res2=", a(11, 10)) } //输出结果 ➜ exec4 go run main.go res2= 1 ``` * **全局匿名函数** 如果将匿名函数赋给一个全局变量,那么这个匿名函数就称为一个全局匿名函数 ``` package main import ( "fmt" ) var ( Func = func(n1 int, n2 int) int { //Func变量名需要大写 return n1 * n2 } ) func main() { //全局匿名函数调用 fmt.Println("Func=", Func(9, 9)) } //输出结果 ➜ exec4 go run main.go Func= 81 ``` ## 6.16 闭包 闭包就是一个`函数`和`其相关的引用环境`(其他函数)组合的一个`整体` **什么是闭包函数** "闭"函数指的该函数是内嵌函数 "包"函数指的该函数包含对外层函数作用域名字的引用(不是对全局作用域) ``` package main import ( "fmt" ) //累加器 func AddUpper() func(int) int { var n int = 0 //此处的变量不会归0,而是属于累加的 return func(x int) int { n += x //n +=x 可以理解为n = n + x //并且每次执行完n变量不会归0,这里的结果会返回给上面的函数 return n } } func main() { //使用前面AddUpper f := AddUpper() fmt.Println(f(1)) //n + 1 = 0+1 fmt.Println(f(2)) //n + 2 = 1+2 fmt.Println(f(3)) //n + 3 = 3+3 } //输出结果 ➜ exec4 go run main.go 1 3 6 ``` **对上面的代码进行说明** (1) AddUpper是一个函数,返回的数据类型是`func(int) int ` (2) 闭包: 返回的是一个匿名函数,但是这个匿名函数引用到函数外的`n`,因此这个匿名函数就和`n`形成一个整体。构成了闭包 ![image.png](https://image.abcdocker.com/abcdocker/2021/04/19/d68a7b34b77ef/d68a7b34b77ef.png) (3) 可以理解为: 闭包是累,函数是操作,n是字段。函数和它使用到的n构成闭包 (4) 当我们反复的调用f函数时,因为n是只初始化一次,因此每调用一次就进行累计 (5) 函数和它引用到的变量共同构成闭包 >闭包实际上是返回的匿名函数,和用到的函数外的变量。它们共同构成一个闭包,而调用的时候它用到的变量不是每一次都会被初始化;每用到一次就会进行叠加一次 ![image.png](https://image.abcdocker.com/abcdocker/2021/04/19/4cf9d1bc3f558/4cf9d1bc3f558.png) ### 6.16.1 闭包实践 **请编写一个程序,具体要求如下** * [x] 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包 * [x] 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg)则返回 文件名.jog,如果已经有.jpg后缀,则返回原文件名 * [x] 要求使用闭包的方式完成 * [x] strings.HasSuffix,该函数可以判断某个字符串是否有指定的后缀 ``` package main import ( "fmt" "strings" ) func makeSuffix(suffix string) func(string) string { //定义匿名函数makeSuffix,接收变量suffix 类型为string //func (string)接收string类型,并返回一个string类型 return func(name string) string { //接收name变量 if !strings.HasSuffix(name, suffix) { //如果name 没有指定的后缀名,则加上,否则返回原来的名字 return name + suffix } return name } } func main() { //第一步返回一个闭包 f := makeSuffix(".jpg") //第二步调用 fmt.Println("文件名处理后=", f("abc")) fmt.Println("文件名处理后=", f("abc.jpg")) fmt.Println("文件名处理后=", f("abcdocker.mp4")) } //输出结果 ➜ exec4 go run main.go 文件名处理后= abc.jpg 文件名处理后= abc.jpg 文件名处理后= abcdocker.mp4.jpg ``` **总结和说明** 1) 返回的匿名函数和`makeSuffix (suffix string)`的suffix变量组合成一个闭包,因为返回的函数引用到suffix这个变量 2) 如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入`后缀名`。比如**.jpg** 而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用 3) 闭包的好处是传入一次后缀即可,因为函数调用一次后,函数是不会保留的,执行完就会销毁。闭包函数是会保留,每次会把suffix值保留下来 ## 6.17 DEFER DEFER主要用于在函数执行完毕后,及时释放资源,Go的设计者提供`defer`**延时机制** 例如: 需要创建资源(比如数据库连接、文件句柄、锁等) ``` //deffer的作用是等函数执行完毕后,在按照先入后出的方式执行deffer语句 package main import ( "fmt" ) func sum(n1 int, n2 int) int { //当函数执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈) //当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行 defer fmt.Println("ok1 n1=", n1) //第一次循环此处不执行,压入到defer 函数内第三个打印 defer fmt.Println("ok2 n2=", n2) //第一次循环此处不执行,压入到defer 函数内第二个打印 res := n1 + n2 fmt.Println("ok3 res=", res) //函数内第一个打印 return res //返回结果 //当return res执行完毕后,释放sum函数,接下来才开始执行defer栈中的语句,优先执行后面加入的语句,最先加入的到最后一个执行 } func main() { res := sum(10, 20) fmt.Println("res= ", res) //函数第四个打印 } //输出结果 ➜ exec4 go run main.go ok3 res= 30 ok2 n2= 20 ok1 n1= 10 res= 30 ``` 在`defer`将语句放入到栈时,也会将相关的值拷贝,并且同时入栈。 ``` package main import ( "fmt" ) func sum(n1 int, n2 int) int { defer fmt.Println("ok1 n1=", n1) //函数内第三个打印 defer fmt.Println("ok2 n2=", n2) //函数内第二个打印 //增加n1++和n2++不会修改defer语句中的n1和n2 //在defer将语句放入到栈时,也会将相关的值拷贝,并且同时入栈。 n1++ n2++ res := n1 + n2 fmt.Println("ok3 res=", res) //函数内第一个打印 return res //返回结果 } func main() { res := sum(10, 20) fmt.Println("res= ", res) //函数第四个打印 } //输出结果 ➜ exec4 go run main.go ok3 res= 32 ok2 n2= 20 ok1 n1= 10 res= 32 ``` defer最主要的价值是在当函数执行完毕后,可以及时的释放函数创建的资源 `实际使用defer案例` ![image.png](https://img.ukx.cn/abcdocker/2021/04/21/86a00d0ac5007/86a00d0ac5007.png) ## 6.18 变量作用域 * 函数内部定义/声明的变量叫局部变量,作用域仅限于函数内部 ``` func test(){ //abc的作用域只在test函数内部 abc := 10 } ``` * 函数外部定义/声明的变量叫做全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效 ``` package main import ( "fmt" ) //全局变量 var abc int = 60 var Name string = "abcdocker" //全局变量不支持类型推导 func test() { //abc的作用域只在test函数内部 abc := 10 Name := "tom~" fmt.Println("abc=", abc) fmt.Println("Name=", Name) } func main() { fmt.Println("abc=", abc) fmt.Println("Name=", Name) } //输出结果 ➜ 01 go run main.go abc= 60 Name= abcdocker ``` * 如果变量是在一个代码块,比如`if`、`for`中,那么这个变量的作用于就在该代码块中 ``` for i :=0; i<=10;i++{ fmt.Println("i=",i) } //如果希望i在函数外被使用,只需要定义一下 var i int for i :=0; i<=10;i++{ fmt.Println("i=",i) } fmt.Println("i=",i) ``` ## 6.19 函数综合练习 >使用函数打印金字塔 ``` package main import "fmt" //将打印金字塔的代码封装到函数中 func printPyramid(totalLevel int) { //设置层数变量 // i 表示层数 for i := 1; i <= totalLevel; i++ { //在打印*号前打印空格 //k<= 总层数-当前层数 for k := 1; k <= totalLevel-i; k++ { fmt.Printf(" ") } //j表示每层打印多少个 for j := 1; j <= 2*i-1; j++ { //开头的第一个和最后一个打印*号,其他的打印空格 //最后一行都打*号 // if j == 1 || j == 2*i-1 || i == totalLevel { fmt.Print("*") //注释其它代表打印实心金字塔 // } else { // fmt.Print(" ") // } } fmt.Println() } } func main() { var n int fmt.Println("请输入打印金字塔的层数") fmt.Scanln(&n) //调用printPyramid函数,就可以打印金字塔 printPyramid(n) } //输出结果 ➜ 01 go run main.go 请输入打印金字塔的层数 10 * *** ***** ******* ********* *********** ************* *************** ***************** ******************* ``` >使用函数打印九九乘法表 ``` package main import "fmt" func jj(num int) { //打印九九乘法表 //表示行数 for i := 1; i <= num; i++ { for j := 1; j <= i; j++ { fmt.Printf("%v * %v = %v \t", j, i, i*j) // \t制表符 } fmt.Println() } } func main() { var num int fmt.Println("请输入需要打印的乘法表") fmt.Scanln(&num) jj(num) } //输出结果 ➜ 01 go run main.go 请输入需要打印的乘法表 7 1 * 1 = 1 1 * 2 = 2 2 * 2 = 4 1 * 3 = 3 2 * 3 = 6 3 * 3 = 9 1 * 4 = 4 2 * 4 = 8 3 * 4 = 12 4 * 4 = 16 1 * 5 = 5 2 * 5 = 10 3 * 5 = 15 4 * 5 = 20 5 * 5 = 25 1 * 6 = 6 2 * 6 = 12 3 * 6 = 18 4 * 6 = 24 5 * 6 = 30 6 * 6 = 36 1 * 7 = 7 2 * 7 = 14 3 * 7 = 21 4 * 7 = 28 5 * 7 = 35 6 * 7 = 42 7 * 7 = 49 ``` >使用函数对给定的一个二维数组(3x3)转置 ``` ``` ## 6.20 字符串常用系统函数 ### 6.20.1 len 统计字符串、数组长度,按字节返回`len(str)`。len属于内建函数,不需要额外引用。直接使用即可 ``` func len(v Type) int //内建函数len返回 v 的长度,这取决于具体类型: 数组:v中元素的数量 数组指针:*v中元素的数量(v为nil时panic) 切片、映射:v中元素的数量;若v为nil,len(v)即为零 字符串:v中字节的数量 通道:通道缓存中队列(未读取)元素的数量;若v为 nil,len(v)即为零 ``` 案例演示 ``` func main() { //golang中的编码同意为utf-8 //ascii的字符 (字母和数字)占一个字节,中文汉字占3个字节 (注意是字节不是字符) str := "你好,abcdocker" //16个字节,中文6个,特殊符号1个,字母9个 fmt.Println("str len=", len(str)) //使用方法[len(需要引用的变量)] } //输出结果 ➜ 01 go run main.go str len= 16 ``` ### 6.20.2 rune 字符串遍历,处理中文问题 `r:= [rune(str)]` ``` //默认情况下直接打印中文会出现乱码的情况 func main() { str := "你好,abcdocker" //循环输出每一行内容 for i:= 0;i < len(str);i++{ fmt.Printf("字符=%c \n",str[i]) } } //输出结果 ➜ 01 go run main.go 字符=ä 字符=½ 字符= 字符=å 字符=¥ 字符=½ 字符=, 字符=a 字符=b 字符=c 字符=d 字符=o 字符=c 字符=k 字符=e 字符=r ``` 解决字符串遍历,中文乱码问题 ``` func main() { str := "你好,abcdocker" //将str转换成rune切片 r := []rune(str) //循环输出每一行内容 for i := 0; i < len(r); i++ { fmt.Printf("字符=%c \n", r[i]) } //输出结果 ➜ 01 go run main.go 字符=你 字符=好 字符=, 字符=a 字符=b 字符=c 字符=d 字符=o 字符=c 字符=k 字符=e ``` ### 6.20.3 []byte 将字符串转换为byte切片 字符串转 []byte: `var bytes = []byte("hello go")` byte输出内容为ascii码 ``` func main() { var bytes = []byte("hello go") fmt.Printf("bytes=%v \n", bytes) } //输出结果 ➜ 01 go run main.go bytes=[104 101 108 108 111 32 103 111] ``` 103 111对应的就是go ![image.png](https://img.ukx.cn/abcdocker/2021/04/25/2081122fcaa0c/2081122fcaa0c.png) **[]byte 转字符串 `str := string([]byte{104,101,108,108,111,32,103,111})`** 有些场景需要将byte类型的切片转换为字符串,按照字符串输出 ``` func main() { str := string([]byte{104, 101, 108, 108, 111, 32, 103, 111}) fmt.Printf("str=%v \n", str) } //输出内容如下 ➜ 01 go run main.go str=hello go ``` ### 6.20.4 strconv.Atoi ``` func Atoi(s string) (i int, err error) ``` 字符串转整数 `n,err :=strconv.Atoi("12")` 如果转不成功会产生一个err,可以对err进行判断 ``` package main import ( "fmt" "strconv" //需要引入包包 ) func main() { //字符串转整数 n, err := strconv.Atoi("123") if err != nil { //nil代表错误,相当于shell $?=0,此处代表如果err不等于nil代表错误 fmt.Println("转换错误", err) } else { fmt.Println("转换结果为", n) //转换成功后,n等于整数 } } //输出结果 ➜ 01 go run main.go 转换结果为 123 //当我们输入一个错误的数值,无法转换为整数时,会提示下面的报错 ➜ 01 go run main.go 转换错误 strconv.Atoi: parsing "abc": invalid syntax ``` ### 6.20.5 strconv.Itoa 整数不存在无法转成字符串的情况,所以没有error的选项 整数转字符串 `str = strcona.Itoa(123456)` ``` package main import ( "fmt" "strconv" ) func main() { str := strconv.Itoa(12312) fmt.Printf("str=数值为: %v str类型为:%T \n", str, str) } //输出结果 ➜ 01 go run main.go str=数值为: 12312 str类型为:string ``` ### 6.20.6 strconv.FomatInt ``` func FormantInt (i int64, bease int ) string //返回i的base进制的字符串传播。base必须在2到36之间,结果中会使用小写字母'a'到'z' 表示大于10的数字 ``` 10进制转2,8,16进制: `str := strconv.FormatInt(132,3)`,返回对应的字符串 ``` package main import ( "fmt" "strconv" ) func main() { str := strconv.FormatInt(123, 2) //将123转换为2进制 fmt.Printf("123对应的二进制是=%v \n", str) str = strconv.FormatInt(123, 8) //将123转换为8进制 fmt.Printf("123对应的八进制是=%v \n", str) str = strconv.FormatInt(123, 16) //将123转换为16进制 fmt.Printf("123对应的十六进制是=%v \n", str) } //输出结果 ➜ 01 go run main.go 123对应的二进制是=1111011 123对应的八进制是=173 123对应的十六进制是=7b ``` ### 6.20.7 strubgs.Contains 查找子串中,是否存在指定的字符: `strubgs.Contains("seafood","foo")` 如果有返回真,否则返回假 ``` func Contains(s, substr string) bool 判断字符串s是否包含子串substr ``` 案例演示 ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { //true a := strings.Contains("seafood", "foo") //seafood中存在foo,所以返回true fmt.Printf("a=%v \n", a) //true //false b := strings.Contains("seafood", "abc") //seafood中不存在abc,所以返回false fmt.Printf("b=%v \n", b) //false } //输出结果 ➜ 01 go run main.go a=true b=false ``` ### 6.20.8 strings.Count 统计一个字符串有几个指定的子串: `strings.Count("ceheese","e")` ``` func Count(s, sep string) int 返回字符串s中有几个不重复的sep子串 ``` 案例演示 ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { num := strings.Count("abcdocker_aaa", "a") //查找abcdocker_aaa中有几个a fmt.Printf("num=%v \n", num) } //输出结果 ➜ 01 go run main.go num=4 //当然如果字符中查找不到,就会返回0 ``` ### 6.20.9 string.gs.EqualFold 不区分大小写的字符串比较 (==是区分大小写) ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { //不区分大小写 a := strings.EqualFold("abc", "Abc") fmt.Printf("结果: %v \n", a) //区分大小写 fmt.Println("结果:", "abc" == "Abc") } //输出结果 ➜ 01 go run main.go 结果: true 结果: false ``` ### 6.20.10 strings.Index 返回子串在字符串**`第一次`**出现的index值,如果没有就返回`-1`(负一)。index值从0 开始计算 ![image.png](http://img.ukx.cn/abcdocker/2021/04/26/da2b9406c89af/da2b9406c89af.png) ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { //正确 index := strings.Index("abcdocker", "er") fmt.Printf("index =%v \n", index) //错误 index = strings.Index("abcdocker", "hello") fmt.Printf("index-error =%v \n", index) //当结果查询错误,找不到对应的会输出-1 } //输出结果 ➜ 01 go run main.go index =7 index-error =-1 ``` ### 6.20.11 strings.LastIndex 返回子串在字符串最后一次出现的Index,如果没有对应的数值,也返回`-1` ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { //正确 index := strings.LastIndex("go golang", "go") //go golang //012345678 //最后一次出现在3,所以index是3 fmt.Printf("index =%v \n", index) //错误 index = strings.LastIndex("go golang", "hello") fmt.Printf("index-error =%v \n", index) //当结果查询错误,找不到对应的会输出-1 } //输出结果 ➜ 01 go run main.go index =3 index-error =-1 ``` ### 6.20.12 strings.Replace 将制定的子串替换成另外一个子串 ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { str := strings.Replace("go go hello", "go", "北京", 1) //第一个"go go hello"代表内容 (这里也可以是一个变量) //go代表要替换的字符 //"北京"代表需要替换为的内容 //1,代表替换几个 -1代表所有 fmt.Printf("str =%v \n", str) } //输出结果 ➜ 01 go run main.go str =北京 go hello //如果使用变量进行替换并不会影响原来的值,原来的变量不会发生变化 ``` ### 6.20.13 strings.Split 按照指定的某个字符,为分隔标示,将一个字符串拆分成**字符串数组** ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { strArr := strings.Split("hello,work,go", ",") //这里的逗号代表以逗号进行拆分,这里的hello,work,go也可以是一个变量。 //这里本身不会改变字符串,只是将其拆分,形成一个新的数组。 之前的不变,这里是属于一个值拷贝,并不会影响之前的变量以及数值 //此时strArr是一个数组,我们可以通过下面的方式获取到 for i := 0; i < len(strArr); i++ { fmt.Printf("strArr_index=%v strArr=%v\n", i, strArr[i]) } //输出正常结果 fmt.Printf("strArr=%v\n", strArr) } //输出结果 ➜ 01 go run main.go strArr_index=0 strArr=hello strArr_index=1 strArr=work strArr_index=2 strArr=go strArr=[hello work go] ``` ### 6.20.14 strings.ToLower 将字符串的字母进行大小写的转换 ToLower 小写 ToUpper 大写 ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { str := "golang Hello" str1 := strings.ToLower(str) str2 := strings.ToUpper(str) fmt.Printf("str全小写=%v\n", str1) fmt.Printf("str全大写=%v\n", str2) } ``` ### 6.20.15 strings.Space 将字符串左右两边的空格去掉 ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { str := strings.TrimSpace(" abcdocker ") //前面有空格,后面也有空格 //处理 fmt.Printf("str=%q\n", str) } //输出结果 ➜ 01 go run main.go str="abcdocker" //-q参数会将字符串加双引号引起来 ``` ### 6.20.16 strings.Trim 将字符串左右两边指定的字符去掉 ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { str := strings.Trim("! abcd!ocker !", " !") //将字符串左右两边的!号及空格去掉 //处理 fmt.Printf("str=%q\n", str) } //输出结果 ➜ 01 go run main.go str="abcd!ocker" //但是中间的!是不可以去除的 ``` ### 6.20.17 strings.TrimLeft 将字符串左边指定的字符去掉 ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { str := strings.TrimLeft("! abcd!ocker !", " !") //将字符串左边的空格和叹号删除 //处理 fmt.Printf("str_Left=%q\n", str) str = strings.TrimRight("! abcd!ocker !", " !") //将字符串右边的空格和叹号删除 //处理 fmt.Printf("str_Right=%q\n", str) } //输出结果 ➜ 01 go run main.go str_Left="abcd!ocker !" str_Right="! abcd!ocker" ``` ### 6.20.18 strings.TrimRight 将字符右边指定的字符去掉 案例:↑ ### 6.20.19 strings.HasPrefix 判断字符串是否以指定的字符串开头 ``` package main import ( "fmt" "strings" //需要引入包 ) func main() { str := strings.HasPrefix("ftp://i4t.com","ftp") //如果是以ftp开头返回true,否则false fmt.Printf("str HasPrefix=%v \n",str) str = strings.HasSuffix("ftp://i4t.com","com") //如果是以com结尾返回true,否则false fmt.Printf("str HasSuffix=%v\n",str) } //输出结果 ➜ 01 go run main.go str HasPrefix=true str HasSuffix=true ``` ### 6.20.20 strings.HasSuffix 判断字符串是否以指定的字符串结束 案例:↑ ## 6.21 时间和日期相关函数 时间与日期的函数尝尝用于订单下单时间,代码执行花费时间等 >时间和日期相关函数需要导入`time`包 ### 6.21.1 time.Time time.Time类型,用于时间表示 **获取当前时间** ``` package main import ( "fmt" "time" ) func main() { now := time.Now() //获取当前时间 fmt.Printf("now当前时间为%v \nnow类型为:%T\n", now, now) } //输出结果 ➜ 01 go run main.go now当前时间为2021-04-27 14:34:49.401729 +0800 CST m=+0.000097106 now类型为:time.Time ``` 上面获取的时间不适合我们阅读,可以通过下面的方法进行处理 ``` package main import ( "fmt" "time" ) func main() { now := time.Now() //获取当前时间 fmt.Printf("now当前时间为%v \nnow类型为:%T\n", now, now) //通过now获取年月日、时分秒 fmt.Printf("年=%v\n", now.Year()) //now是上面的变量 fmt.Printf("月=%v\n", now.Month()) fmt.Printf("月=%v\n", int(now.Month())) //强制转换为数字 fmt.Printf("日=%v\n", now.Day()) fmt.Printf("时=%v\n", now.Hour()) fmt.Printf("分=%v\n", now.Minute()) fmt.Printf("秒=%v\n", now.Second()) } //输出结果 ➜ 01 go run main.go now当前时间为2021-04-27 15:01:22.816391 +0800 CST m=+0.000088057 now类型为:time.Time 年=2021 月=April 月=4 日=27 时=15 分=1 秒=22 ``` 上面的方式虽然打印出时分秒、年月日,但是看的不太友好,我们需要进行使用格式化输出,输出成对应的格式 `格式化输出第一种方式` ``` package main import ( "fmt" "time" ) func main() { now := time.Now() //获取当前时间 fmt.Printf("%d年%d月%d日:%d:%d:%d\n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) } //输出结果 ➜ 01 go run main.go 2021年4月27日:15:10:28 //我们也可以将内容返回给一个变量,方便以后存入数据库中 func main() { now := time.Now() //获取当前时间 dataStr := fmt.Sprintf("%d-%d-%d:%d:%d:%d\n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) fmt.Printf("data=%v", dataStr) } //输出结果 ➜ 01 go run main.go data=2021-4-27:15:22:45 ``` `格式化输出第二种方式` 使用time.Format()方法完成 >注意: 下面的数字不允许改变,格式可以修改 ``` package main import ( "fmt" "time" ) func main() { now := time.Now() //获取当前时间 fmt.Println(now.Format("2006/01/02 15:04:05")) //输入完整时间戳,此处的时间不可以改变,格式可以改动 fmt.Println(now.Format("2006-01-02 15:04:05")) //输出年月日 fmt.Println(now.Format("2006-01-02")) fmt.Println(now.Format("2006年01月02日")) //输出时分秒 fmt.Println(now.Format("15:04:05")) fmt.Println(now.Format("15时04分05秒")) } //输出结果 ➜ 01 go run main.go 2021/04/27 16:57:11 2021-04-27 16:57:11 2021-04-27 2021年04月27日 16:57:11 16时57分11秒 ``` ### 6.21.2 时间常量 ``` const ( Nanosecond Duration = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond Minute = 60 * Second Hour = 60 * Minute ) ``` 在程序中可用于获取指定单位的时间,比如100毫秒 >结合Sleep来使用时间常量 >例如:每隔1秒打印一个数字,打印到5就退出 ``` package main import ( "fmt" "time" ) func main() { i := 0 for { i++ fmt.Println(i) time.Sleep(time.Second) if i == 5 { break } } } //输出结果 ➜ 01 go run main.go 1 2 3 4 5 ``` >每隔0.1秒打印一个数字,打印到5退出 ``` package main import ( "fmt" "time" ) func main() { i := 0 for { i++ fmt.Println(i) time.Sleep(time.Millisecond * 100) if i == 5 { break } } } //输出结果 ➜ 01 go run main.go 1 2 3 4 5 ``` **获取当前unix时间戳和unixnano时间戳** >可以获取随机的数字 * unix 时间戳 (获取秒数) * unixnano时间戳 (获取纳秒数) ``` func (t time) Unix() int64 Unix将t表示为Unix时间,即从时间点January 1,1970 UTC到时间t所经过的时间(单位秒) func (t time) UnixNano() int64 UnixNano将t表示为Unix时间,即从时间点January 1,1970 utc到时间点t所经过的时间(单位纳秒)。如果纳秒为单位的unix时间超过了int64能表示的范围,结果是未定义的。 注意这就意味着Time零值调用UnixNano方法的话,结果是未定义的 ``` **unix和unixnano使用** ``` package main import ( "fmt" "time" ) func main() { now := time.Now() fmt.Printf("unix时间戳=%v unixnano时间戳=%v \n",now.Unix(),now.UnixNano()) } //输出结果 ➜ 01 go run main.go unix时间戳=1619581076 unixnano时间戳=1619581076595019000 ➜ 01 go run main.go unix时间戳=1619581077 unixnano时间戳=1619581077404640000 ➜ 01 go run main.go unix时间戳=1619581078 unixnano时间戳=1619581078288449000 ➜ 01 go run main.go unix时间戳=1619581081 unixnano时间戳=1619581081008677000 ``` ### 6.21.3 统计函数执行时间 ``` package main import ( "fmt" "strconv" //引用转换函数 "time" //引入time包 ) func test() { str := "" for i := 0; i < 100000; i++ { str += "hello" + strconv.Itoa(i) } } func main() { //在test函数执行前,先获取当前的unix时间戳 start := time.Now().Unix() test() //执行test函数 //获取执行完毕后unix时间戳 end := time.Now().Unix() fmt.Printf("执行test函数消费时间为%v秒\n", end-start) //结束时间减去开始时间 } //输出结果 ➜ 01 go run main.go 执行test函数消费时间为6秒 ``` ## 6.22 内置函数 golang设计者为了编程方便,提供了一些函数,这些函数可以直接使用 ### 6.22.1 len 用来求长度,比如string、array、alice、map、channel ``` 统计字符串、数组长度,按字节返回len(str)。len属于内建函数,不需要额外引用。直接使用即可 func len(v Type) int //内建函数len返回 v 的长度,这取决于具体类型: 数组:v中元素的数量 数组指针:*v中元素的数量(v为nil时panic) 切片、映射:v中元素的数量;若v为nil,len(v)即为零 字符串:v中字节的数量 通道:通道缓存中队列(未读取)元素的数量;若v为 nil,len(v)即为零 案例演示 func main() { //golang中的编码同意为utf-8 //ascii的字符 (字母和数字)占一个字节,中文汉字占3个字节 (注意是字节不是字符) str := "你好,abcdocker" //16个字节,中文6个,特殊符号1个,字母9个 fmt.Println("str len=", len(str)) //使用方法[len(需要引用的变量)] } //输出结果 ➜ 01 go run main.go str len= 16 ``` ### 6.22.2 new 用来分配内存,主要用来**分配值类型**,比如int、float32、structu返回的是指针。 ``` package main import ( "fmt" _ "strconv" //引用转换函数 _ "time" //引入time包 ) func main() { num2 := new(int) //此时num2为 *int类型 *num2 = 100 fmt.Printf("num2的类型%T \nnum2的值%v\nnum2地址%v\nnum2指针指向的值%v\n", num2, num2, &num2, *num2) } //输出结果 ➜ 01 go run main.go num2的类型*int num2的值0xc00001a090 //这个地址是系统分配,每次不唯一。根据当前情况内存分配 num2地址0xc00000e028 //这个地址是系统分配,每次不唯一。根据当前情况内存分配 num2指针指向的值100 //默认是0 ``` **内存分析图** `new(int)` ![image.png](https://img.ukx.cn/abcdocker/2021/04/28/6e535a5cb42f8/6e535a5cb42f8.png) ### 6.22.3 make 用来分配内存,主要用来分配**引用类型**,比如chan、map、slice ## 6.23 错误处理 1) go语言追求简洁优雅,所以go语言不支持传统的try...catch...finally这样处理 2) go中引入的处理方式为: `defer`、`panic`、`recover` 3) go中可以抛出一个panic的异常,然后在defer中通过`reconver`捕获这个异常,进行正常处理 ### 6.23.1 使用defer+reconver来处理error错误 案例: 下面的代码出现异常,10无法除0所以提示下面的报错。 并且main函数打印操作没有输出 ![image.png](https://img.ukx.cn/abcdocker/2021/04/28/f6df07ea1200c/f6df07ea1200c.png) ``` package main import ( "fmt" ) func test() { //使用defer配合匿名函数还有reconver来捕获和处理异常 defer func() { //定义匿名函数 err := recover() //reconver内置函数,可以捕获到异常 if err != nil { //nil不等于0,说明有异常 fmt.Println("err=", err) //输出异常 } }() //使用匿名函数 n1 := 10 n2 := 0 res := n1 / n2 fmt.Printf("res=%v", res) } func main() { //调用test函数 test() fmt.Println("main函数下面的代码") } //输出结果 ➜ 01 go run main.go err= runtime error: integer divide by zero main函数下面的代码 ``` 也可以使用下面的写法 ``` func test() { //使用defer配合匿名函数还有reconver来捕获和处理异常 defer func() { //定义匿名函数 if err := recover(); err != nil { //nil不等于0,说明有异常 fmt.Println("err=", err) //输出异常 } }() ``` ### 6.23.2 自定义错误 go程序中,也支持自定义错误,使用`errors.New`和`panic`内置函数 1) errors.New("错误说明"),会返回一个**error**类型的值,表示一个错误 2) panic内置函数接收一个`interface {}`类型的值作为参数,可以接受error类型的变量,**输出错误信息,并退出程序** >errors包实现了创建错误值的函数。 ``` package main import ( "errors" "fmt" ) func intconfig(name string) (err error) { //返回值为err error if name == "config.ini" { //读取 return nil } else { //返回一个自定义错误 return errors.New("读取文件错误") } } //定义调用intconfig函数 func readfile() { err := intconfig("config.ini") if err != nil { panic(err) //如果读取文件发送错误,就输出这个错误,并终止程序 } fmt.Println("readfile代码") } func main() { //调用readfile函数 readfile() fmt.Println("main代码") } //输出内容 ➜ 01 go run main.go readfile代码 main代码 ``` 当我们将文件名`config.ini`进行修改,if判断不是config.ini会输出下面的错误。并且会终端下面的执行 ![image.png](https://img.ukx.cn/abcdocker/2021/04/28/cfd5199e4cde9/cfd5199e4cde9.png) >这里返回的类型都是error类型的! # 七、数组与切片 数组可以存放多个**同一类型数据**。数组也是一个数据类型,在go中,数组是`值类型` ## 7.1 数组入门 案例: 一个养鸡场有8只鸡,请问这8只鸡的总体重是多少? 平均体重为多少? ``` package main import ( _ "errors" "fmt" ) func hensFunc() { //定义一个数组 var hens [8]float64 //给数组的每个元素赋值,元素的下标是从0开始的 hens[0] = 3.0 hens[1] = 5.0 hens[2] = 1.0 hens[3] = 3.4 hens[4] = 2.0 hens[5] = 50.0 hens[6] = 150.0 hens[6] = 200.0 //新增一只鸡只需要给hens数组添加一个元素, 并且赋值即可 //遍历数组求出总体重 totalWeight := 0.0 //设置总体重变量 //使用for循环遍历hens数组 for i := 0; i < len(hens); i++ { //这里的len是求出数组的个数,也可以直接写死 totalWeight += hens[i] //每个数组的数相加 } //此时的totalWeight已经是7只鸡的总数,接下来我们需要求出平均体重 avgWeight := fmt.Sprintf("%.2f", totalWeight/float64(len(hens))) //后面的len计算完是int类型,所以我们需要强制转换 //%.2f保留小数点2位,如果不使用这个变量,float64会打印出7位 fmt.Printf("一共有%v鸡\n", len(hens)) fmt.Printf("所有鸡总体重为%vkg 平均体重为%vkg\n", totalWeight, avgWeight) } func main() { hensFunc() } //执行结果 ➜ 02 go run main.go 一共有8鸡 所有鸡总体重为264.4kg 平均体重为33.05kg ``` ## 7.2 数组定义和内存布局 **数组的定义** var 数组名 [数组大小]数据类型 ``` var a [5]int ``` 赋初值 ``` a[0]=1 a[1]=30 ... ``` ![image.png](https://img.ukx.cn/abcdocker/2021/05/07/09f7e57d89889/09f7e57d89889.png) * 数组的地址可以通过数组名来获取 `&intArr` * 数组的第一个元素地址,就是数组的首地址 * 数组的各个元素的地址间隔是依据数组的类型决定,例如int64为8个字节,int32位4个字节,以此类推 ![image.png](https://img.ukx.cn/abcdocker/2021/05/07/813eda570d247/813eda570d247.png) ## 7.3 数组使用 **访问数组的元素** 数组名[下标] 比如: 使用a数组的第三个元素 `a[2]` >从终端循环输入5个成绩,保存到float64数组,并输出 ``` package main import ( _ "errors" "fmt" ) func main() { var source [5]float64 for i := 0; i < len(source); i++ { fmt.Printf("请输入第%d个元素的值\n", i+1) fmt.Scanln(&source[i]) } //变量数组打印 for i := 0; i < len(source); i++ { fmt.Printf("source [%d]=%v \n", i, source[i]) } } //输出结果 ➜ 03 go run main.go 请输入第1个元素的值 10.1 请输入第2个元素的值 2 请输入第3个元素的值 13 请输入第4个元素的值 4 请输入第5个元素的值 6 source [0]=10.1 source [1]=2 source [2]=13 source [3]=4 source [4]=6 ``` **数组初始化方式** ``` 有四种初始化方式 package main import ( _ "errors" "fmt" ) func main() { var num1 [3]int = [3]int {1,2,3} var num2 = [3]int {1,2,3} var num3 = [...]int {1,2,3} //可以指定元素值对应的下标 var num4 = [3]string{1:"abc",0:"aaa",2:"mmm"} fmt.Println(num1) fmt.Println(num2) fmt.Println(num3) fmt.Println(num4) } //输出结果 ➜ 03 go run main.go [1 2 3] [1 2 3] [1 2 3] [aaa abc mmm] ``` ## 7.4 数组的遍历 * 方式一: 传统for循环遍历 ``` for i := 0; i < len(source); i++ { fmt.Printf("source [%d]=%v \n", i, source[i]) } ``` * 方式二: for-range遍历 for-range遍历是go语言一种特有的结构,,可以用来遍历访问数组的元素 基本语法 ``` for index, value :=range array01{ //:=是类型推导,定义+赋值 //array01遍历的数组名称 ... } ``` 语法说明: 1.第一个返回值index是数组的下标 2.第二个value是该下标位置的值 3.他们都是仅在for循环内部可见的局部变量 4.遍历数组元素的时候,如果不想使用下标index,可以直接把下标index标为下划线`_` 5.index和value的名称不是固定的,即可以自定指定,一般命为index和value **for-range案例** for-range遍历数组 ``` package main import ( "fmt" ) func main() { var num = [3]string{"a", "b", "c"} // fmt.Println(num) for i, v := range num { fmt.Printf("i=%v v=%v\n", i, v) } fmt.Println() //如果我们只想要元素的值v,不想要下标可以通过[_]下划线来忽略 for _, v := range num { fmt.Printf("i= v=%v\n", v) } } //输出结果 ➜ 04 go run main.go i=0 v=a i=1 v=b i=2 v=c i= v=a i= v=b ``` > i,v的作用于只在for循环内有效,属于局部变量 ## 7.5 数组细节 1) 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其固定长度不能动态变化 ``` var arr01 [3]int arr01[0] = 1 arr02[1] = 2 arr03[2] = 2.2 //需要是相同类型的组合,否则报错 //其长度也要固定,不能动态调整 arr04 [3] = 10 //上面只定义了3个,这里在添加会报错 ``` 2) 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能**混用** 3) 数组创建后,如果没有赋值,默认值如下 ``` 数值类型数组(整数、浮点数): 默认值是0 字符串数组: 默认值是"" bool数组: 默认值是false ``` 4) 数组的使用步骤 * 声明数组并开辟空间 * 数组各个元素赋值(不赋值默认零值) * 使用数组 5) 数组的下标是从0开始 6) 数组下标必须在指定范围内使用,否则报`panic`错误:数组越界 ``` 例如: var arr [5]int 则有效下标为0-4 ``` 7) Go的数组属于`值类型`,在默认情况下是值传递。因此会进行值拷贝。**数组间不会互相影响** ![image.png](https://img.ukx.cn/abcdocker/2021/05/10/c7c5f7f2fac62/c7c5f7f2fac62.png) >在go中,数组的类型是数组长度的一部分 (可以理解[4]int 和 [5]int不是同一个类型) **8) 如果想在其它函数中修改原来的数组,可以使用引用传递`(指针方式)`;效率高,速度快** ![image.png](https://img.ukx.cn/abcdocker/2021/05/10/dcea3f7a91060/dcea3f7a91060.png) 代码如下: ``` package main import ( "fmt" ) func test01(arr *[3]int) { //此时arr为数组指针 (*arr)[0] = 88 //(*arr)代表使用取值符,上面已经将arr设置为指针,我们需要使用取值符取出数组的0号索引 fmt.Printf("*arr地址= %p \n", &arr) } func main() { arr := [3]int{11, 22, 33} test01(&arr) //arr默认已经是指针类型,我们需要使用&符号使用 fmt.Println("main arr=", arr) fmt.Printf("arr数组首地址= %p\n", &arr) } //输出结果 ➜ 04 go run main.go *arr地址= 0xc0000ae018 main arr= [88 22 33] arr数组首地址= 0xc0000b6000 ``` 9) 在go中长度是数组类型的一部分,在传递函数参数时需要考虑数组长度 ❎ 案例一 ``` func modify(arr []int){ arr[0] = 100 fmt.Println("modify的arr",arr) } func main(){ var arr = [...]int{1,2,3} modify(arr) } //编译错误,因为不能把[3]int传给[]int ``` ❎ 案例二 ``` func modify(arr [4]int){ arr[0] = 100 fmt.Println("modify的arr",arr) } func main(){ var arr = [...]int{1,2,3} modify(arr) } //编译错误,不能将[3]int传给][4]int,编译器判断这俩个不是同一种类型 ``` ✅ 案例三 ``` func modify(arr [3]int){ arr[0] = 100 fmt.Println("modify的arr",arr) } func main(){ var arr = [...]int{1,2,3} modify(arr) } ``` ## 7.6 数组案例 (1) 创建一个byte类型的26个元素的数组,分别放置`A`-`Z`。使用for循环访问所有元素并打印出来。提示: 字符数据运算`'A'+1 ->'B'` 思路分析 1.声明一个数组 var mychar [26]byte 2.使用for循环,利用字符可以进行运算的特点来赋值`'A' + 1`等于`B` 3.for循环打印数组输出内容 ``` package main import ( "fmt" ) func main() { var numChar [26]byte for i := 0; i < 26; i++ { numChar[i] = 'A' + byte(i) } for i := 0; i < 26; i++ { fmt.Printf("%c ", numChar[i]) } } //输出结果 ➜ 04 go run main.go A B C D E F G H I J K L M N O P Q R S T U V W X Y Z //%c按照字符输出 ``` (2) 请求出一个数组的最大值,并获得对应的下标 ``` package main import ( "fmt" ) func main() { //思路 //1.声明一个数组 var intArr[5] = [...]int {1,-1,9,90,11,9000} //2.假定第一个元素就是最大值,下标是0 //3.从第二个元素开始循环比较,如果发现有更大的则替换 //定义数组并赋值 var intArr [6]int = [...]int{1, -1, 9, 9, 111, 9000} //定义默认元素最大值为下标0 maxVal := intArr[0] maxValIndex := 0 //执行for循环 for i := 1; i < len(intArr); i++ { //从第二个元素开始比较,如果有更大的数,则进行替换 if maxVal < intArr[i] { maxVal = intArr[i] maxValIndex = i } } fmt.Printf("maxVal=%v maxValIndex=%v \n", maxVal, maxValIndex) } //执行结果 ➜ 04 go run main.go maxVal=9000 maxValIndex=5 ``` (3) 请求出一个数组的和和平均值。for-range ``` package main import ( "fmt" ) func main() { //思路 //1.声明一个数组 var intArr[5]int = [...]int{1,-1,9,99,11} //2.声明sum变量,用于求出和 //3.通过sum变量除 intArr[5]统计出平均数 var intArr [5]int = [...]int{1, -1, 9, 90, 12} sum := 0 //sum设置一个变量 for _, v := range intArr { //因为我们不需要Index数,所以这里直接使用_过滤 sum += v //计算总和 } //打印总和和平均数 fmt.Printf("intArr数组总和为%v intArr数组平均数为%v \n", sum, float64(sum)/float64(len(intArr))) //这里sum和intArr数组为int类型,但是如果我们取平均数都是用可能无法获取到小数点,为了准确性。这里强制将int类型转换为float64,在进行相除 } //执行结果 ➜ 04 go run main.go intArr数组总和为111 intArr数组平均数为22.2 ``` ### 7.6.1 数组反转 随机生成5个数,并将其反转打印 ``` 思路分析 1.随机生成5个随机数,使用rand.Intn函数 (使用time包中的纳秒生成随机数) 2.当我们得到5个随机数后,将随机数放入数组中 3.反转打印,交换的次数是数组的大小除2,倒数第一个和第一个元素交换,倒数第二个和第二个元素交换 package main import ( "fmt" "math/rand" //rand包实现了伪随机数生成器。 "time" //打印时间包包 ) func main() { var numArr [5]int //定义数组 len := len(numArr) //计算数组长度,方便后续使用 rand.Seed(time.Now().UnixNano()) //使用纳秒生成随机数,如果不使用可能造成多次执行随机数不变 for i := 0; i < len; i++ { numArr[i] = rand.Intn(100) //给每个数随机生成0~100内 } fmt.Println("交换前: ", numArr) //接下来我们需要反向打印,交换的次数为 len / 2 //倒数第一和第一个元素交换,倒数第二个和第二个元素交换 temp := 0 //做一个临时变量 for i := 0; i < len/2; i++ { temp = numArr[len-1-i] numArr[len-1-i] = numArr[i] //二次赋值 numArr[i] = temp } //打印交换后数组 fmt.Println("交换后: ", numArr) } //输出结果 ➜ 04 go run main.go 交换前: [8 42 44 65 59] 交换后: [59 65 44 42 8] ``` ## 7.7 切片基础介绍 **什么情况下使用切片?** 当我们统计学生成绩的时候,但是学生的个数不确定,这时候就需要使用切片 **切片基本介绍** 1) 切片的英文是slice 2) 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用类型的传递地址 3) 切片的使用和数组**类似**,遍历切片、访问切片的元素和求切片长度`len(slice)`都一样 4) 切片的长度是可以变换的,因此切片是一个可以动态变化的数组。 5) 切片定义的基本语法 ``` var 变量名 []类型 例如: var a []int ``` **切片快速入门** ``` package main import ( "fmt" ) func main() { //声明一个数组 var ArrNum [5]int = [5]int{1, 2, 3, 4, 5} //声明一个切片 slice := ArrNum[1:3] //slice := ArrNum[1:3] 声明一个切片 //1.slice为切片名称 //2.ArrNum[1:3] 表示slice切片引用到ArrNum这个数组 //3.引用ArrNum数组起始下标为1,最后的下班为3(不包含3) fmt.Println("ArrNum=", ArrNum) fmt.Println("slice元素是=", slice) fmt.Println("slice元素的个数=", len(slice)) fmt.Println("slice容量=", cap(slice)) //切片内容可以动态变化 //cap(内置函数) 容量: 目前切片可以存放最大个数的元素(最多元素的个数)切片的容量是可以动态变化的,容量一般是个数的2倍(不一定,有可能是1.5倍或者其它倍数) } //输出结果如下 ➜ 04 go run main.go ArrNum= [1 2 3 4 5] slice元素是= [2 3] slice元素的个数= 2 slice容量= 4 ``` ## 7.8 切片在内存中形式 **切片在内存中如何布局** ![图片.png](http://img.ukx.cn/abcdocker/2021/06/01/7df55c6fe682f/7df55c6fe682f.png) ``` 总结: 1.slice的确是一个引用类型 2.slice从底层来说,其实就是一个数据结构(struct结构体) type slice struct{ ptr *[2]int len int cap int } ``` ## 7.9 切片的使用 >切片使用包含三种方式 **● 第一种** 第一种方式: 定义一个切片,然后让切片去引用一个已经创建好的数组 ``` 参考上面案例 ``` **● 第二种** 第二种方式: 通过make来创建切片 ``` package main import "fmt" func main() { var slice []int = make([]int, 4, 10) //slice []int 定义切片数据类型 //make ([]type) make括号里面第一个为切片的类型 //4 = len切片大小 //10 = cap切片容量 fmt.Println("slice切片:", slice) //默认情况下,切片值均为0 fmt.Println("slice len=", len(slice), "\nslice cap=", cap(slice)) //重新赋值 slice[0] = 100 //slice[0]表示访问切片的第一个元素,这里的含义为将切片第一个元素进行赋值 slice[2] = 200 //slice[2]表示访问切片的第三个元素,并重新赋值 fmt.Println("slice切片:", slice) } //输出结果如下 ➜ 05 go run main.go slice切片: [0 0 0 0] slice len= 4 slice cap= 10 slice切片: [100 0 200 0] ``` 基本语法如下 ``` var 切片名 []type = make([]type,len,[cap]) 参数说明: type :就是数据类型 len: 大小 cap: 切片容量 ``` `func make`为内置函数 ``` func make(Type,size IntegerType) Type 内置函数make分配并初始化一个类型为切片、映射、或通道的对象。其第一个实参为类型,而非值。make的返回类型与其参数相同,而非指向它的指针。其具体结果取决于具体的类型 切片: size指定了其长度。该切片的容量等于其长度。切片支持第二个整数实参可用来指定不同的容量;它必须不小于其长度,,因此make([]int,0,10)会分配一个长度为0,容量为10的切片。 映射: 初始分配的创建取决于size,但产生映射的长度为0。size可以省略,这种情况下就会分配一个小的起始大小 通道: 通道的缓存根据指定的缓存容量初始化。若size为零或被忽略,该通道则无缓存 ``` **当我们通过`make` 创建的数组,只可以使用slice下标访问,无法通过函数进行访问。对外不可见** ![图片.png](http://img.ukx.cn/abcdocker/2021/06/02/8498ecdc252cb/8498ecdc252cb.png) >针对于第一种和第二种使用方式的说明 - 通过make方式创建切片可以指定切片的大小和容量 - 如果没有给切片的各个元素赋值,那么就会使用默认值[int,float=0 string = "" bool = false] - 通过make 方式创建的切片对应的数组是由make底层维护,对外不可见,即只能通过slice去访问各个元素。 **● 第三种** 定义一个切片,直接就指定具体数组,使用原理类似make的方式 ``` package main import "fmt" func main() { var slice []string = []string{"abc", "ukx", "i4t"} fmt.Println("slice=", slice) fmt.Println("slice len=", len(slice)) fmt.Println("slice cap=", cap(slice)) } //输出结果 ➜ 06 go run main.go slice= [abc ukx i4t] slice len= 3 slice cap= 3 ``` **小结** 1) 第一种使用方式是直接饮用数组,这个数组是事先存在的 2) 第二种使用方式是通过`make`来创建切片,make也会创建一个数组,是由切片在底层进行维护 ## 7.10 切片的遍历 切片的遍历和数组一样,也有两种方式 > - for循环常规遍历 > - for-range结构遍历切片 - for循环常规方式遍历演示 ``` package main import "fmt" func main() { var arr [5]int = [...]int{10, 20, 30, 40, 50} //定义数组 slice := arr[1:4] //定义切片 //for 循环遍历 for i := 0; i < len(slice); i++ { fmt.Printf("slice[%v]=%v \n", i, slice[i]) } } //输出结果如下 ➜ 06 go run main.go slice[0]=20 slice[1]=30 slice[2]=40 ``` - for-range结构遍历切片演示 ``` package main import "fmt" func main() { var arr [5]int = [...]int{10, 20, 30, 40, 50} //定义数组 slice := arr[1:4] //定义切片 //for range循环遍历 for i, v := range slice { fmt.Printf("i=%v v=%v \n", i, v) } } //输出结果 ➜ 06 go run main.go i=0 v=20 i=1 v=30 i=2 v=40 ``` ## 7.11 append动态添加 >append内置函数,可以对切片进行动态添加 `func append(slice []type, elems ...Type) []Type` 内建函数append将元素追加到切片的末尾。若它有足够的容量,其目标就会重新切片以容纳新的元素。否则,就会分配一个新的基本数组。append返回更新后的切片,因此必须存储追加后的结果 案例演示: ``` package main import "fmt" func main() { //使用append内置函数,对切片进行动态追加 var slice []int = []int{10, 20, 30} //通过append直接给slice追加具体元素 slice = append(slice, 31, 32) fmt.Println("slice=", slice) } //输出结果 ➜ 06 go run main.go slice= [10 20 30 31 32] ############################################## package main import "fmt" func main() { //使用append内置函数,对切片进行动态追加 var slice []int = []int{10, 20, 30} //通过append直接给slice追加具体元素 slice = append(slice, 31, 32) fmt.Println("slice=", slice) //另外一种追加方式,通过将slice的切片追加给自己 slice = append(slice, slice...) //后面的slice也可以是别切片,但是后面的3个点位固定写法 (...前面的必须为切片,不能是数组) fmt.Println("slic2=", slice) } //输出结果 ➜ 06 go run main.go slice= [10 20 30 31 32] slic2= [10 20 30 31 32 10 20 30 31 32] ``` **切片append操作的底层原理分析:** 1) 切片append操作的本质就是对数组库容 2) go底层会创建一个新的数组newArr (安装扩容后大小) 3) 将slice原来包含的元素拷贝到新的数组newArr 4) slice重新引用到newArr 5) 注意newArr是在底层来维护的 ## 7.12 切片拷贝操作 切片使用copy内置函数完成拷贝,案例如下 >**slice1和slice2数据空间是独立的,相当于值拷贝** ``` package main import "fmt" func main() { var slice1 []int = []int{1, 2, 33} var slice2 = make([]int, 10) //使用copy函数进行拷贝切片 copy(slice2, slice1) //slice2为需要覆盖的切片,slice1为需要拷贝的切片 fmt.Println("slice1=", slice1, "slice2=", slice2) } //输出结果 ➜ 06 go run main.go slice1= [1 2 33] slice2= [1 2 33 0 0 0 0 0 0 0] ``` >**提示:** copy(para1,para2): para1和para2都是**切片类型** 当slice拷贝到数值a中,并且a的数只有一个,copy拷贝并不会扩容,只会拷贝第一个元素 ``` package main import "fmt" func main() { var a []int = []int{1, 2, 3, 4, 5} var slice = make([]int, 1) fmt.Println(slice) copy(slice, a) fmt.Println(slice) } //输出结果 ➜ 06 go run main.go [0] [1] ``` ## 7.13 切片属于引用类型 切片属于引用类型,所以在传递时,遵守引用传递机制。 >案例1 ``` package main import "fmt" func main() { var slice []int var arr [5]int = [...]int{1, 2, 3, 4, 5} //创建了一个数组 slice = arr[:] //数组所有元素赋值给切片 var slice2 = slice //将slice切片赋值给slice2切片 slice[0] = 10 fmt.Println("slice2", slice2) //输出多少? fmt.Println("slice", slice) //输出多少? fmt.Println("arr", arr) //输出多少? } //输出结果 ➜ 06 go run main.go slice2 [10 2 3 4 5] slice [10 2 3 4 5] arr [10 2 3 4 5] ``` >案例2 ``` package main import "fmt" func test(slice []int) { slice[0] = 100 //这里修改slice[0],会改变实参 } func main() { var slice = []int{1, 2, 3, 4} fmt.Println("slice=", slice) test(slice) fmt.Println("slice", slice) //由于test函数修改的下标0元素,此时输出应该是[100,2,3,4] } //输出结果 ➜ 06 go run main.go slice= [1 2 3 4] slice [100 2 3 4] ``` ## 7.14 STRING和SLICE - string底层是一个byte数组,因此string也可以进行切片处理 ``` package main import "fmt" func main() { //string底层是一个byte数组,因此string也可以进行切片处理 str := "hello@abcdocker" //使用切片进行获取abcdocker slice := str[6:] fmt.Println("slice=", slice) } //输出结果 ➜ 06 go run main.go slice= abcdocker ``` - string和切片内存示意图 ![image.png](https://img.ukx.cn/abcdocker/2021/06/07/3bcefa6c59a65/3bcefa6c59a65.png) - string是不可变的,也就是不能通过`str[0] = 'z'`方式来修改字符串 案例如下: ![image.png](https://img.ukx.cn/abcdocker/2021/06/07/27a3681d793ef/27a3681d793ef.png) - 如果需要修改字符串,可以先将string --> []byte(切片) / 或者 [] runne (切片) ->修改->重写转成string ![image.png](https://img.ukx.cn/abcdocker/2021/06/07/0c6524781805d/0c6524781805d.png) >byte类型是无法处理中文,英文是占用1个字节,中文是占用3个字节。 所以如果使用byte处理中文会出现乱码的情况 解决方法: 将string转成 []rune即可,因为[]rune是按字符处理,兼容汉字 ``` package main import "fmt" func main() { //string底层是一个byte数组,因此string也可以进行切片处理 str := "hello@abcdocker" //使用切片进行获取abcdocker slice := str[6:] //获取第六个到最后一个 fmt.Println("slice=", slice) // 将hello@abcdocker修改为iello@abcdocker //将str修改为byte类型的数组 arr1 := []rune(str) //替换字符 arr1[0] = '你' //将类型转换回string str = string(arr1) fmt.Println("str=", str) } //输出结果 ➜ 06 go run main.go slice= abcdocker str= 你ello@abcdocker ``` ## 7.15 切片Demo 编写一个函数`fbn(n int)` 1) 可以接收一个n int 2) 能够将斐波那契的的数列放到切片中 3) 提示,斐波那契的数列形式 `arr[0]=1;arr[1]=1;arr[2]=2;arr[3]=3;arr[4]=5;arr[5]=8` >斐波那契的规律为前面两个数相加的和等于下一位数 代码如下 ``` package main import ( "fmt" ) //为什么不使用数组,因为数组在声明的时候就需要把数量确定下来,但是切片make的时候进行分配空间。所以下面使用切片进行创建 /* 思路: 1.声明一个函数,让它返回一个切片 fbn(n int) ([uint64]) 2.编写斐波那契 (n int)进行for循环来存放斐波那契数列 (0对应1,1对应的也是1) */ //编写函数切片 func fbn(n int) ([]uint64){ //返回一个切片 //声明一个切片,切片大写 n fbnSlice := make([]uint64, n ) //第一个数和第二个数斐波那契数为1 fbnSlice[0] = 1 fbnSlice[1] = 1 //使用for循环来存放斐波那契的数列 for i := 2;i < n; i ++{ fbnSlice[i] = fbnSlice[i - 1] + fbnSlice[i -2] //arr[0]=1;arr[1]=1;arr[2]=2;arr[3]=3;arr[4]=5;arr[5]=8 //斐波那契的规律为前面两个数相加的和等于下一位数,其中0和1数值都为1 } //接下来我们将切片返回 return fbnSlice } func main(){ //测试 v := fbn(10) //给fbn函数传入一个10 (代表求出10个斐波那契数),并使用v变量名进行接收 fmt.Println("v=",v) //打印结果 } //输出结果如下 PS C:\Users\Administrator\Desktop\GOstudy\day3\01> go run .\main.go v= [1 1 2 3 5 8 13 21 34 55] ``` # 八、排序和查找 ## 8.1 排序基本介绍 排序是将一组数据,以指定的顺序进行排列的过程。 **排序的分类:** * 内部排序: 指将需要处理的所有数据都加载到内存中进行排序。包括(`交换式排序法`、`选择式排序法`和`插入式排序法`),适用于量小的场景 交换式排序: 交换式排序属于内部排序法,是运用数据值比较后,依判断规则对数据位置进行交换,以达到排序的目的。交换式排序法又可以分为两种:**(1) 冒泡排序法(Bubble sort)** (2)快速冒泡排序法(Quick sort) * 外部排序: 数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。包括(`合并排序法`和`直接合并排序法`) ## 8.2 冒泡排序 交换式排序法-冒泡排序(Bubble Sorting)的基本思想是: 通过对待排序序列从后向前(从下标最大的元素开始),依次比较相邻元素的排序码,若发现逆序则交换,使排序码较小的元素逐渐从后部移向前部(从下标最大的单元移向下标较小的单元) 因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。 ![图片.png](https://image.abcdocker.com/abcdocker/2021/06/21/dfe532da52b51/dfe532da52b51.png) >冒泡排序规则 - 一共会经过arr.lengh-1的轮次比较,每一轮将会确定一个数的位置 - 每一轮的比较次数再逐渐减少 [5,4,3,2,1] - 当发现前面的一个数比后面的一个数大时候就进行交换。 **Demo演示** ``` // 首先我们使用for循环一步步打印,先排序最大的数 package main import ( "fmt" ) //3. 创建函数,进行排序 func BubbleSort(arr *[5]int) { //4.传入指针类型数组,此时函数中的arr 等于main函数中的arr //6.定义临时变量,接收数值 temp := 0 //5.按照大小进行排序,最大数放在最后 for j := 0; j < 4; j++ { //根据图片需要进行4次比较,所以我们写4 //(*arr)表示数组,[j]代表元素,第几位元素 if (*arr)[j] > (*arr)[j+1] { //6. (*arr)进行取值运算,如果前面的数大于后面的数就进行交换 //7.通过临时变量进行交换 temp = (*arr)[j] (*arr)[j] = (*arr)[j+1] (*arr)[j+1] = temp } } fmt.Println("第一次排序arr=", (*arr)) } func main() { //1. 定义数组 arr := [5]int{11, 22, 44, 1, 8} //数组大小为5 //2. 将数组传递给一个函数,完成排序 BubbleSort(&arr) } //输出结果 ➜ 07 go run main.go 第一次排序arr= [11 22 1 8 44] ``` 接下来我们可以设置第二轮排序,实际上只需要复制`for`循环中的变量即可 ``` //第二轮排序 for j := 0; j < 3; j++ { //按照上图说明,每一次排序都会确认一位,所以我们这里减1 if (*arr)[j] > (*arr)[j+1] { temp = (*arr)[j] (*arr)[j] = (*arr)[j+1] (*arr)[j+1] = temp } } fmt.Println("第二次排序arr=", (*arr)) ``` 最后拷贝4个for循环 ``` package main import ( "fmt" ) //3. 创建函数,进行排序 func BubbleSort(arr *[5]int) { //4.传入指针类型数组,此时函数中的arr 等于main函数中的arr //6.定义临时变量,接收数值 temp := 0 //5.按照大小进行排序,最大数放在最后 for j := 0; j < 4; j++ { //根据图片需要进行4次比较,所以我们写4 //(*arr)表示数组,[j]代表元素,第几位元素 if (*arr)[j] > (*arr)[j+1] { //6. (*arr)进行取值运算,如果前面的数大于后面的数就进行交换 //7.通过临时变量进行交换 temp = (*arr)[j] (*arr)[j] = (*arr)[j+1] (*arr)[j+1] = temp } } fmt.Println("第一次排序arr=", (*arr)) //第二轮排序 for j := 0; j < 3; j++ { //按照上图说明,每一次排序都会确认一位,所以我们这里减1 if (*arr)[j] > (*arr)[j+1] { temp = (*arr)[j] (*arr)[j] = (*arr)[j+1] (*arr)[j+1] = temp } } fmt.Println("第二次排序arr=", (*arr)) //第三轮排序 for j := 0; j < 2; j++ { //按照上图说明,每一次排序都会确认一位,所以我们这里减1-1 if (*arr)[j] > (*arr)[j+1] { temp = (*arr)[j] (*arr)[j] = (*arr)[j+1] (*arr)[j+1] = temp } } fmt.Println("第三次排序arr=", (*arr)) //第三轮排序 for j := 0; j < 1; j++ { //按照上图说明,每一次排序都会确认一位,所以我们这里减1-1-1 if (*arr)[j] > (*arr)[j+1] { temp = (*arr)[j] (*arr)[j] = (*arr)[j+1] (*arr)[j+1] = temp } } fmt.Println("第四次排序arr=", (*arr)) } func main() { //1. 定义数组 arr := [5]int{11, 22, 44, 1, 8} //数组大小为5 //2. 将数组传递给一个函数,完成排序 BubbleSort(&arr) } //输出结果 ➜ 07 go run main.go 第一次排序arr= [11 22 1 8 44] 第二次排序arr= [11 1 8 22 44] 第三次排序arr= [1 8 11 22 44] 第四次排序arr= [1 8 11 22 44] ``` ✅ 正确冒泡排序如下 ``` package main import ( "fmt" ) //3. 创建函数,进行排序 func BubbleSort(arr *[5]int) { //4.传入指针类型数组,此时函数中的arr 等于main函数中的arr //6.定义临时变量,接收数值 temp := 0 //8.外层嵌套for循环 for i := 0; i < len(*arr)-1; i++ { //5.按照大小进行排序,最大数放在最后 for j := 0; j < len(*arr)-1-i; j++ { //根据图片需要进行4次比较,数组长度-1在-i //(*arr)表示数组,[j]代表元素,第几位元素 if (*arr)[j] > (*arr)[j+1] { //6. (*arr)进行取值运算,如果前面的数大于后面的数就进行交换 //7.通过临时变量进行交换 temp = (*arr)[j] (*arr)[j] = (*arr)[j+1] (*arr)[j+1] = temp } } fmt.Println("第",i ,"次排序arr=", (*arr)) } } func main() { //1. 定义数组 arr := [5]int{11, 22, 44, 1, 8} //数组大小为5 //2. 将数组传递给一个函数,完成排序 BubbleSort(&arr) } //输出结果 ➜ 07 go run main.go 第 0 次排序arr= [11 22 1 8 44] 第 1 次排序arr= [11 1 8 22 44] 第 2 次排序arr= [1 8 11 22 44] 第 3 次排序arr= [1 8 11 22 44] ``` >冒泡面试可能需要手写,所以需要背下来。 ## 8.3 查找 在Golang只能给,常用的查找有两种: * 顺序查找 * 二分查找 (该数组是有序) ### 8.3.1 顺序查找 顺序查找可以通过下面的案例进行演示 数列: 白眉鹰王、金毛狮王、紫衫龙王、青翼蝠王,从键盘中任意一个名称,判断数列中是否包含此名称 **第一种方式** ``` package main import ( "fmt" ) func main() { //定义数组 names := [4]string{"白眉鹰王", "金毛狮王", "紫衫龙王", "青翼蝠王"} var inputName string fmt.Println("请输入任意名称") fmt.Scanln(&inputName) //第一种for循环方式 for i := 0; i < len(names); i++ { if inputName == names[i] { fmt.Printf("匹配到%v,下标为%v\n", inputName, i) break } else if i == len(names) - 1 { //判断如果已经循环了4次还没匹配到就提示没有找到并退出 fmt.Println("没有匹配,请重新输入!") } } } //输出结果 ✅ 输入正确结果 ➜ 08 go run main.go 请输入任意名称 金毛狮王 匹配到金毛狮王,下标为1 ❌ 输入错误结果 ➜ 08 go run main.go 请输入任意名称 123 没有匹配,请重新输入! ``` **第二种方式** 推荐使用 ``` package main import ( "fmt" ) func main() { //定义数组 names := [4]string{"白眉鹰王", "金毛狮王", "紫衫龙王", "青翼蝠王"} var inputName string fmt.Println("请输入任意名称") fmt.Scanln(&inputName) //第二种for循环方式 input := -1 for i := 0; i < len(names); i++ { if inputName == names[i] { input = i fmt.Printf("匹配到%v,下标为%v\n", inputName, i) break } } if input == -1 { fmt.Println("没有匹配") } } ``` ### 8.3.2 二分查找 二分查找思路分析: ![image.png](https://img.ukx.cn/abcdocker/2021/06/24/b7b7542b9b939/b7b7542b9b939.png) 二分查找代码: ``` package main import ( "fmt" ) //2.创建二分查询函数 func BinaryFind(arr *[6]int, leftIndex int, rightIndex int, findVal int) { //数组大小为6个,类型为int类型。需要定义leftIndex和rightIndex,确认从哪个下标开始,还需要定义findVal //先找到中间的下标middle middle := (leftIndex + rightIndex) / 2 //确定完下标后,接下来开始判断 // 3.判断leftIndex是否大于rightindex if leftIndex > rightIndex { fmt.Println("找不到") return } // 4.判断 if (*arr)[middle] > findVal { // arr是指针,需要用括号加*号取值 //middle > findVal 说明我们要查找的书应该在 leftIndex --- middle -1中间 BinaryFind(arr, leftIndex, middle-1, findVal) //传入参数 } else if (*arr)[middle] < findVal { //middle < findVal 说明我们要查找的书应该在 middle +1 --- rightIndex中间 BinaryFind(arr, middle+1, rightIndex, findVal) //传入参数 } else { //大于小于都没有就是可能是等于 fmt.Printf("找到了,查到的数为%v 下标为%v\n", findVal,middle) } } func main() { //1.数组列表 arr := [6]int{1, 8, 10, 89, 1000, 2000} //需要有序排序 //5.调用函数 BinaryFind(&arr,0, len(arr) -1 , 10 ) //arr数组地址,0 left下标(代表最小),len(arr) -1 表示最大下标rightindex,需要- 1不然跑到2000后面了 //最后一个10代表我们需要查找的数 //这里我们需要传入地址,因为BinaryFind函数内部接收的是一个数组的指针 } //输出结果 ➜ 09 go run main.go 找到了,查到的数为10 下标为2 ``` ## 8.4 二维数组介绍 二维数组常见于五子棋游戏,游戏地图,棋盘等 ## 8.5 二维数组快速入门 案例: 请使用二维数组输出如下图形 > 0 0 0 0 0 0 > 0 0 1 0 0 0 > 0 2 0 3 0 0 > 0 0 0 0 0 0 >**所谓二维数组就是一维数组里面的元素又是数组** **二维数组实际上就是一维数组的扩展而已** ``` package main import ( "fmt" ) func main() { //首先定义/声明二维数组 var arr [4][6]int //可以理解4个数组,每个数组里面还有6个int,数组套数组 //接下来进行复制,否则默认打印都为0 arr[1][2] = 1 arr[2][1] = 2 //一维数组的第二个数组中的下标为1的数 arr[2][3] = 3 for i := 0; i < 4; i++ { for j := 0; j < 6; j++ { fmt.Print(arr[i][j], " ") //[i]打印最外层,[j]打印数组里面的数组 } fmt.Println() //打印空行,否则都竖着打印了 } } //输出结果 ➜ 10 go run main.go 0 0 0 0 0 0 0 0 1 0 0 0 0 2 0 3 0 0 0 0 0 0 0 0 ``` **二维数组使用方式** 第一种: - 语法: var 数组名[大小][大小]类型 - 比如: `var aa [2][3]int[][]`,在赋值 第二种: - 声明; var数组名[大小][大小]类型=[大小][大小]类型{{初值}},{{初值..}} - 赋值 (有默认值,比如int 类型就是0) 赋值 ``` package main import "fmt" func main() { var arr [2][3]int = [2][3]int{{1, 2,3}, {4, 2,5}} //一共有2个数组,每个数组里面有3个元素 fmt.Println("arr=", arr) } //输出结果 ➜ 11 go run main.go arr= [[1 2 3] [4 2 5]] ``` ## 8.7 二维数组在内存中存在形式 ![1624866035289.png](https://img.ukx.cn/abcdocker/2021/06/28/e5c6d30cf4b61/e5c6d30cf4b61.png) ## 8.8 二维数组的遍历 二维数组的遍历有两种,第一种是`双for循环`完成,第二种是`for-range方式`完成遍历 - 双层for循环案例 ``` package main import "fmt" func main() { arr := [2][3]int{{1, 2, 3}, {4, 2, 5}} //一共有2个数组,每个数组里面有3个元素 //首先打印第一个数组最外层的 for i := 0; i < len(arr); i++ { //这里的for循环实际上就是执行arr[2],2个数字 for j := 0; j < len(arr[i]); j++ { //这里的for循环实际上就是打印每个数组里面的元素 fmt.Printf("%v", arr[i][j]) //打印时添加可以增加\t增加美观 } //每执行一个数组就进行换行 fmt.Println() } } //执行结果 ➜ 11 go run main.go 123 425 ``` - for-range方式案例 ``` package main import "fmt" func main() { arr := [2][3]int{{1, 2, 3}, {4, 2, 5}} //一共有2个数组,每个数组里面有3个元素 for i, v := range arr { //最外层数组,i=有2个数组,v=是一维数组的值,不是某个元素 for j, v2 := range v { //j表示第几个元素,v2表示值 fmt.Printf("arr[%v][%v]=%v \t", i, j, v2) } fmt.Println() //换行 } } //执行结果 ➜ 11 go run main.go arr[0][0]=1 arr[0][1]=2 arr[0][2]=3 arr[1][0]=4 arr[1][1]=2 arr[1][2]=5 ``` ## 8.9 二维数组案例 **要求:** 定义二维数组,用于保存三个班,每个班五名同学成绩,要求出每个班级平均分、以及所有班级平均分 ``` package main import ( "fmt" ) /* **要求:** 定义二维数组,用于保存三个班,每个班五名同学成绩,要求出每个班级平均分、以及所有班级平均分 */ func main() { //1.定义一个二维数组 var scores [3][5]float64 //2.for循环遍历数组 for i := 0; i < len(scores); i++ { for j := 0; j < len(scores[i]); j++ { fmt.Printf("请输入第%d个班级的第%d个学生成绩\n", i+1, j+1) fmt.Scanln(&scores[i][j]) } } //3.遍历输出成绩后的二维数组,统计平均分 totalSum := 0.0 //用于累计所有班级平均分 for i := 0; i < len(scores); i++ { sum := 0.0 //定义每个班级的总分 for j := 0; j < len(scores[i]); j++ { sum += scores[i][j] } totalSum += sum fmt.Printf("第%d班级的总分为%v 平均分为%v\n", i+1, sum, sum/float64(len(scores[i]))) } fmt.Printf("所有班级的总分为%v 所有班级的平均分为%v \n", totalSum, totalSum/15) fmt.Println(scores) } ``` # 九、MAP ## 9.1 Map基本介绍 map是key-value数据结构,又称为字段或者关联数组。类似其它语言的**集合** ## 9.2 Map声明 基本语法 ``` var map变量名 map[keytype]valuetype #后面的map为关键字,必须要填写 #keytype key的类型 #valuetype valuet类型 ``` **key可以是什么类型** golang中的map的key可以是很多种类型,例如bool、数字、string、指针、channel(管道),还可以是只包含前面几个类型的结构体、接口、数组等。通常为`int`、`string` >注意: slice,map还有function不可以,因为这几个没法用==来判断 **value可以是什么类型** valuetype的类型和key基本上一样,通常为:`数字(整数,浮点数)`、`string`、`map`、`strcuct` **map 声明的举例** ``` var a map[string]string //key为string,值为string var a map[string]int var a map[int]string var a map[string]map[string]string //key为string,值为另外一个map ``` >**注意:** 声明是不会分配内存的,初始化需要make,分配内存后才能赋值和使用 ![1625551273246.png](https://img.ukx.cn/abcdocker/2021/07/06/24ff44d58cd83/24ff44d58cd83.png) ## 9.3 Map使用 ``` package main import ( "fmt" ) func main() { var a map[string]string //声明map //因为map默认不分配数据空间(内存),所以我们需要make创建一个数据空间 a = make(map[string]string, 10) //make的第一个参数要写map的数据类型,10代表分配10对key-value a["n1"] = "abc" //赋值 a["n3"] = "ukx" a["n2"] = "i4t" //如果key的值相同,map会进行覆盖 fmt.Println(a) //key不可以重复,值可以重复 //map无法保证每次都是有序 } ``` ### 9.3.1 Map使用方式 map使用方式有下面几种 `方式一` ``` //声明 var abc map[string]string //make make(map[string]string,10) abc = make(map[string]string,10) ``` `方式二` ✅ 推荐使用 ``` //声明就直接make abc := make(map[string]string) abc["no1"] = "北京" abc["no2"] = "上海" abc["no3"] = "深圳" fmt.Println(abc) ``` `方式三` ``` //声明,直接赋值 var abc map[string]string = map[string]string{ "no1" : "北京", "no2" : "上海", } 或者~ abc := map[string]string{ "no1" : "北京", "no2" : "上海", } ``` >应用案例: 演示一个key-value的**value是map**的类型 例如: 存放3个学生信息,每个学生信息有name和sex信息 思路: map[string]map[string]string (第一个map为学生学号,后面的map为name和性别) ``` qwe := make(map[string]map[string]string) //后面的值是一个map类型,所以还需要make一下 qwe["n1"] = make(map[string]string, 2) qwe["n1"]["name"] = "丛宇鸿" qwe["n1"]["sex"] = "男" qwe["n2"] = make(map[string]string, 2) qwe["n2"]["name"] = "abc" qwe["n2"]["sex"] = "女" //如果我们只想打印一个信息,之后需要输入第一个key即可 fmt.Println(qwe["n2"]) //取出第二个学生的完全信息 fmt.Println(qwe["n2"]["sex"]) //只要第二个学生的性别 fmt.Println(qwe) ``` ## 9.4 Map增删改查 - **增、改** map["key"] = value 如果key还没有就增加,如果已存在就修改 - **删** map删除使用`delete`内置函数,如果key存在,就删除该key-value,如果key不存在,不操作,但是也不会报错 ``` abc["no1"] = "成都" delete(abc,"no1") fmt.Println(abc) ``` 如果我们要删除map的所有key,没有一个专门的方法一次性删除,**可以遍历一下key**,然后进行逐个删除 或者通过make一个新的,让原来的称为垃圾,被gc回收 ``` func main() { abc := map[string]string{ "no1": "北京", "no2": "上海", } abc = make(map[string]string) fmt.Println(abc) } ``` - **查** ``` v, key := abc["no1"] if key { fmt.Println("no1存在,值为", v) } else { fmt.Println("no1不存在") } ``` ## 9.5 Map遍历 for range遍历支持map,但是不能保证每次遍历次序是一样的。 ``` package main import ( "fmt" ) func main() { qwe := make(map[string]map[string]string) //后面的值是一个map类型,所以还需要make一下 qwe["n1"] = make(map[string]string, 2) qwe["n1"]["name"] = "丛宇鸿" qwe["n1"]["sex"] = "男" qwe["n2"] = make(map[string]string, 2) qwe["n2"]["name"] = "abc" qwe["n2"]["sex"] = "女" //使用for-range遍历比较复杂的map for k, v := range qwe { //这里的k=n1,第一个map的key //这里的v=map,是一个map,所以需要在加一层for range遍历 fmt.Println("k=", k) for k1, v1 := range v { //因为外层的v是一个for循环,我们直接循环v这个值即可 fmt.Printf("\tk1=%v v=%v\n", k1, v1) //这里的k1代表的是map里面的key,v1就是key对应值 //\t为制表符,有层级关系 } fmt.Println() } } //输出结果 ➜ 14 go run main.go k= n1 k1=name v=丛宇鸿 k1=sex v=男 k= n2 k1=name v=abc k1=sex v=女 ``` **通过len统计map长度** ``` fmt.Printf("qwe函数一共有 %v 对\n",len(qwe)) ``` ## 9.6 Map切片 切片的数据类似如果是map,则我们称为slice of map,map切片,这样使用则**map个数就可以动态变化** ``` // 使用一个map来记录monster的信息name和age,也就是说一个monster对应一个map,并且monster的个数可以动态变化 ``` >需要使用golang内置函数append函数,进行动态增加,前面数组中已经介绍过了 ``` package main import ( "fmt" ) func main() { //声明一个map切片,并进行make monsters := make([]map[string]string, 2) if monsters[0] == nil { monsters[0] = make(map[string]string, 2) //在使用的过程中,需要make monsters[0]["name"] = "牛博网" monsters[0]["age"] = "200" } // fmt.Println(monsters) //使用append进行动态扩容切片 newmonsters := map[string]string{ "name": "牛老师", "age": "200", } monsters = append(monsters, newmonsters) //将newmonsters追加到monsters,并且重新赋值 fmt.Println(monsters) } //输出结果 ➜ 16 go run main.go [map[age:200 name:牛博网] map[] map[age:200 name:牛老师]] ``` ## 9.7 Map排序 1.golang中没有一个专门的方法针对map的key进行排序 2.golang中map默认是无序的,也不是按照添加顺序存放,所以每次遍历可能不一样 3.golang中map的排序是先将key进行排序,然后根据key值遍历输出即可 ``` package main import ( "fmt" "sort" //引用排序 ) func main() { num := make(map[int]int) num[1] = 10424240 num[2] = 10 num[3] = 1044 num[4] = 1041 num[55] = 10414 num[22221] = 104 num[2222] = 10441 // fmt.Println(num) //1.先将map的key放入切片中 //2.对切片进行排序 //3.遍历切片,然后按照key来输出map的值 var keys []int //循环num[1--22221] for k, _ := range num { keys = append(keys, k) //将k 追加到keys切片中 } //使用sort进行排序 sort.Ints(keys) //需要排序的切片 fmt.Println(keys) //将num1...num222221进行输出 fmt.Println() //遍历切片,输出值 for _, k := range keys { //这里的_是下标,我们不需要下标,所以忽略 fmt.Printf("num[%v] = %v\n", k, num[k]) //这里的k就是我们num[1...2222]的值,num[k] 就是num[1] 对应的结果 } } //输出结果 ➜ 17 go run main.go [1 2 3 4 55 2222 22221] num[1] = 10424240 num[2] = 10 num[3] = 1044 num[4] = 1041 num[55] = 10414 num[2222] = 10441 num[22221] = 104 ``` ## 9.8 Map 注意事项 - map是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改会,会直接修改原来的map ``` package main import ( "fmt" ) func test(map1 map[int]int){ map1[10] = 100 } func main(){ map1 := make(map[int]int) map1[1] = 1 map1[2] = 14 map1[9] = 20 map1[10] = 15 test(map1) fmt.Println(map1) } ``` - map动态扩容达到后,再想增加map元素,会自动扩容,map**可以动态的增长键值对`key-value`** - map的value也经常使用struct(结构体)类型,更适合管理复杂的数据 ## 9.9 map常见演示案例 1.使用map[string]map[string]string的map类型 2.key: 表示某用户名,是唯一的,不可以重复 3.如果某个用户粗奴在,就将其密码修改`888888`,如果不存在就增加这个用户信息(包括昵称nickname和密码pwd) 4.编写一个函数modifyUser(users map[string]map[string]string,name string)完成上述功能 ``` package main import ( "fmt" ) //定义函数 func modifyUser(users map[string]map[string]string, name string) { //map的key是一个string类型,value是一个map类型(里面有2对),定义了一个name,也是string类型 //modifyUser函数接收users和name参数 //简单就是传一个用户名,判断用户名中是否有这个密码 //判断users中有没有对应的名字 if users[name] != nil { //如果users[name]有这个用户了,就是不等于 //有这个用户 users[name]["passwd"] = "888888" } else { //没有这个用户就增加用户名和密码 //首先make一个map users[name] = make(map[string]string, 2) //需要有2对,昵称和密码 users[name]["passwd"] = "888888" users[name]["nickname"] = "新增用户昵称" + name } } func main() { //定制users map //后面的值也是map,所以我们在使用前还需要在make一下 users := make(map[string]map[string]string, 10) //这里我们定义一个存在的用户,即存在用户只会修改密码 users["i4t"] = make(map[string]string) //因为value是map,所以这里直接写密码是要make的 users["i4t"]["passwd"] = "111111" users["i4t"]["nikename"] = "运维啊" //传入users和name到modifyUser函数中 modifyUser(users, "docker") modifyUser(users, "test") modifyUser(users, "i4t") //打印结果 fmt.Println(users) } ``` ![1626171499838.png](https://img.ukx.cn/abcdocker/2021/07/13/c59b72b853683/c59b72b853683.png) # 十、面向对象 - golang也支持面向对象编程`OOP`,和传统的面向对象编程有区别,并不是**纯粹的面向对象语言**。所以说golang支持面向对象编程特性是比较准确的 - golang没有类(class),go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,可以理解golang是基于struct来实现OOP特性的 - golang面向对象编程非常简,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等 - golang有面向对象的继承、封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承: golang没有extends关键字,继承是通过匿名字段来实现 - golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(type system)的一部分,通过接口关联,耦合性低 ## 10.1 结构体 1) 结构体是自定义的数据类型 2) 结构体变量(实例)是具体的,实际的,代表一个具体的变量 ``` package main import ( "fmt" ) type Cat struct { //定义一个名称为cat的struct Name string //名称 Age int //年龄 Eat string } func main() { // e := Cat var e Cat e.Name = "abc" //复制,如果struct不赋值,默认都是空或者0 e.Age = 4 e.Eat = "<・)))><<" fmt.Println(e) fmt.Printf("名称:%v 年龄:%v 喜欢:%v \n", e.Name, e.Age, e.Eat) } //输出结果 ➜ 01 go run main.go {abc 4 <・)))><<} 名称:abc 年龄:4 喜欢:<・)))><< ``` **结构体变量在内存中的布局** 当我们声明一个结构体时,它的内存变量已经生成。结构体是一个值类型 ![1626346801158.png](https://img.ukx.cn/abcdocker/2021/07/15/f05afe42199a6/f05afe42199a6.png) ### 10.1.1 声明结构体 声明结构体基本语法 ``` type 结构体名称(标示符) struct{ field1 type //字段名称及字段类型 field2 type } 如果我们定义的结构体名称及字段名称首字母大写,表示可以在其他包中使用 ``` >在结构体字段中,一般是基本数据类型、数组、引用类型 **指针、slice和map的默认值都是nil,即还没有分配空间 (需要make后使用)** ``` //声明struct type Person struct{ ptr *int //指针类型 slice []int //切片类型 map1 map[string]string //map类型 } **** 下面为引用 ***** //使用slice,需要make p1.slice = make([]int, 10) p1.slice[0] = 100 //使用map,也需要make p1.map1 = make(map[string]string) p1.map["key1"] = "docker" fmt.Println(p1) ``` **不同结构体变量**的字段是独立的,互不影响,一个结构体变量字段的更改不会影响另外一个 >结构体是值类型,它们之间的拷贝是通过值拷贝,不会互相影响 ``` package main import ( "fmt" ) type test struct { Name string Age int } func main() { var a test a.Name = "i4t" a.Age = 15 fmt.Printf("默认a=%v\n", a) //我们将a通过值拷贝到b b := a b.Name = "docker" b.Age = 20 fmt.Printf("b=%v\n", b) fmt.Printf("a=%v\n", a) } //输出结果 ➜ 02 go run main.go 默认a={i4t 15} b={docker 20} a={i4t 15} ``` `b := a `在内存中的变化 ![1626419822344.png](https://img.ukx.cn/abcdocker/2021/07/16/959fd6d31342a/959fd6d31342a.png) ### 10.1.2 创建结构体变量的四种方式 - 方式一,直接声明 ``` var presson Person 变量名称 结构体 ``` - 方式二,{} ``` 案例 var person Person = Preson{} package main import ( "fmt" ) type Fname struct { Name string Age int Like string } func main() { v := Fname{} v.Name = "abc" v.Age = 18 v.Like = "eat" fmt.Println(v) //还可以直接在v :=直接引用 v1 := Fname{"i4t", 20, "computer"} fmt.Println(v1) } //输出结果 ➜ 03 go run main.go {abc 18 eat} {i4t 20 computer} ``` - 方式三 (new) `var person *Person = new(Person)` 因为person是一个指针,因此标准写法为`(*person).Name = "abc"`,当然也可以采用简化的写法`person.Name = "abc"` 原因: go设计者为了程序员方便使用,底层会对`person.Name = "abc"` 进行处理,会自动给**person** 加上取值运算`(*person).Name = "abc"` ``` package main import ( "fmt" ) type Fname struct { Name string Age int Like string } func main() { v := Fname{} v.Name = "abc" v.Age = 18 v.Like = "eat" fmt.Println(v) var person *Fname = new(Fname) //通过创建指针来修改变量 person.Name = "i4t" person.Age = 19 person.Like = "play" fmt.Println(*person) } //输出结果 ➜ 03 go run main.go {abc 18 eat} {i4t 19 play} ``` - 方式四 (-&) > **1. 方式三和方式四返回的是`结构体`** **2. 结构体指针访问字段的标准方式应该是: (*结构体指针).字段名** 例如:`(*person).Name = "docker"` **3. 但go做了一个简化,也支持结构体指针.字段名**,比如`person.Name = "i4t"`。go编辑器底层对person.Name做了转化(*person).Name `var person *Person = &Person{}` 案例演示 ``` package main import ( "fmt" ) type Fname struct { Name string Age int Like string } func main() { v := Fname{} v.Name = "abc" v.Age = 18 v.Like = "eat" fmt.Println(v) var person *Fname = &Fname{} //struct结构体名称为Fname (*person).Name = "go" (*person).Age = 20 //同样也可以使用简化的命令 person.Name = "Time" person.Age = 21 person.Like = "playgame" fmt.Println(*person) //打印person变量 } //输出结果 ➜ 03 go run main.go {abc 18 eat} {Time 21 playgame} ``` 同样,也可以在创建时直接赋值 ``` var person *Fname = &Fname{"abc",18} ``` **总结:** 1.第三种和第四种方式返回的是`结构体指针` 2.结构体指针访问字段的标准方式应该是: *(*结构体).字段名 `(*person).Name = "tomcat"` 3.go做了一个简化,也支持结构体指针.字段,比如`person.Name = "tomcat"`。go底层编辑器对person.name做了转化,所以可以直接使用 ### 10.1.3 struct类型的内存分配机制 **变量总是存在内存中的** 结构体变量在内存中存在方式见下图 ``` package main import ( "fmt" ) type lei struct { Name string Age int } func main() { var p1 lei p1.Age = 10 p1.Name = "abc" var p2 *lei = &p1 //将p1地址赋给p2 //输出P2结果 fmt.Println((*p2).Age) fmt.Println(p2.Age) //修改p2内容 p2.Name = "tom" //打印输出结果 fmt.Printf("p2.Name=%v p1.Name=%v \n", p2.Name, p1.Name) fmt.Printf("p2.Name=%v p1.Name=%v \n", (*p2).Name, p1.Name) //这里只是表示(*p2).Name和p2.Name作用相同 } //输出结果 ➜ 04 go run main.go 10 10 p2.Name=tom p1.Name=tom p2.Name=tom p1.Name=tom ``` 上述代码在内存中的布局图 ![1628064683541.png](https://img.ukx.cn/abcdocker/2021/08/04/e69a905442fce/e69a905442fce.png) ### 10.1.4 结构体所有字段在内存中是连续分布的 ![1628496490675.png](https://img.ukx.cn/abcdocker/2021/08/09/a5b1b470a6b08/a5b1b470a6b08.png) **连续分布的优点:** 在取值的时候通过内存的地址进行加减取值,速度会比较快 ### 10.1.5 结构体是用户单独定义类型,和其他类型进行转换时需要有完全相同的字段 根据标题的提示,也就是它的名称、个数和类型需要完全一致 ``` package main import "fmt" type A struct { Num int } type B struct { Num float64 } func main() { //定义a 和b var a A var b B fmt.Println(a, b) //此时我们直接打印是没有问题的 //但是目前a和b的结构体数据类型不相同,我们不可以把a赋给b a = A(b) //此时编辑器提示类型不相同(强制转换也不行) //不只是类型不相同不可以转换,包括名字,数据类型,个数等都相同,才可以进行转换 } ``` ### 10.1.6 结构体进行type重新定义(相当于取别名),golang认为是新的数据类型,但是互相之间可以强转 ``` package main import ( "fmt" ) type Student struct { Name string Age int } type Stu struct { Name string Age int } func main() { var stu1 Student var stu2 Stu // stu2 = stu1 //错误 stu2 = Stu(stu1) fmt.Println(stu1, stu2) } //输出结果 ➜ 07 go run main.go { 0} { 0} ``` ### 10.1.7 结构体序列化 struct的每个字段上,可以写上一个tag,该tag可以通过`反射机制`获取,常见的场景就是**序列化**和**反序列化** **为什么需要序列化?** ![1629461378745.png](https://img.ukx.cn/abcdocker/2021/08/20/0f7fa3e6959f7/0f7fa3e6959f7.png) **案例演示:** ``` package main import ( "encoding/json" "fmt" //引用json反射 ) type Student struct { Name string `json:"name"` //因为是要通过别的包引入Student这个结构体,所以下面的变量必须要大写 Age int `json:"age"` //通过后面`json:" "`实现给结构体打tag标签 Like string `json:"like"` } func main() { //接下来给结构体进行赋值 Stu := Student{"abcdocker", 20, "english"} //接下来对Stu进行处理 jsonStr, err := json.Marshal(Stu) //调用json.Marshal方法来进行处理 //因为json.Marshal会返回2个值,一个是byte类型和err错误,我们这里加一个判断 if err != nil { fmt.Println("json错误", err) } fmt.Println("jsonStr", string(jsonStr)) //通过json.Marshal方法返回的是byte类型,所以我们需要通过string类型转换一下 } //输出结果 ➜ 07 go run main.go jsonStr {"name":"abcdocker","age":20,"like":"english"} 温馨提示: 如果我们在struct结构体中后面不添加tag标签,那么使用的时候会提示大写,但是我们又不能取消大写。如果取消了encoding包就调用不到了 ➜ 07 go run main.go jsonStr {"Name":"abcdocker","Age":20,"Like":"english"} //不添加tag输出结果 ``` ## 10.2 方法 Golang中的方法是**作用在指定的数据类型**上的。(和指定的数据类型绑定),因此**自定义类型**,都是可以有方法的,而不仅仅是struct ### 10.2.1 方法的调用和声明 **自定义方法语法说明** ``` type Person struct { Name string } func (a Person) test() { fmt.Println("test()", a.Name) } ``` 对上面语法的说明 - `func (a Person){}` 表示Person结构体有一方法,方法名为test - `(a Person)` 体现test方法是和Person绑定的 **Demo案例演示** ``` package main import ( "fmt" ) type Person struct { Name string } //给Person类型,绑定一个方法 func (a Person) test() { //a=变量名 Person=结构体 test方法的名字 fmt.Println("test()", a.Name) //输出名字 } func main() { //定义一个Person实例 var a Person a.Name = "i4t" a.test() //调用方法 } //A结构体有一个方法test ``` 总结说明 - test方法和Person类型绑定 - test方法只能通过Person类型的变量来调用,而不能直接调用,也不能使用其它类型的变量调用 - `func (a Person)test(){}` 其中**a**表示哪个Person变量调用,这个a就表示谁的副本;这点和函数传参非常相似 - `a`这个名字,是可以自己指定并且随意 ![1629703765980.png](https://img.ukx.cn/abcdocker/2021/08/23/f0977c066856b/f0977c066856b.png) ### 10.2.2 方法的快速入门 1) 给Person结构体添加speak方法,输出 xxxx 运维开发。 ``` package main import ( "fmt" ) type Speak struct { Name string } func (input Speak) speak() { fmt.Println(input.Name, "运维开发") } func main() { var a Speak fmt.Println("请输入名称") fmt.Scanln(&a.Name) a.speak() } //输出结果 ➜ 02 go run main.go 请输入名称 i4t i4t 运维开发 ``` 2) 给Person结构体添加jisuan方法,可以计算从1+..+1000的结果 ``` package main import ( "fmt" ) type Speak struct { Name string } func (input Speak) jisuan() { res := 0 for i := 1; i <= 1000; i++ { res += i } fmt.Println(input.Name, "res等于", res) } func main() { var a Speak a.jisuan() } //输出结果 ➜ 02 go run main.go res等于 500500 ``` 3) 给Person结构体添加jisuan2方法,该方法可以接收一个数n,计算从1+..+n的结果 ``` package main import ( "fmt" ) type Speak struct { Name string Num int } func (input Speak) jisuan2(n int) { //返回n参数 res := 0 for i := 1; i <= n; i++ { res += i } fmt.Println(input.Name, "res等于", res) } func main() { var a Speak a.Name = "i4t" a.jisuan2(2) //传入n } //输出结果 ➜ 02 go run main.go i4t res等于 3 ``` 4) 给Person结构体添加getSum方法,计算两个数的和,并返回结果 ``` package main import ( "fmt" ) type Speak struct { Name string Num int } func (input Speak) getSum(n1 int, n2 int) int { //多个返回值需要(int) return n1 + n2 //返回n1+n2的结果 } func main() { var a Speak res := a.getSum(10, 20) fmt.Println(res) } //输出结果 ➜ 02 go run main.go 30 ``` ### 10.2.3 方法的调用和传参机制原理 方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,也当做实参传递给方法。 ![1629720638280.png](https://img.ukx.cn/abcdocker/2021/08/23/9063d4f94009c/9063d4f94009c.png) **说明** - 在通过一个变量去调用方法时,其调用机制和函数一样。 - 不一样的地方是,调用方法该变量本身也会作为一个参数传递到方法中 (如果是值类型那么是值拷贝,如果是引用类型则是地址拷贝) **案例演示** 1.声明一个结构体Circle,字段为radius 2.声明一个方法area和Circlee绑定,可以返回面积 ``` package main import ( "fmt" ) type Circle struct { radius float64 } func (c Circle) area() float64 { return 3.14 * c.radius * c.radius } func main() { var c Circle c.radius = 4 res := c.area() fmt.Println(res) } //输出结果 ➜ 03 go run main.go 50.24 ``` ![1629960582016.png](https://img.ukx.cn/abcdocker/2021/08/26/6b120aa9d00dc/6b120aa9d00dc.png) ### 10.2.4 方法的声明和定义 ``` func (recevier type) methoName (参数列表) (返回值列表) { 方法体 return 返回值 } ``` - 参数列表: 表示方法输入 - recervier type: 表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型 - receiver type: type可以是结构体,也可以其它的自定义类型 - receiver: 就是type类型的一个变量(实例),比如: Person结构体的变量(实例) - 返回值列表: 表示返回的值,可以多个 - 方法主体: 表示为了实现某一个功能代码块(计算、写数据库等) - return语句不是必须的 ### 10.2.5 方法注意事项 **1.结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式** **2.修改结构体变量,可以通过结构体指针的方式来处理** ![1630055385834.png](https://img.ukx.cn/abcdocker/2021/08/27/80f2d5a8d6b24/80f2d5a8d6b24.png) 值引用的逻辑图 ![1630055523603.png](https://img.ukx.cn/abcdocker/2021/08/27/0420fc7d7ae35/0420fc7d7ae35.png) **3.golang中的方法作用在指定的数据类型上** `golang的方法作用在指定的数据类型上`即:和`指定的数据类型绑定`。因此`自定义类型,都可以有方法`,而不仅仅是struct,比如int、float64等 ``` package main import ( "fmt" ) type interger int func (in interger) inp() { fmt.Println("i=", in) } func main() { var in interger in = 10 in.inp() } //输出结果 ➜ 04 go run main.go i= 10 ``` **4.方法的访问范围控制规则** 和函数一样,方法名首字母小写,只能在本地访问,方法首字母大写。可以在本包和其它包访问 (这里可以参考函数) **5.如果一个类型实现了String()这个方法,那么`fmt.Println`默认会调用这个变量的String()进行输出** >比如我们输出结构体日志,就可以通过这种方法进行输出 ``` package main import ( "fmt" ) type Abc struct { Name string Age int Like string } func (a *Abc) String() string { //指针绑定,绑定到Abc struct //给*Abc实现方法String() str := fmt.Sprintf("Nmae=[%v] Age=[%v] Like=[%v]", a.Name, a.Age, a.Like) //将结果返回 return str } func main() { //定义变量 stu := Abc{ Name: "tomcat", Age: 18, Like: "eat 老冰棍", } fmt.Println(&stu) //这里需要传入地址,如果不添加&,会将结果输出。就不会调用我们的String方法,所以我们需要使用&获取地址 //如果你实现了 *Abc 类型的String方法,就会自动调用 fmt.Println(stu) //如果没有实现会直接输出结果 } //输出结果 ➜ 07 go run main.go Nmae=[tomcat] Age=[18] Like=[eat 老冰棍] {tomcat 18 eat 老冰棍} ``` ### 10.2.6 方法和函数的区别 - 调用方式不一样 函数的调用方式: 函数名(实参列表) 方法的调用方式: 变量.方法名(实参列表) - 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然 - 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样可以 ### 10.2.7 方法练习题 **第一题、** 编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个`10*7`的矩形,在main方法中调用该方法 ``` package main import ( "fmt" ) //定义字段,也可以没有字段 type MethodUtils struct { } //编写方法 func (Me MethodUtils) pri() { for i := 1; i <= 10; i++ { //打印10行,相当于最外层的for循环 for j := 1; j <= 8; j++ { fmt.Print("*") } fmt.Println() } } func main() { //在main函数中调用 var m MethodUtils m.pri() //调用 } //输出结果 ➜ 08 go run main.go ******** ******** ******** ******** ******** ******** ******** ******** ******** ******** ``` **第二题、** 编写一个方法,提供m和n俩个参数,方法中打印一个`m*n`的矩形 ``` package main import ( "fmt" ) type Mn struct { } func (mn Mn) printmn(m int, n int) { for i := 1; i <= m; i++ { //打印10行,相当于最外层的for循环 for j := 1; j <= n; j++ { fmt.Print("*") } fmt.Println() } } func main() { var M Mn //在方法中传参 M.printmn(2, 4) } //输出结果 ➜ 09 go run main.go **** **** ``` **第三题、** 编写一个方法算出该矩形的面积(可以接收长len,和宽width),将其作为方法返回值,在main方法中调用,接收返回的面积值并打印 ``` package main import ( "fmt" ) type MethodUtils struct { } func (m MethodUtils) sum(len float64, width float64) float64 { //sum后面为传入数据,最后一个float64为返回,可以写多个 return len * width } func main() { var m MethodUtils areaRes := m.sum(2.5, 8.7) fmt.Println("结果", areaRes) } //输出结果 ➜ 10 go run main.go 结果 21.75 ``` **第四题、** 编写方法: 判断一个数是奇数还是偶数 ``` package main import ( "fmt" ) type MethodUtils struct { } func (M *MethodUtils) Jnum(num int) { if num%2 == 0 { fmt.Printf("数字%v 为偶数\n", num) } else { fmt.Printf("数字%v 为奇数\n", num) } } func main() { var M MethodUtils M.Jnum(103) } //输出结果 ➜ 11 go run main.go 数字103 为奇数 ``` **第五题、** 根据行、列、字符打印对应的行数和列数的字符,比如: 行:3,列:2,字符`*`,则打印相应的结果 ``` package main import ( "fmt" ) type Num struct { } func (N *Num) Sum(n int, m int, key string) { for i := 1; i <= n; i++ { for j := 1; j <= m; j++ { fmt.Print(key) } fmt.Println() } } func main() { var num Num num.Sum(7, 7, "=") //输入需要打印的行和列以及打印出的符号 } //输出结果 ➜ 12 go run main.go ======= ======= ======= ======= ======= ======= ======= ``` **第六题、** 定义小小计算器结构体`Calcuator`,实现加减乘除四个功能 实现形式1: 分四个方法完成: 实现形式2: 用一个方法完成: 第一种方式: 通过4个方法完成 ``` package main import ( "fmt" ) type Calcuator struct { Num1 float64 Num2 float64 } func (calcuator *Calcuator) getsum() float64 { //加号 return calcuator.Num1 + calcuator.Num2 } func (calcuator *Calcuator) getsub() float64 { //减号 return calcuator.Num1 - calcuator.Num2 } func main() { var calcuator Calcuator calcuator.Num1 = 1.1 calcuator.Num2 = 1.2 //Sprintf 保留小数点2位 fmt.Println(calcuator.getsum()) fmt.Println(fmt.Sprintf("%.2f", calcuator.getsub())) } //输出结果 ➜ 13 go run main.go 2.3 -0.10 ``` 还有另外一种方式,将所有的加减乘除放在一个方法中 ``` package main import ( "fmt" ) type Calcuator struct { Num1 float64 Num2 float64 } func (calcuator *Calcuator) getsum(Operator byte) float64 { res := 0.0 switch Operator { case '+': res = calcuator.Num1 + calcuator.Num2 case '-': res = calcuator.Num1 - calcuator.Num2 case '*': res = calcuator.Num1 * calcuator.Num2 case '/': res = calcuator.Num1 / calcuator.Num2 default: fmt.Println("请输入[+ - * /]") } return res } func main() { var calcuator Calcuator calcuator.Num1 = 1.2 calcuator.Num2 = 1.1 fmt.Println(calcuator.getsum('+')) fmt.Println(calcuator.getsum('-')) fmt.Println(calcuator.getsum('*')) fmt.Println(calcuator.getsum('/')) } //输出结果 ➜ 14 go run main.go 2.3 0.09999999999999987 1.32 1.0909090909090908 ``` ## 10.3 面向对象编程应用 1) 声明(定义)结构体,确定结构体名 2) 编写结构体的字段 3) 编写结构体的方法 **学生案例** (1) 编写一个Student结构体,包含name、gender、age、id、score字段,分别为string、string、int、int、float64 (2) 结构体中声明一个say方法,返回string类型,方法返回信息中包含所有字段 (3) 在main方法中,创建Student结构体实例(变量),并访问say方法,并将结果调用输出 ``` package main import ( "fmt" ) type Student struct { name string gender string age int id int score float64 } func (student *Student) say() string { info := fmt.Sprintf("名称[%v] 性别[%v] 年龄[%v] ID[%v] 成绩[%v]", student.name, student.gender, student.age, student.id, student.score) //Sprintf根据format参数生成格式化的字符串并返回该字符串,参数需要和表达式的数据类型匹配 return info } func main() { var student = Student{ //创建一个变量来接受Student结构体值 name: "abc", gender: "男", age: 20, id: 1, score: 88.2, } say := student.say() fmt.Println(say) } //输出结果 ➜ 02 go run main.go 名称[abc] 性别[男] 年龄[20] ID[1] 成绩[88.2] ``` **长方形案例** 1) 创建一个Box结构体,在其中声明三个字段表示一个立方体的长、宽、高,长宽高要从终端获取 2) 声明一个方法获取立方体的体积 3) 创建一个Box结构体变量,打印给定尺寸的立方体的体积 ``` package main import ( "fmt" ) type Box struct { len float64 //长 wide float64 //宽 high float64 //高 } func (box *Box) get() float64 { return box.len * box.wide * box.high } func main() { var box = Box{ len: 1.1, wide: 2.0, high: 3.0, } res := box.get() fmt.Printf("体积为:%.2f \n", res) } //输出结果 ➜ 04 go run main.go 体积为:6.60 ``` **景区门票案例** * 景区门票根据游人的年龄收取不同价格的门票,比如大于18,收费20元。其它情况门票免费 * 请编写Visitor结构体,根据年龄段决定能够购买的门票价格并输出 ``` package main import ( "fmt" ) type Visitor struct { Name string Age int } func (mon *Visitor) sum() { if mon.Age > 18 { fmt.Printf("\n您的名称: %v\n 您的年龄: %v\n 门票售价: 20", mon.Name, mon.Age) fmt.Println() } else { fmt.Printf("\n 您的名称: %v\n 您的年龄: %v\n 门票价格: 免费", mon.Name, mon.Age) fmt.Println() } } func main() { var mon Visitor for { fmt.Println("请输入您的姓名") fmt.Scanln(&mon.Name) if mon.Name == "n" { fmt.Println("退出成功!") fmt.Println() break } fmt.Println("请输入您的年龄") fmt.Scanln(&mon.Age) mon.sum() } } //输出结果 ➜ 05 go run main.go 请输入您的姓名 999 请输入您的年龄 8 您的名称: 999 您的年龄: 8 门票价格: 免费 请输入您的姓名 n 退出成功! ``` ## 10.4 创建结构体遍历时指定字段值 Golang在创建结构体变量(实例)时,可以直接指定字段的值 **创建结构体变量时指定字段值有以下方式** - 方式一 ``` var stu Student = student{"abc",10} //另外一种 stu := Student{"abc",10} //更简介的方式 var stu Student = Student{ Name: "abc", Age: 10, } //或 stu := Student{ Name: "abc", Age: 20, } ``` - 方式二 ``` var stu *Student = &student{"abc",10} var stu *Student = &student{ Name: "abc", Age: 20, } ``` - **方式三,返回结构体指针类型** ``` package main import ( "fmt" ) type Sum struct { //声明结构体 Name string Age int } func main() { //使用结构体 var Stu = &Sum{"abc", 20} fmt.Println(Stu) //第二种方式,类型推导 Stu1 := &Sum{"dddd", 20} fmt.Println(Stu1) //第三种方式,也可以在创建变量时将结构体的字段名和字段值写在一起 Stu2 := &Sum{ Name: "ttt", Age: 20, } fmt.Println(Stu2) //默认输出会带一个&符,可以通过取值符号来进行过滤 fmt.Println(*Stu2) } //输出结果 ➜ 06 go run main.go &{abc 20} &{dddd 20} &{ttt 20} {ttt 20} ``` ## 10.5 工厂模式 在Golang的结构体没有**`构造函数`**,通常可以使用工厂模式来解决这个问题。 **需求: 为什么需要使用工厂模式** 例如我们在`module`包中,需要引用Student结构体,并且需要结构体名称首字母小写。那我们直接引用module包就不可以,这时候就需要使用工厂模式来解决问题 ``` //main包 package main import ( "fmt" "go_code/factory/model" ) func main() { var student = model.NewStudent("小明", 20) //此时我们调用返回的也是指针类型 fmt.Println(student) //如果我们不想要指针类型的&符,可以通过*来获取 fmt.Println(*student) } //model包 package model //定义一个结构体 type student struct { Name string Age int } //创建一个方法,我们调用方法来使用结构体 func NewStudent(n string,a int) *student{ //传入2个参数,并且返回一个结构体 return &student{ Name: n, Age : a, } } //输出结果 ➜ main go run main.go &{小明 20} {小明 20} //当我们需要取结构体的某个字段,也可以通过下面的写法 fmt.Println("name=", student.Name) ``` ![1631524437916.png](https://img.ukx.cn/abcdocker/2021/09/13/a590fb3ab6a06/a590fb3ab6a06.png) **如果我们结构体字段首字母也是小写,可以通过下面的方式解决** ``` //main包 package main import ( "fmt" "go_code/factory/model" ) func main() { var student = model.NewStudent("小明", 20) //此时我们调用返回的也是指针类型 fmt.Println(student) //如果我们不想要指针类型的&符,可以通过*来获取 fmt.Println(*student) //解决结构体字段小写报错 fmt.Println("age=", student.AgeSum()) //这里还是指针类型,通过指针去调用一个方法,来获取age字段值 } //model package model //定义一个结构体 type student struct { Name string age int } //创建一个方法,我们调用方法来使用结构体 func NewStudent(n string, a int) *student { //传入2个参数,并且返回一个结构体 return &student{ Name: n, age: a, } } //添加解决结构体小写问题 func (age *student) AgeSum() int { //返回一个int类型 return age.age } //输出结果 ➜ main go run main.go &{小明 20} {小明 20} age= 20 ``` ## 10.6 面向对象编程思想-抽象 在结构体时,就是将一类事务所有的属性(字段)和行为(方法)提取出来,行程一个物理模型(结构体)。这种行为叫做抽象 ![1631700834325.png](https://img.ukx.cn/abcdocker/2021/09/15/fc5cc22af4a27/fc5cc22af4a27.png) 案例演示 ``` package main import ( "fmt" ) //定义结构体 type Account struct { UserName string UserPass int UserBalance float64 Pwd int Money float64 } //方法 //1.存款 func (account *Account) query() { if account.UserBalance < 0 { fmt.Println("余额输入错误,请从新输出") } account.UserBalance += account.Money fmt.Printf("存款成功,存款%v元\n", account.Money) } //2.取款 func (account *Account) qukuan() { if account.UserBalance < 0 || account.Money > account.UserBalance { fmt.Printf("取款输入错误,已退出\n") return } account.UserBalance -= account.Money fmt.Printf("取款成功,取款%v元\n", account.Money) } //3.查询 func (account *Account) chaxun(user string) { fmt.Printf("用户名:%v 余额剩余: %v\n", account.UserName, account.UserBalance) } func main() { var account Account var xx int //默认密码 account.Pwd = 123456 for { fmt.Println("请输入用户名") fmt.Scanln(&account.UserName) fmt.Println("请输入密码") fmt.Scanln(&account.UserPass) if account.UserPass != account.Pwd { fmt.Printf("密码输入错误,请检查密码\n") return } else { fmt.Printf("登陆成功! 用户名:%v \n", account.UserName) } for { fmt.Println("请输入选项 [1]查询 [2]存款 [3]取款 [0]退出") fmt.Scanln(&xx) switch xx { case 1: account.chaxun(account.UserName) case 2: fmt.Printf("请输入存款金额") fmt.Scanln(&account.Money) account.query() case 3: fmt.Printf("请输入取款金额") fmt.Scanln(&account.Money) account.qukuan() case 0: fmt.Printf("账号%v 退出成功!\n", account.UserName) return default: fmt.Println("请从新输入,输入[1] [2] [3] [0]") } } } } ``` 输出结果 ![1631709085714.png](https://img.ukx.cn/abcdocker/2021/09/15/343a4d48820ae/343a4d48820ae.png) ## 10.7 面向对象三大特征-封装 封装(encapsulation)就是把抽象出的**字段和对字段的操作**封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。 **封装的理解和好处** - 隐藏实现细节 - 可以对数据进行验证,保证数据合理 **如何体现封装** - 对结构体中的属性进行封装 - 通过方法,包 实现封装 **封装的实现步骤** 1) 将结构体、字段(属性)的**首字母小写**(小写后不支持导出,其它包不可以直接调用) 2) 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数 3) 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值 ``` func (var 结构体类型名) Set xx (参数列表) (返回值列表){ //加入数据验证的业务逻辑 var.字段 = 参数 } ``` 4) 提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值 ``` func (var 结构体类型名) Get xxx(){ return var.age; } ``` 特别说明: 在Golang开发中并没有特别强调封装,Golang本身对面向对象的特性做了简化 >封装案例: 编写一个person.go,不能随便查看的人年龄,工资等隐私,并对输入的年龄进行合理的验证 ## 10.8 面向对象三大特征-继承 ## 10.9 接口 (INTERFACE) ## 10.10 面向对象-多态 ## 10.11 类型断言 # 十一、项目一 家庭收支记账软件 # 十二、项目二 客户关系系统
目录
目录