Golang 基础知识笔记

Golang Basic Knowledge Notes

Posted by BlueFat on Saturday, May 6, 2023

一、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

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. 变量表示内存中的一个存储区域
  2. 该区域有自己的名称 (变量名)和类型(数据类型)

示意图: image.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 := "sundayhk"
        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 = "sundayhk"

func main() {
    fmt.Println("n1=",n1,"n2=",n2,"name=",name)
}

结果如下:
n1= 100 n2= 200 name= sundayhk

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. 当左右两边都是数值型时,择做加法运算
  2. 当左右两边都是字符串,则做字符串拼接
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 = "sundayhk"
    var res = str1 + str2 //做拼接操作
    fmt.Println("res=", res)
}

image.png

二、数据类型基本介绍

image.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)
}

字符类型本质探讨

  • 字符型存储到计算机中,需要将字符对应的码值(整数)找出来 存储: 字符—>对应码值—>二进制—>存储 读取: 二进制—>码值—>字符—>读取
  • 字符和码值的对应关系是通过字符编码表决定的
  • 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

  1. 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值
var ptr *int = &num

image.png

  1. 获取指针类型所指向的值,使用: *
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

案例演示

  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

2.11 标示符的命名规则

  1. 由26个英文字母大小写,0-9,_组成
  2. 数据不可以开头。var 3num int
  3. Golang中严格区分大小写
  4. 标示符不能包含空格
  5. 下划线_本身在Go中是一个特殊的标示符,称为空标示符。可以代表任何其他的标示符,但是它对应的值会被忽略。所以仅能被作为占位符使用,不能作为标示符
  6. 不能以系统保留关键字作为标示符(一共有25个),比如ifbreakfor

三、运算符

运算符是一种特殊的符号,用以表示数据的运算、复制和比较

运算符有以下分类

  • 算术运算符
  • 赋值运算符
  • 比较运算符/关系运算符
  • 逻辑运算符
  • 位运算符
  • 其它运算符

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” + “sundayhk” “hello sundayhk”

案例演示 //演示/的使用特点

//演示/的使用特点
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
  2. 关系表达式 通常在 if结构的条件中或循环结构的条件中

image.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

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)
}

//输入内容
请输入姓名 
sundayhk
请输入年龄
20
请输入薪水
10000
请输入是否通过考试[true & false]
true

//输出内容
您的名字是:sundayhk
 您的年龄是: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
请输入您的姓名,年龄,薪水,是否通过考试,请使用空格隔开
sundayhk 19 10000 false

//输出内容
名字是sundayhk 
年龄是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 位运算

五、程序流程控制

在程序中,程序运行的流程控制决定程序是如何执行的,主要有三大流程控制语句

  • 顺序控制
  • 分支控制
  • 循环控制

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

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

双分支控制

基本语法

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:
        语句块
    }
  • switch的执行流程是,先执行表达式,得到值,然后和case的表达式进行比较,如果相等,就匹配,然后执行对应case的语句块,最后退出switch控制
  • 如果switch的表达式的值没有和任何的case的表达式匹配成功,则执行default的语句块。
  • golang的case后的表达式可以有多个,使用逗号间隔
  • 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语句,简洁高效
  2. 其他情况,对区间判断和结果为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("sundayhk")
  • 执行循环变量迭代,i++可以解析为(i = i +1)
  • 反复执行 2,3,4 步骤,执行循环为Fakse,就退出循环

image.png

for循环使用的注意事项

1)循环条件是返回一个布尔值的表达式 2)for循环的第三种使用方式

    for 循环判断条件{
        //循环执行语句
    }
    
//将变量初始化和变量迭代写到其他位置

案例演示

package main
import "fmt"

func main() {
    j := 1 //循环变量初始化
    for j <= 10 {
        fmt.Println("您好,sundayhk", j)
        j++
    }
}

//输出结果
01 go run main.go 
您好,sundayhk 1
您好,sundayhk 2
您好,sundayhk 3
您好,sundayhk 4
您好,sundayhk 5
您好,sundayhk 6
您好,sundayhk 7
您好,sundayhk 8
您好,sundayhk 9
您好,sundayhk 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("欢迎登陆sundayhk运维系统")
    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 
欢迎登陆sundayhk运维系统
请输入登陆用户名
abc
请输入登陆密码
abc
你还有2次登陆机会 
请输入登陆用户名
张无忌
请输入登陆密码
888
恭喜您登陆成功!

5.8 跳转控制语句-CONTINUE

continue语句用于结束本次循环,继续执行下一次循环。 continue语句出现在多层嵌套的循环语句体重,可以通过标签指明要跳转的那一层循环

基本语法

{   ...
    continue;
    ...
}   

image.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

案例演示

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("sundayhk", i)
    }
    fmt.Println("hello word")
}

说明

  1. 如果return是在普通的函数,则表示跳出该函数,即不在执行函数中的return后面代码,也可以理解成终止函数
  2. 如果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.sundayhk) //utils为包名 sundayhk为变量名,变量需要大写
    // 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

我们将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

  • 当一个文件要使用其它函数或变量时,需要先引入对应的包 1)引入方式1: import "包名" 2)引入方式2:

    import ("包名")
    
  • package指令在文件第一行,然后是import指令

  • 在import包时,路径从$GOPATH的src下开始,不需要带src,编译器会自动从src下开始引入

  • 为了其它包文件可以访问到本地的函数,则该函数名的首字母需要大写 image.png

  • 同样我们如果要定义一个变量名,也是相同的使用方式 image.png

  • 在访问其它包函数或变量时,其语法是包名.函数名 image.png

  • 如果包名较长,GO支持给包取别名,注意: 取别名后,原来的包名就不能使用了 演示 image.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 sundayhk main.go
➜  main ls
sundayhk main.go
➜  main ./sundayhk
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

下面是在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

在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

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

案例演示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 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

递归函数练习题3

有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个。以后每天猴子都吃其中的一半,然后再多吃一个。当到第十天时,想再吃时(还没吃),发现只有一个桃子,请问最初一共有多少个桃子

分析: 1.第十天只有一个桃子了 2.第九天桃子= (第十天桃子数量 + 1) 2 3.规律: 第n天的桃子数量 peach(n) =(peach(n+1)+1) x 2

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 

6.11 函数注意事项

  1. 函数的形参列表可以是多个,返回值列表也可以是多个
  2. 形参列表和返回值列表的数据类型可以是值和引用类型
  3. 函数的命名遵循标示符命名规范,首字母不能是数字,首字母大写该函数可以被本包或者其他包文件使用,首字母小写,只能被本包文件使用,其他包文件不能使用
  4. 函数中的变量是局部的,函数外不生效
  5. 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响原来的值。例如main函数中的n1和test函数中的n1修改test函数中的n1不会影响main函数中的n1
  6. 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内指针的方式操作变量。从效果上看类似引用
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) intgetSum类型是func(int, int) int
res= 30

取值会直接修改num的变量,所以*n1 = *n1+10 可以理解为num == 20 +10 (*n1是取值,而不是复制的关系) 7) GO函数不支持传统的函数重载

  1. 在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

分析: 在函数中可以将函数给交一个变量,并且可以通过变量进行完成调用

  1. 函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用
package mainimport "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.gorest2= 110
  1. 为了简化数据类型定义,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
  1. 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
  1. 使用_占位符,来忽略返回值

image.png

  1. 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

值类型默认是值传递: 变量直接存储值,内存通常在栈中分配

image.png

引用类型默认是引用地址: 变量存储的是一个地址,这个地址对应的空间才正常存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就称为一个垃圾,由GC来回收

image.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

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= sundayhk

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 (3) 可以理解为: 闭包是累,函数是操作,n是字段。函数和它使用到的n构成闭包 (4) 当我们反复的调用f函数时,因为n是只初始化一次,因此每调用一次就进行累计 (5) 函数和它引用到的变量共同构成闭包

闭包实际上是返回的匿名函数,和用到的函数外的变量。它们共同构成一个闭包,而调用的时候它用到的变量不是每一次都会被初始化;每用到一次就会进行叠加一次

image.png

6.16.1 闭包实践

请编写一个程序,具体要求如下

  • 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包
  • 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg)则返回 文件名.jog,如果已经有.jpg后缀,则返回原文件名
  • 要求使用闭包的方式完成
  • 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("sundayhk.mp4"))
}

//输出结果
➜  exec4 go run main.go
文件名处理后= abc.jpg
文件名处理后= abc.jpg
文件名处理后= sundayhk.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 go

6.18 变量作用域

  • 函数内部定义/声明的变量叫局部变量,作用域仅限于函数内部
func test(){
    //abc的作用域只在test函数内部
    abc := 10
}
  • 函数外部定义/声明的变量叫做全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
package main
import (
    "fmt"
)

//全局变量
var abc int = 60
var Name string = "sundayhk"
//全局变量不支持类型推导

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= sundayhk
  • 如果变量是在一个代码块,比如iffor中,那么这个变量的作用于就在该代码块中
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为 nillen(v)即为零

案例演示

func main() {
    //golang中的编码同意为utf-8
    //ascii的字符 (字母和数字)占一个字节,中文汉字占3个字节 (注意是字节不是字符)
    str := "你好,sundayhk"             //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 := "你好,sundayhk"
    //循环输出每一行内容
    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 := "你好,sundayhk"
    //将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

[]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("sundayhk_aaa", "a") //查找sundayhk_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

package main
import (
    "fmt"
    "strings" //需要引入包
)

func main() {
    //正确
    index := strings.Index("sundayhk", "er")
    fmt.Printf("index =%v \n", index)
    //错误
    index = strings.Index("sundayhk", "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(" sundayhk    ") //前面有空格,后面也有空格
    //处理
    fmt.Printf("str=%q\n", str)
}

//输出结果
01 go run main.go
str="sundayhk"
//-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://sundayhk.com","ftp") //如果是以ftp开头返回true,否则false
    fmt.Printf("str HasPrefix=%v \n",str)
    str = strings.HasSuffix("ftp://sundayhk.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就退出

const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)

每隔0.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

获取当前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为 nillen(v)即为零

案例演示

func main() {
    //golang中的编码同意为utf-8
    //ascii的字符 (字母和数字)占一个字节,中文汉字占3个字节 (注意是字节不是字符)
    str := "你好,sundayhk"             //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

6.22.3 make

用来分配内存,主要用来分配引用类型,比如chan、map、slice

6.23 错误处理

  1. go语言追求简洁优雅,所以go语言不支持传统的try…catch…finally这样处理
  2. go中引入的处理方式为: deferpanicrecover
  3. go中可以抛出一个panic的异常,然后在defer中通过reconver捕获这个异常,进行正常处理

6.23.1 使用defer+reconver来处理error错误

案例: 下面的代码出现异常,10无法除0所以提示下面的报错。 并且main函数打印操作没有输出 image.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.Newpanic内置函数

  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

这里返回的类型都是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

  • 数组的地址可以通过数组名来获取 &intArr
  • 数组的第一个元素地址,就是数组的首地址
  • 数组的各个元素的地址间隔是依据数组的类型决定,例如int64为8个字节,int32位4个字节,以此类推

image.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个,这里在添加会报错
  1. 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用
  2. 数组创建后,如果没有赋值,默认值如下
数值类型数组(整数、浮点数):   默认值是0
字符串数组:      默认值是""
bool数组:         默认值是false
  1. 数组的使用步骤
  • 声明数组并开辟空间
  • 数组各个元素赋值(不赋值默认零值)
  • 使用数组
  1. 数组的下标是从0开始

  2. 数组下标必须在指定范围内使用,否则报panic错误:数组越界

例如: var arr [5]int 则有效下标为0-4
  1. Go的数组属于值类型,在默认情况下是值传递。因此会进行值拷贝。数组间不会互相影响

image.png

在go中,数组的类型是数组长度的一部分 (可以理解[4]int 和 [5]int不是同一个类型)

8) 如果想在其它函数中修改原来的数组,可以使用引用传递(指针方式);效率高,速度快

image.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
  1. 在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

总结:

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

针对于第一种和第二种使用方式的说明

  • 通过make方式创建切片可以指定切片的大小和容量
  • 如果没有给切片的各个元素赋值,那么就会使用默认值[int,float=0 string = "” bool = false]
  • 通过make 方式创建的切片对应的数组是由make底层维护,对外不可见,即只能通过slice去访问各个元素。

● 第三种 定义一个切片,直接就指定具体数组,使用原理类似make的方式

package main
import "fmt"

func main() {
    var slice []string = []string{"abc", "ukx", "sundayhk"}
    fmt.Println("slice=", slice)
    fmt.Println("slice len=", len(slice))
    fmt.Println("slice cap=", cap(slice))
}

//输出结果
06 go run main.go 
slice= [abc ukx sundayhk]
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@sundayhk"
    //使用切片进行获取sundayhk
    slice := str[6:]
    fmt.Println("slice=", slice)
}

//输出结果
06 go run main.go
slice= sundayhk
  • string和切片内存示意图

image.png

  • string是不可变的,也就是不能通过str[0] = 'z'方式来修改字符串

案例如下:

image.png

  • 如果需要修改字符串,可以先将string –> []byte(切片) / 或者 [] runne (切片) ->修改->重写转成string

image.png

byte类型是无法处理中文,英文是占用1个字节,中文是占用3个字节。 所以如果使用byte处理中文会出现乱码的情况

解决方法: 将string转成 []rune即可,因为[]rune是按字符处理,兼容汉字

package main
import "fmt"

func main() {
    //string底层是一个byte数组,因此string也可以进行切片处理
    str := "hello@sundayhk"
    //使用切片进行获取sundayhk
    slice := str[6:] //获取第六个到最后一个
    fmt.Println("slice=", slice)
    // 将hello@sundayhk修改为iello@sundayhk
    //将str修改为byte类型的数组
    arr1 := []rune(str)
    //替换字符
    arr1[0] = '你'
    //将类型转换回string
    str = string(arr1)
    fmt.Println("str=", str)
}

//输出结果
06 go run main.go
slice= sundayhk
str= 你ello@sundayhk

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

冒泡排序规则

  • 一共会经过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

二分查找代码:

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

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(管道),还可以是只包含前面几个类型的结构体、接口、数组等。通常为intstring

注意: slice,map还有function不可以,因为这几个没法用==来判断

value可以是什么类型

valuetype的类型和key基本上一样,通常为:数字(整数,浮点数)stringmapstrcuct

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

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"] = "sundayhk" //如果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["sundayhk"] = make(map[string]string) //因为value是map,所以这里直接写密码是要make的
    users["sundayhk"]["passwd"] = "111111"
    users["sundayhk"]["nikename"] = "运维啊"
    //传入users和name到modifyUser函数中
    modifyUser(users, "docker")
    modifyUser(users, "test")
    modifyUser(users, "sundayhk")
    
    //打印结果
    fmt.Println(users)
}

1626171499838.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

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 = "sundayhk"
    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={sundayhk 15}
b={docker 20}
a={sundayhk 15}

b := a 在内存中的变化

1626419822344.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{"sundayhk", 20, "computer"}
    fmt.Println(v1)
}

//输出结果
03 go run main.go 
{abc 18 eat}
{sundayhk 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 = "sundayhk"
    person.Age = 19
    person.Like = "play"
    fmt.Println(*person)
}

//输出结果
03 go run main.go
{abc 18 eat}
{sundayhk 19 play}
  • 方式四 (-&)

1. 方式三和方式四返回的是结构体 **2. 结构体指针访问字段的标准方式应该是: (*结构体指针).字段名 **例如:(*person).Name = “docker”3. 但go做了一个简化,也支持结构体指针.字段名,比如person.Name = “sundayhk”。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

10.1.4 结构体所有字段在内存中是连续分布的

1628496490675.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

案例演示:

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{"sundayhk", 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":"sundayhk","age":20,"like":"english"}
温馨提示: 如果我们在struct结构体中后面不添加tag标签,那么使用的时候会提示大写,但是我们又不能取消大写。如果取消了encoding包就调用不到了
07 go run main.go
jsonStr {"Name":"sundayhk","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 = "sundayhk"
    a.test() //调用方法
}
//A结构体有一个方法test

总结说明

  • test方法和Person类型绑定
  • test方法只能通过Person类型的变量来调用,而不能直接调用,也不能使用其它类型的变量调用
  • func (a Person)test(){} 其中a表示哪个Person变量调用,这个a就表示谁的副本;这点和函数传参非常相似
  • a这个名字,是可以自己指定并且随意 1629703765980.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
请输入名称
sundayhk
sundayhk 运维开发
  1. 给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()
}
  1. 给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 = "sundayhk"
    a.jisuan2(2)        //传入n
}

//输出结果
02 go run main.go
sundayhk res等于 3
  1. 给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

说明

  • 在通过一个变量去调用方法时,其调用机制和函数一样。
  • 不一样的地方是,调用方法该变量本身也会作为一个参数传递到方法中 (如果是值类型那么是值拷贝,如果是引用类型则是地址拷贝)

案例演示

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

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

值引用的逻辑图 1630055523603.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

如果我们结构体字段首字母也是小写,可以通过下面的方式解决

//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

案例演示

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

10.7 面向对象三大特征-封装

封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。

封装的理解和好处

  • 隐藏实现细节
  • 可以对数据进行验证,保证数据合理

如何体现封装

  • 对结构体中的属性进行封装
  • 通过方法,包 实现封装

封装的实现步骤

  1. 将结构体、字段(属性)的首字母小写(小写后不支持导出,其它包不可以直接调用)
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
  3. 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
func (var 结构体类型名) Set xx (参数列表) (返回值列表){
        //加入数据验证的业务逻辑
        var.字段 = 参数
}
  1. 提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值
func (var 结构体类型名) Get xxx(){
    return var.age;
}

特别说明: 在Golang开发中并没有特别强调封装,Golang本身对面向对象的特性做了简化

封装案例: 编写一个person.go,不能随便查看的人年龄,工资等隐私,并对输入的年龄进行合理的验证

10.8 面向对象三大特征-继承

10.9 接口 (INTERFACE)

10.10 面向对象-多态

10.11 类型断言

十一、项目一 家庭收支记账软件

十二、项目二 客户关系系统