Golang学习笔记

Golang语言简介

图标上的动物

这个动物叫做金花鼠,英文名称为gordon

Go语言的由来

目的: 用更少的代码,更短的编译时间,创建运行更快的程序,享受更多的乐趣。

Go 语言起源 2007 年,并于 2009 年正式对外发布。它从 2009 年 9 月 21 日开始作为谷歌公司 20% 兼职项目,即相关员工利用 20% 的空余时间来参与 Go 语言的研发工作。该项目的三位领导者均是著名的 IT 工程师:Robert Griesemer,参与开发 Java HotSpot 虚拟机;Rob Pike,Go 语言项目总负责人,贝尔实验室 Unix 团队成员,参与的项目包括 Plan 9,Inferno 操作系统和 Limbo 编程语言;Ken Thompson,贝尔实验室 Unix 团队成员,C 语言、Unix 和 Plan 9 的创始人之一,与 Rob Pike 共同开发了 UTF-8 字符集规范。自 2008 年 1 月起,Ken Thompson 就开始研发一款以 C 语言为目标结果的编译器来拓展 Go 语言的设计思想。

这是一个由计算机领域 “发明之父” 所组成的黄金团队,他们对系统编程语言,操作系统和并行都有着非常深刻的见解

在 2008 年年中,Go 语言的设计工作接近尾声,一些员工开始以全职工作状态投入到这个项目的编译器和运行实现上。Ian Lance Taylor 也加入到了开发团队中,并于 2008 年 5 月创建了一个 gcc 前端。

Russ Cox 加入开发团队后着手语言和类库方面的开发,也就是 Go 语言的标准包。在 2009 年 10 月 30 日,Rob Pike 以 Google Techtalk 的形式第一次向人们宣告了 Go 语言的存在。

直到 2009 年 11 月 10 日,开发团队将 Go 语言项目以 BSD-style 授权(完全开源)正式公布了 Linux 和 Mac OS X 平台上的版本。Hector Chu 于同年 11 月 22 日公布了 Windows 版本。

作为一个开源项目,Go 语言借助开源社区的有生力量达到快速地发展,并吸引更多的开发者来使用并改善它。自该开源项目发布以来,超过 200 名非谷歌员工的贡献者对 Go 语言核心部分提交了超过 1000 个修改建议。在过去的 18 个月里,又有 150 开发者贡献了新的核心代码。这俨然形成了世界上最大的开源团队,并使该项目跻身 Ohloh 前 2% 的行列。大约在 2011 年 4 月 10 日,谷歌开始抽调员工进入全职开发 Go 语言项目。开源化的语言显然能够让更多的开发者参与其中并加速它的发展速度。Andrew Gerrand 在 2010 年加入到开发团队中成为共同开发者与支持者。

在 Go 语言在 2010 年 1 月 8 日被 Tiobe(闻名于它的编程语言流行程度排名)宣布为 “2009 年年度语言” 后,引起各界很大的反响。目前 Go 语言在这项排名中的最高记录是在 2017 年 1 月创下的第13名,流行程度 2.325%。

  • 时间轴:
  • 2007 年 9 月 21 日:雏形设计
  • 2009 年 11 月 10日:首次公开发布
  • 2010 年 1 月 8 日:当选 2009 年年度语言
  • 2010 年 5 月:谷歌投入使用
  • 2011 年 5 月 5 日:Google App Engine 支持 Go 语言

从 2010 年 5 月起,谷歌开始将 Go 语言投入到后端基础设施的实际开发中,例如开发用于管理后端复杂环境的项目。有句话叫 “吃你自己的狗食”,这也体现了谷歌确实想要投资这门语言,并认为它是有生产价值的。

Go 语言的官方网站是 golang.org,这个站点采用 Python 作为前端,并且使用 Go 语言自带的工具 godoc 运行在 Google App Engine 上来作为 Web 服务器提供文本内容。在官网的首页有一个功能叫做 Go Playground,是一个 Go 代码的简单编辑器的沙盒,它可以在没有安装 Go 语言的情况下在你的浏览器中编译并运行 Go,它提供了一些示例,其中包括国际惯例 “Hello, World!”。

Go语言的特点

  1. 从 C 语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型,调用参数传值,指针等 等,也保留了和 C 语言一样的编译执行方式及弱化的指针
    举一个案例(体验):

    1
    2
    3
    4
    //go 语言的指针的使用特点(体验) 
    func testPtr(num *int) {
    *num = 20
    }
  2. 引入包的概念,用于组织程序结构,Go 语言的一个文件都要归属于一个包,而不能单独存在。

1
package main	//一个go文件需要在一个包里
  1. 垃圾回收机制,内存自动回收,不需开发人员管理

  2. 天然并发 (重要特点)
    (1) 从语言层面支持并发,实现简单
    (2) goroutine,轻量级线程,可实现大并发处理,高效利用多核。
    (3) 基于 CPS 并发模型(Communicating Sequential Processes )实现

  3. 吸收了管道通信机制,形成 Go 语言特有的管道 channel 通过管道 channel , 可以实现不同的 goroute 之间的相互通信。

  4. 函数可以返回多个值。举例: //写一个函数,实现同时返回 和,差

    1
    2
    3
    4
    5
    6
    //go 函数支持返回多个值
    func getSumAndSub(n1 int, n2 int) (int, int ) {
    sum := n1 + n2 //go 语句后面不要带分号.
    sub := n1 - n2
    return sum , sub
    }
  5. 新的创新:比如切片 slice、延时执行 defer

Golang环境配置

Mac 下搭建 Go 开发环境-安装和配置 SDK

安装SDK

安装包名称: go1.9.2.darwin-amd64.tar.gz

链接: https://pan.baidu.com/s/19GZeuyrJl8B7nByqtk5FDQ 提取码: 4cqv

上传位置: Mac 一般放在用户目录下 go_dev/go

1
2
3
4
5
6
7
8
9
10
11
12
13
 ~ mkdir -p go_dev/go
~ cd go_dev/go
~/go_dev/go ls
go1.9.2.darwin-amd64.tar.gz

# 解压压缩包
~/go_dev/go tar xf go1.9.2.darwin-amd64.tar.gz
~/go_dev/go ls
go go1.9.2.darwin-amd64.tar.gz

~/go_dev/go ls go
AUTHORS CONTRIBUTORS PATENTS VERSION bin doc lib pkg src
CONTRIBUTING.md LICENSE README.md api blog favicon.ico misc robots.txt test

image-20220602212849223

Mac 下配置 Golang 环境变量

使用 root 用户,修改 /etc/profile 增加环境变量的配置

1
2
3
4
5
6
7
8
9
10
11
12
~ sudo vim /etc/profile
# 最下方添加如下内容
export GOROOT=$HOME/go_dev/go/go
export PATH=$PATH:$GOROOT/bin
export GOPATH=$HOME/goproject

# 配置立即生效
~ source /etc/profile

# 查看golang版本
~ go version
go version go1.9.2 darwin/amd64

Mac下如何开发Go程序

准备工作

打开VSCode,创建对应的目录

image-20220603141311064

1
2
3
4
5
6
package main
import "fmt"

func main(){
fmt.Println("hello world")
}

生成可执行文件并运行

1
2
3
4
5
6
7
8
 ~  cd goproject/src/go_code/project01/main
~/goproject/src/go_code/project01/main  ls
hello.go
~/goproject/src/go_code/project01/main  go build hello.go
~/goproject/src/go_code/project01/main  ls
hello hello.go
~/goproject/src/go_code/project01/main  ./hello
hello world
image-20220603141508417

我们也可以使用-o指定当前生成的二进制文件的名称

1
~/goproject/src/go_code/project01/main  go build -o hello1 hello.go

生成linux平台可执行的二进制文件

1
GOOS=linux GOARCH=amd64 go build -o hello1 hello.go

直接运行Go文件

1
2
 ~/goproject/src/go_code/project01/main  go run hello.go
hello world
image-20220603141707130

Go开发须知

Go程序开发的注意事项

1) Go 源文件以 “go” 为扩展名。
2) Go应用程序的执行入口是main()函数。这个是和其它编程语言(比如java/c)
3) Go语言严格区分大小写。
4) Go方法由一条条语句构成,每个语句后不需要分号(Go语言会在每行后自动加分号),这也体现出 Golang 的简洁性。
5) Go编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否则报错
6) Go语言定义的变量或者import的包如果没有使用到,代码不能编译通过。
7) 大括号都是成对出现的,缺一不可。

Go 语言的转义字符(escape char)

说明:常用的转义字符有如下:
1) \t : 表示一个制表符,通常使用它可以排版。
1) \n :换行符
3) \ :一个
4) " :一个”
5) \r :一个回车 fmt.Println(“天龙八部雪山飞狐\r 张飞”);

举例:

1
2
3
4
5
6
7
package main
import "fmt" //fmt 包中提供格式化,输出,输入的函数.

func main() {
//要求:要求:请使用一句输出语句,达到输入如下图形的效果
fmt.Println("姓名\t 年龄\t 籍贯\t 地址\njohn\t20\t 河北\t 北京")
}

输出如下:

image-20220604171333256

注释

Go中的注释分为行注释与块注释

行注释://

块注释:/* 内容 */

代码风格

正确格式:

1
2
3
4
5
package main
import "fmt"
func main(){
fmt.Println("hello,world!")
}

错误格式:

1
2
3
4
5
6
package main
import "fmt"
func main()
{
fmt.Println("hello,world!")
}

一行字符最长不超过 80 个字符,超过的请使用换行展示,尽量保持格式优雅

官方指南

在线调试地址: https://go.dev/play/

包使用查询: https://pkg.go.dev/

中文版在线标准库文档

地址: https://studygolang.com/pkgdoc

变量

Golang变量使用的三种方式

  • 第一种:指定变量类型,声明后若不赋值,使用默认值

image-20220614201426090

  • 第二种:根据值自行判定变量类型(类型推导)

image-20220614201653849

  • 第三种:省略 var, 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误

image-20220614201938837

  • 多变量声明

在编程中,有时我们需要一次性声明多个变量,Golang 也提供这样的语法

举例说明:

image-20220614202917231

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"

func main(){
//一次性声明多个变量
//方式一
// var n1, n2, n3 int
// fmt.Println("n1=",n1, "n2=",n2, "n3=",n3)

//方式二
// var n1, name, n3 = 100, "tom", 888
// fmt.Println("n1=",n1, "name=", name, "n3=", n3)

//方式三
n1, name, n3 := 100, "tom~", 888
fmt.Println("n1=",n1, "name=", name, "n3=", n3)
}

如何一次性声明多个全局变量【在 go 中函数外部定义变量就是全局变量】:

1
2
3
4
5
6
7
8
9
10
//定义全局变量
var n1 = 100
var n2 = 200
var name = "jack"
//上面的声明方式,也可以改成一次性声明
var (
n3 = 300
n4 = 900
name2 = "mary"
)
  • 该区域的数据值可以在同一类型范围内不断变化(重点)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"

// 变量使用的注意事项
func main(){

var i int = 10

i = 20
i = 30

fmt.Println("i=", i)
i = 1.2 // 这样写是错误的,不可以改变数据的类型
}

image-20220712133621221

  • 变量在同一个作用域(在一个函数或者在代码块)内不能重名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"

// 变量使用的注意事项
func main(){

var i int = 10

i = 20
i = 30

fmt.Println("i=", i)
//i = 1.2 // 这样写是错误的,不可以改变数据的类型

// 变量在同一个作用域(在一个函数或者在代码块)内不能重名
var i int = 66
i := 77

}
image-20220712134606264
  • 变量=变量名+值+数据类型,这一点请大家注意,变量的三要素
  • Golang 的变量如果没有赋初值,编译器会使用默认值, 比如 int 默认值 0,string 默认值为空串,小数默认为 0

变量的声明/初始化/赋值

声明变量

基本语法: var 变量名 数据类型

1
2
3
//举例子
var i int //声明一个int类型的变量 i,若不赋值,默认值为0
var s string //声明一个string类型的变量 s,若不赋值,默认值为""

初始化变量

基本语法: var 变量名 数据类型 = 值

1
2
3
//举例子
var i int = 10
var s string = "hello world"

给变量赋值

先声明变量,再给变量赋值

1
2
3
4
5
6
//举例子
var i int
i = 10

//上面这种写法等于下面这种
i := 10 //自动判断数值类型

+号的使用

  • 当左右两边都是数值型时,则做加法运算

  • 当左右两边都是字符串,则做字符串拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
// 数值相加
i := 1
j := 2
sum := i + j
fmt.Println(sum)

// 字符拼接
s1 := "hello"
s2 := "world"
s := s1 + s2
fmt.Println(s)
}

数据类型

image-20220712144636767

整数类型

类型 占用存储空间 表数范围
int8 1字节 -128-127
int16 2字节 -2^15-2^15-1
int32 4字节 -2^31-2^31-1
int64 8字节 -2^63-2^63-1
  • 其他整数类型
类型 有无符号 占用存储空间 表数范围 备注
int 32位系统4个字节
64位系统8个字节
-2^31 ~ 2^31-1
-2^63 ~ 2^63-1
uint 32位系统4个字节
64位系统8个字节
0 ~ 2^32-1
0 ~ 2^64-1
rune 与int32一样 -2^31 ~ 2^31-1 等价int32,表示一个Unicode码
byte 与uint8一样 0 ~ 255 当要存储字符时选用byte

整数型使用细节

  • Golang 各整数类型分:有符号和无符号,int uint 的大小和系统有关。
  • Golang 的整型默认声明为 int 型
1
2
3
4
5
6
7
8
9
package main
import "fmt"

func main() {
var i = 100 //此时 i 是什么类型?
//查看数据类型
fmt.Printf("i的数据类型为 %T \n", i)

}

image-20220712151142979

  • 如何在程序查看某个变量的字节大小和数据类型 (使用较多)
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"unsafe"
)

func main() {
var i int64 = 100 //此时 i 是什么类型?
//查看数据类型
fmt.Printf("i的数据类型为 %T , 占用字节数为 %d\n", i, unsafe.Sizeof(i))

}

image-20220712151508731

  • Golang程序中整型变量在使用时,遵守保小不保大的原则,即:在保证程序正确运行下,尽量使用占用空间小的数据类型。【如:年龄】
  • bit: 计算机中的最小存储单位。byte:计算机中基本存储单元。[二进制再详细说] 1byte = 8 bit

浮点数类型

类型 占用存储空间 表数范围
单精度float32 4字节 -3.403E38 ~ 3.403E38
双精度float64 8字节 -1.798E308 ~ 1.798E308
  • 关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数位+尾数位
    说明:浮点数都是有符号的.
1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var i float32 = 12.34
var n1 float32 = -0.00089
var n2 float64 = -7809656.09
fmt.Println("i=", i, "n1=", n1, "n2=", n2)
}

image-20220712152309512

  • 尾数部分可能丢失,造成精度损失。 -123.0000901
1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
var n1 float32 = -123.0000901
var n2 float64 = -123.0000901
fmt.Println("n1=", n1, "n2=", n2)
}

image-20220712153712369

说明: float64 的精度比 float32 的要准确.如果我们要保存一个精度高的数,则应该选用 float64

  • 浮点型的存储分为三部分:符号位+指数位+尾数位 在存储过程中,精度会有丢失

浮点型使用细节

  • Golang浮点类型有固定的范围和字段长度,不受具体OS(操作系统)的影响
  • Golang 的浮点型默认声明为 float64 类型。
  • 浮点型常量有两种表示形式
    • 十进制数形式:如:5.12 .512 (必须有小数点)
    • 科学计数法形式:如:5.1234e2 = 5.12 * 10 的 2 次方 5.12E-2 = 5.12/10 的 2 次方
  • 通常情况下,应该使用 float64 ,因为它比 float32 更精确。

字符类型

Golang 中没有专门的字符类型,如果要存储单个字符(字母),一般使用 byte 来保存。
字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。也 就是说对于传统的字符串是由字符组成的,而 Go 的字符串不同,它是由字节组成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
var c1 byte = 'a'
var c2 byte = 'o'

// 当我们直接输出byte值,就是输出了对应的字符的码值
fmt.Println("c1=", c1)
fmt.Println("c2=", c2)

// 如果想要输出对应的字符,需要使用格式化输出
fmt.Printf("c1=%c, c2=%c", c1, c2)

var c3 byte = "北" //报错,overflow溢出
fmt.Printf("c3=%c, 对应的码值为%d", c3, c3)
}

image-20220712155922714

对上面代码说明:

1) 如果我们保存的字符在 ASCII 表的,比如[0-1, a-z,A-Z..]直接可以保存到 byte
2) 如果我们保存的字符对应码值大于 255,这时我们可以考虑使用 int 类型保存
3) 如果我们需要安装字符的方式输出,这时我们需要格式化输出,即 fmt.Printf(“%c”, c1).

字符类型使用细节

1) 字符常量是用单引号(‘’)括起来的单个字符。例如:var c1 byte = ‘a’ var c2 int = ‘中’ var c3 byte = ‘9’
2) Go 中允许使用转义字符 ‘\’来将其后的字符转变为特殊字符型常量。例如:var c3 char = ‘\n’ // ‘\n’表示换行符
3) Go 语言的字符使用 UTF-8 编码 ,如果想查询字符对应的 utf8 码值 http://www.mytju.com/classcode/tools/encode_utf8.asp
英文字母-1个字节 汉字-3个字节
4) 在 Go 中,字符的本质是一个整数,直接输出时,是该字符对应的 UTF-8 编码的码值。
5) 可以直接给某个变量赋一个数字,然后按格式化输出时%c,会输出该数字对应的 unicode 字符
6) 字符类型是可以进行运算的,相当于一个整数,因为它都对应有 Unicode 码.

布尔类型

  • 布尔类型也叫 bool 类型,bool 类型数据只允许取值 true 和 false

  • bool类型占1个字节。

  • bool 类型适于逻辑运算,一般用于程序流程控制

string类型

字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本

  • Go语言的字符串的字节使用UTF-8编码标识Unicode文本,这样Golang统一使用UTF-8编码,中文 乱码问题不会再困扰程序员。

  • 字符串一旦赋值了,字符串就不能修改了:在 Go 中字符串是不可变的。

  • 字符串的两种表示形式:
    (1) 双引号, 会识别转义字符
    (2) 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果

  • 字符串拼接方式

1
2
str := "hello" + "world"
str += "haha"
  • 当一行字符串太长时,需要使用到多行字符串,可以如下处理
1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
// 结尾需要跟上 + 号,才能跟下一行拼接
var s1 string = "hello " + "world " + "I " + "am " +
"chinese\n"
fmt.Println(s1)
}

基本数据类型的默认值

数据类型 默认值
整型 0
浮点型 0
字符串 “”
布尔类型 false

基本数据类型的相互转换

Golang 和 java / c 不同,Go 在不同类型的变量之间赋值时需要显式转换。也就是说 Golang 中数 据类型不能自动转换。

  • 表达式 T(v) 将值 v 转换为类型 T
    • T: 就是数据类型,比如 int32,int64,float32 等等
    • v: 就是需要转换的变量

注意事项

1) Go中,数据类型的转换可以是从表示范围小–>表示范围大,也可以范围大—>范围小
2) 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化!
3) 在转换中,比如将 int64 转成 int8 【-128—127】 ,编译时不会报错,只是转换的结果是按 溢出处理,和我们希望的结果不一样。 因此在转换时,需要考虑范围.

基本数据类型转换为string

方式一:fmt.Sprintf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"

func main(){
var num1 int64 = 10
var num2 float64 = 10.12
var b bool = false
var char byte = 'h'
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("str Type %T, str=%q\n", str, str)

str = fmt.Sprintf("%c", char)
fmt.Printf("str Type %T, str=%q\n", str, str)

}

image-20220712165818290

方式二:strconv

1
2
3
4
5
6
7
8
9
10
11
12
13
// 第二种方式
var num3 int32 = 99
var num4 float32 = 12.3456
var b2 bool = true

str = strconv.FormatInt(int64(num3), 10) //必须转换为64
fmt.Printf("str Type %T, str=%q\n", str, str)

str = strconv.FormatFloat(float64(num4), 'f', 10, 32) //必须转换为64
fmt.Printf("str Type %T, str=%q\n", str, str)

str = strconv.FormatBool(b2)
fmt.Printf("str Type %T, str=%q\n", str, str)

image-20220712171720242

string转换为基本数据类型

strconv.Parsexxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"strconv"
)

func main() {
var str string = "true"
var b bool
// strconv.ParseBool(str)会返回两个值(value bool, err error),我们不需要error信息
b, _ = strconv.ParseBool(str)
fmt.Printf("b Type %T, b=%v\n", b, b)

var str2 string = "123456"
var num1 int64 //必须为64才能被 strconv.ParseInt 识别
var num2 int
num1, _ = strconv.ParseInt(str2, 10, 64)
num2 = int(num1)
fmt.Printf("num1 type %T, num1=%v\n", num1, num1)
fmt.Printf("num2 type %T, num2=%v\n", num2, num2)

var str3 string = "123.456"
var f1 float64
f1, _ = strconv.ParseFloat(str3, 64)
fmt.Printf("f1 Type %T, f1=%v", f1, f1)

}

注意事项

在将 String 类型转成 基本数据类型时,要确保 String 类型能够转成有效的数据,比如 我们可以 把 “123” , 转成一个整数,但是不能把 “hello” 转成一个整数,如果这样做,Golang 直接将其转成 0 , 其它类型也是一样的道理. float => 0 bool => false

指针

基本介绍

  • 基本数据类型,变量存的就是值,也叫值类型

  • 获取变量的地址,用&,比如: var num int, 获取 num 的地址:&num
    分析一下基本数据类型在内存的布局.

  • 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值

    比如:var ptr *int = &num

    举例说明:指针在内存的布局.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var i int = 10

fmt.Println("i的地址为=", &i)


var ptr *int = &i
fmt.Printf("ptr=%v\n", ptr)
fmt.Printf("ptr的地址=%v\n", &ptr)
fmt.Printf("ptr指向的值=%v\n", *ptr)
}
image-20220713174905334

案例演示

1) 写一个程序,获取一个 int 变量 num 的地址,并显示到终端
2) 将 num 的地址赋给指针 ptr , 并通过 ptr 去修改 num 的值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var num int = 9
fmt.Println("num=", num) // 输出值
fmt.Println("num的地址=", &num) // 输出地址

// 通过指针修改num的值为10
var ptr *int
ptr = &num
*ptr = 10
fmt.Println("num=", num)
}

image-20220713180155567

使用细节

1) 值类型,都有对应的指针类型, 形式为 *数据类型,比如 int 的对应的指针就是 *int, float32 对应的指针类型就是 *float32, 依次类推。
2) 值类型包括:基本数据类型 int 系列,float 系列, bool, string数组结构体 struct

值类型和引用类型

1) 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
2) 引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型

使用特点

  • 值类型

变量直接存储值,内存通常在栈中分配

image-20220714144713333
  • 引用类型

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

image-20220714144504424
  • 内存的栈区和堆区示意图
image-20220714144751395

标识符的命名规范

标识符概念

1) Golang 对各种变量、方法、函数等命名时使用的字符序列称为标识符
2) 凡是自己可以起名字的地方都叫标识符

命名规则

1) 由 26 个英文字母大小写,0-9 ,_ 组成
2) 数字不可以开头。var num int //ok var 3num int //error
3) Golang中严格区分大小写。
var num int
var Num int
说明:在 golang 中,num 和 Num 是两个不同的变量
4) 标识符不能包含空格。
5) 下划线”_”本身在 Go 中是一个特殊的标识符,称为空标识符。可以代表任何其它的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用
6) 不能以系统保留关键字作为标识符(一共有 25 个),比如 break,if 等等…

标识符命名注意事项

  • 包名:保持 package 的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和 标准库不要冲突 fmt

  • 变量名、函数名、常量名:采用驼峰法

    举例:

    var stuName string = “tom” 形式: xxxYyyyyZzzz …

    var goodPrice float32 = 1234.5

  • 如果变量名函数名常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能
    在本包中使用 ( 注:可以简单的理解成,首字母大写是公开的,首字母小写是私有的) ,在 golang 没有 public , private 等关键字。

系统保留关键字

break default func interface select
case defer go map struct
chan else goto package switch
const fallthrogh if range type
continue for import return var

系统的预定义标识符

append bool byte cap close complex
complex64 complex128 uint16 copy false float32
float64 imag int int8 init16 uint32
int32 int64 iota len make new
nil panic uint64 print println real
recover string true uint uint8 uintprt

运算符

运算符的基本介绍

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

1) 算术运算符
2) 赋值运算符
3) 比较运算符/关系运算符 4) 逻辑运算符
4) 位运算符
5) 其它运算符

算术运算符

算术运算符是对数值类型的变量进行运算的,比如:加减乘除。在 Go 程序中使用的非常多

算术运算符的一览表

运算符 运算 范例 结果
+ 正号 +3 3
- 负号 -4 -4
+ 5 + 5 10
- 6 - 4 2
* 3 * 4 12
/ 5 / 5 1
% 取模(取余) 7 % 5 2
++ 自增 a=2 a++ a=3
自减 a=2 a– a=1
+ 字符串相加 “He” + “llo” “Hello”

演示 / 的使用特点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
// 两个整数相除,那么结果会去掉小数点,保留整数
fmt.Println(10 / 4)

var a float32 = 10 / 4
fmt.Println(a)

// 如果想要保留小数怎么办?
var b float32 = 10.0 / 4
fmt.Println(b)
}
image-20220714160546154

演示 % 的使用特点

1
2
3
4
5
//演示 %的使用
// 看一个公式 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) // =1
fmt.Println("-10%-3=", -10 % -3) // =-1
image-20220714161720468

关系运算符(比较运算符)

基本介绍

  1. 关系运算符的结果都是 bool 型,也就是要么是 true,要么是 false

  2. 关系表达式 经常用在 if 结构的条件中或循环结构的条件中

关系运算符一览图

运算符 运算 范例 结果
== 相等于 4==3 false
!= 不等于 4!=3 true
< 小于 4<3 false
> 大于 4>3 true
<= 小于等于 4<=3 false
>== 大于等于 4>=3 true

逻辑运算符

基本介绍

用于连接多个条件(一般来讲就是关系表达式),最终的结果也是一个 bool 值

逻辑运算的说明

  • 假定A值为True
  • 假定B值为False
运算符 描述 实例
&& 逻辑 与运算符,如果两边的操作数都是True,则为True,否则为False (A && B)为False
|| 逻辑 或运算符,如果两边的操作数有一个是True,则为True,否则为False (A || B)为True
! 逻辑 否运算符,如果条件为True,则逻辑为False,否则为True !(A && B)为True

细节说明

1) && 也叫短路与:如果第一个条件为false,则第二个条件不会判断,最终结果为false
2) || 也叫短路或:如果第一个条件为true, 则第二个条件不会判断,最终结果为true

赋值运算符

赋值运算符就是将某个运算后的值,赋给指定的变量

赋值运算符的分类

运算符 描述 实例
= 简单地赋值运算符,将一个表达式的值赋给一个左值 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
<<= 左移后赋值 C <<= 2 等于 C = C<<2
>>= 右移后赋值 C >>= 2 等于 C = C>>2
&= 按位与后赋值 C &= 2 等于 C = C & 2
^= 按位异或后赋值 C ^= 2 等于 C = C ^ 2
|= 按位或后赋值 C |= 2 等于 C = C | 2

赋值运算符的特点

  • 运算顺序从右往左

  • 赋值运算符的左边 只能是变量,右边 可以是变量、表达式、常量值

  • 复合赋值运算符等价于下面的效果

    比如:a+=3 等价于 a=a+3

位运算符

运算符 描述
& 按位与运算符”&”是双目运算符,其功能是参与运算的两数各对应的二进位相与
运算规则是:有一个为1,结果为1,否则为0
| 按位或运算符”|”是双目运算符,其功能是参与运算的两数各对应的二进位相或
运算规则是:同时为1,结果为1,否则为0
^ 按位异或运算符”^”是双目运算符,其功能是参与运算的两数各对应的二进位相异或
运算规则是:当二进位不同时,结果为1,否则为0
<< 左移运算符”<<”是双目运算符,其功能是把”<<”左边的运算数的各二进位全部左移若干位,高位丢弃,低位补0,左移n位就是乘以2的n次方.
>> 右移运算符”>>”是双目运算符,其功能是把”>>”左边的运算数的各二进位全部右移若干位,右移n位就是除以2的n次方.

其它运算符说明

运算符 描述 实例
& 返回变量存储地址 &a;将给出变量的实际地址
* 指针变量 *a;是一个指针变量

运算符的优先级

运算符的优先级的一览表

从上到下,优先级依次递减

分类 描述 关联性
后缀 () [] -> . ++ – 左到右
单目 + - ! ~ (type) * & sizeof 右到左
乘法 * / % 左到右
加法 + - 左到右
移位 << >> 左到右
关系 < <= > >= 左到右
相等 == != 左到右
按位AND & 左到右
按位XOR ^ 左到右
按位OR | 左到右
逻辑AND && 左到右
逻辑OR || 左到右
赋值运算符 = += -= *= /= %= >>= <<= &= ^= |= 右到左
逗号 , 左到右

键盘输入语句

介绍

在编程中,需要接收用户输入的数据,就可以使用键盘输入语句来获取。InputDemo.go

步骤

  • 导入fmt

  • 调用 fmt 包的 fmt.Scanln() 或者 fmt.Scanf()

官方文档查询使用方法: https://pkg.go.dev/fmt#Scan

演示案例

要求:可以从控制台接收用户信息,【姓名,年龄,薪水, 是否通过考试 】。

  • 使用 fmt.Scanln() 获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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("请输入是否通过考试: ")
fmt.Scanln(&isPass)

fmt.Printf("名字是 %v\n年龄是 %v\n薪水是 %v\n是否通过考试: %v\n", name, age, sal, isPass)
}
image-20220721204511796
  • 使用 fmt.Scanf() 获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
// 要求:可以从控制台接收用户信息,【姓名,年龄,薪水, 是否通过考试 】。
var name string
var age byte
var sal float32
var isPass bool

// 方式二: fmt.scanf,可以按照指定的格式输入
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)
}
image-20220721210421076

进制

对于整数,有四种表示方式:

  • 二进制: 0,1 ,满 2 进 1。
    在 golang 中,不能直接使用二进制来表示一个整数,它沿用了 c 的特点。
  • 十进制: 0-9 ,满 10 进 1。
  • 八进制: 0-7 ,满 8 进 1. 以数字 0 开头表示。
  • 十六进制: 0-9及A-F,满16进1. 以0x或0X开头表示。此处的 A-F 不区分大小写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
var i int = 5
// 二进制输出
fmt.Printf("%b \n", i)

// 八进制输出, 满 8 进 1,以数字 0 开头
var j int = 011
fmt.Println("j=", j)

// 十六进制输出,满 16 进 1, 以 0x 或 0X 开头
var k int = 0x11
fmt.Println("k=", k)
}
image-20220721212308826

进制的图示

十进制 十六进制 八进制 二进制
0 0 0 0
1 1 1 1
2 2 2 10
3 3 3 11
4 4 4 100
5 5 5 101
6 6 6 110
7 7 7 111
8 8 10 1000
9 9 11 1001
10 A 12 1010
11 B 13 1011
12 C 14 1100
13 D 15 1101
14 E 16 1110
15 F 17 1111
16 10 20 10000
17 11 21 10001

进制的转换

  • 其他进制转十进制
    • 二进制转十进制
    • 八进制转十进制\
    • 十六进制转十进制
  • 十进制转其它进制
    • 十进制转二进制
    • 十进制转八进制
    • 十进制转十六进制
  • 二进制转其它进制
    • 二进制转八进制
    • 二进制转十六进制
  • 其他进制转二进制
    • 八进制转二进制
    • 十六进制转二进制

其它进制转十进制

二进制如何转十进制

规则: 从最低位开始(右边开始),将每个位上的数提取出来,乘以2的(位数-1)次方,然后求和

  • 案例: 请将二进制 1011 转换成十进制
1
2
3
1011 = 1*2^(1-1) + 1*2^(2-1) + 0*2^(3-1) + 1*2^(4-1)
= 1 + 2 + 0 + 8
= 11

八进制如何转十进制

image-20220722172941782

规则: 从最低位开始,将每个位上的数提取出来,乘以8的(位数-1)次方,然后求和

  • 案例: 请将0123转换为十进制
1
2
3
0123 = 3*8^(1-1) + 2*8^(2-1) + 1*8^(3-1) + 0*8^(4-1)
= 3 + 16 + 64 + 0
= 83

十六进制转十进制

image-20220722173946386

规则: 从最低位开始,将每个位上的数提取出来,乘以16的(位数-1)次方,然后求和

  • 案例: 请将0x34A转成十进制
1
2
3
0x34A = 10*16^(1-1) + 4*16^(2-1) + 3*16^(3-1)
= 10 + 64 + 768
= 842

十进制转其它进制

十进制转二进制

规则: 将该数不断除以2,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制

  • 案例: 请将56转成二进制
1
2
3
4
5
6
7
8
9
56/2=28 - 0
28/2=14 - 0
14/2=7 - 0
7/2=3 - 1
3/2=1 - 1
1/2=0 - 1 // 1除以任何大于1的正整数,商都为0,余数为1

1 1 1 0 0 0 = 56
32 16 8 4 2 1

十进制转八进制

规则: 将该数不断除以8,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制

  • 案例: 请将156转成八进制
1
2
3
4
5
156/8=19 - 4
19/8=2 - 3
2/8=0 - 2 // 2除以82;(商0

八进制: 0234=156

十进制转十六进制

规则: 将该数不断除以16,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制

  • 案例: 请将356转成十六进制
1
2
3
4
5
356/16=22 - 4
22/16=1 - 6
1/16=0 - 1

十六进制: 0x164=356

二进制转八进制/十六进制

二进制转八进制

规则: 将二进制数每三位一组(从低位开始组合),转成对应的八进制即可

  • 案例: 请将二进制 11010101 转成八进制
1
11010101 => (11) (010) (101) => 0325

二进制转十六进制

规则: 将二进制数每四位一组(从低位开始组合),转成对应的十六进制即可

  • 案例: 请将二进制 11010101转成十六进制
1
11010101 => (1101) (0101) => 0x135 => 0xD5

八进制/十六进制转二进制

八进制转二进制

规则: 将八进制数每1位,转成对应的一个三位数即可

  • 案例: 请将0237转成二进制
1
0237 => (010) (011) (111) => 10011111

十六进制转二进制

规则: 将八进制数每1位,转成对应的一个四位数即可

  • 案例: 请将0x237转成二进制
1
0x237 => (0010) (0011) (0111) => 1000110111

位运算

原码、反码、补码

精简6句话:

  • 二进制的最高位是符号位: 0表示正数,1表示负数
    • 1 => [0000 0001]
    • -1 => [1000 0001]
  • 正数的源码/反码/补码都一样
  • 负数的反码=原码符号位不变,其他位取反
    • 1 => 原码[0000 0001]
    • 1 => 反码[0000 0001]
    • 1 => 补码[0000 0001]
    • -1 => 原码[1000 0001]
    • -1 => 反码[1111 1110]
    • -1 => 补码[1111 1111]
  • 负数的补码=反码+1
  • 0的反码/补码都是0
  • 计算机运算时以补码方式进行运算

位运算符和移位运算符

  • Golang中有3个位运算,它们的运算规则是:
    • 按位与&: 两位全为1,结果为 1,否则为 0
    • 按位或|: 两位有一个为 1,结果为 1,否则为 0
    • 按位异或 ^: 两位一个为 0,一个为 1,结果为 1,否则为 0

注意: 整数的反码补码都一样,所以补码=原码,负数的时候需要用补码先推回到反码,再用反码推回原码

计算值 原码 反码 补码
2 0000 0010 0000 0010 0000 0010
3 0000 0011 0000 0011 0000 0011
2&3 - - 0000 0010(2)
2|3 - - 0000 0011(3)
2^3 - - 0000 0001(1)
-2 1000 0010 1111 1101 1111 1110
2 0000 0010 0000 0010 0000 0010
-2^2 1000 0100(-4) 1111 1011(补码-1) 1111 1100
  • Golang中有2个移位运算符: **
    **右移运算符 >>:
    低位溢出,符号位不变,并用符号位补溢出的高位
    左移运算符 <<: 符号位不变,低位补 0

  • 案例演示

    1
    2
    a := 1 >> 2 // 0000 0001 => 0000 0000 = 0
    c := 1 << 2 // 0000 0001 => 0000 0100 = 4

流程控制

这个章节不写案例,没啥好写的,不懂的最好去查一下相关案例,能更加快速的上手

顺序控制

代码中,没有判断,也没有跳转.因此程序按照默认的流程执行,即顺 序控制。

这个不必过多介绍,这个不明白啥意思别往下看了,你学不会了兄弟-_-||

分支控制

单分支

  • 基本语法
1
2
3
if 条件 {
PASS
}

双分支

注意: 下面的else不可以换行,必须为 }esle{

  • 基本语法
1
2
3
4
5
if 条件 {
PASS
}else{
PASS
}

多分支

  • 基本语法
1
2
3
4
5
6
7
8
9
if 条件 {
PASS
}else if 条件 {
PASS
}...
...
}else{
PASS
}

需要注意的是,if语句的判断条件中,不能只出现赋值语句,例如 if a=5,但是可以写成if a=5;a>4

嵌套分支

在一个分支结构中又完整的嵌套了另一个完整的分支结构,里面的分支的结构称为内层分支外面的分支结构称为外层分支。

1
2
3
4
5
6
7
if 条件 {
if 条件 {
PASS
}else{
PASS
}
}

switch分支

  • switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上到下逐一测 试,直到匹配为止。
  • 匹配项后面也不需要再加 break

基本语法

1
2
3
4
5
6
7
8
9
switch 表达式 {
case 表达式1:
PASS
case 表达式2:
PASS
...
default:
PASS
}

总结:

1) switch的执行的流程是,先执行表达式,得到值,然后和case的表达式进行比较,如果相等,
就匹配到,然后执行对应的 case 的语句块,然后退出 switch 控制。
2) 如果 switch 的表达式的值没有和任何的 case 的表达式匹配成功,则执行 default 的语句块。执行后退出 switch 的控制.
3) golang的case后的表达式可以有多个,使用逗号间隔.
4) golang 中的 case 语句块不需要写 break , 因为默认会有,即在默认情况下,当程序执行完 case 语
句块后,就直接退出该 switch 控制结构。

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
var i int
fmt.Println("请输入一个数字: ")
fmt.Scanf("%v", &i)

switch i {
case 1:
fmt.Println("数值1")
case 2:
fmt.Println("数值2")
case 3:
fmt.Println("数值3")
default:
fmt.Println("输入错误")
}
}

image-20220725151918021

也可以写成下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
var i int
fmt.Println("请输入一个数字: ")
fmt.Scanf("%v", &i)

switch {
case i == 1:
fmt.Println("数值1")
case i == 2:
fmt.Println("数值2")
case i == 3:
fmt.Println("数值3")
default:
fmt.Println("输入错误")
}
}
  • switch 穿透-fallthrough: 如果在 case 语句块后增加 fallthrough ,则会继续执行下一个 case,也 叫 switch 穿透
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
var num int = 10

switch num {
case 10:
fmt.Println("OK1")
fallthrough
case 20:
fmt.Println("OK2")
fallthrough
case 30:
fmt.Println("OK3")
default:
fmt.Print("没有匹配到")
}
}

image-20220725153413174

循环控制

for循环

方式一

  • 基本语法
1
2
3
for 循环变量初始化;循环条件;循环变量迭代{
PASS
}
  • 例子: 打印十遍Hello World
1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
for i := 1; i <= 10; i++ {
fmt.Println("Hello World")
}
}

image-20220725154621742

方式二

将变量初始化和变量迭代写到其它位置

  • 基本语法
1
2
3
for 循环判断条件 {
PASS
}
  • 例子: 打印十遍Hello World
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
var i int = 1
for i <= 10 {
fmt.Println("Hello World")
i++
}
}

方式三: 无限循环

等价 for ; ; {} 是一个无限循环, 通常需要配合 break 语句使用

  • 基本语法
1
2
3
for {
PASS
}
  • 例子:输出1-10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
i := 1
for {
if i <= 10 {
fmt.Println(i)
i++
} else {
break //退出循环
}
}
}

image-20220725160145059

for-range

可以方便的遍历字符串与数组

  • 例子(传统方式): 按照索引顺序,输出字符串的单个字符
1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var str string = "Hello,World!"
for i := 0; i<len(str);i++{
fmt.Printf("%c \n", str[i])
}
}

image-20220725161130562

  • 例子(for-range): 字符串遍历
1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var str string = "Hello,World!"
for index,value := range str{
fmt.Printf("index=%d, value=%c \n", index, value)
}
}

image-20220725161927874

对于传统的字符串遍历来说,如果字符串中含有中文,会报错

原因是传统的对字符串的遍历是按照字节来遍历,而一个汉字在 utf8 编码是对应 3 个字节。

如何解决?需要要将 str 转成 []rune 切片

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
var str string = "Hello,World!你好世界!"
str_rune := []rune(str)
for i := 0; i<len(str_rune);i++{
fmt.Printf("%c \n", str_rune[i])
}
}

for-range的方式处理中文不会出问题

while 和 do..while

while

Go 语言没有 while 和 do…while 语法,这一点需要注意一下,如果我们需要使用类似其它语
言(比如 java / c 的 while 和 do…while ),可以通过 for 循环来实现其使用效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
i := 1
for {
if i > 10 {
break
}
fmt.Println(i)
i++
}
}

就是上面for循环中说的for ; ; {} 是一个无限循环

do..while

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
i := 1
for {
fmt.Println(i)
i++
if i > 10 {
break
}
}
}
  • 练习: 利用*输出金字塔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
)

func main() {
var line int
fmt.Println("请输入金字塔的行数: ")
fmt.Scanf("%d", &line)

// 输出一个矩形
for i := 1; i <= line; i++ {
// 空格
for j := 1; j <= line-i; j++ {
fmt.Print(" ")
}
// 星号
for k := 1; k <= 2*i-1; k++ {
fmt.Print("*")
}
fmt.Println()
}
}

break

这个很好理解,主动退出循环嘛,这里不做过多解释,但是有一个标签的功能需要说一下

  • break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块
  • break默认跳出最近的 for 循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

func main() {
var line int = 10

label:
for i := 1; i <= line; i++ {
fmt.Println("1")
for j := 1; j <= line-i; j++ {
if j == 5{
break label //直接跳出指定位置的循环
}
fmt.Println("2")
}
}
}

image-20220725171828159

continue

continue 语句用于结束本次循环,继续执行下一次循环。

continue 语句出现在多层嵌套的循环语句体中时,可以通过标签指明要跳过的是哪一层循环 , 这个和前面的 break 标签的使用的规则一样.

这里就不演示了

放个截图

image-20220725172334025

goto

1) Go 语言的 goto 语句可以无条件地转移到程序中指定的行。
2) goto语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能。
3) 在 Go 程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序
都产生困难

  • 一个案例演示快速说明,麻利滴!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import "fmt"

func main() {
var num int = 10

if num > 5 {
goto label
}

fmt.Println("我小于5!")

label:
fmt.Println("我大于5!")
}

image-20220725174915995

return

return 使用在方法或者函数中,表示跳出所在的方法或函数,在讲解函数的时候,会详细的介绍。

说明:

1) 如果 return 是在普通的函数,则表示跳出该函数,即不再执行函数中 return 后面代码,也可以理解成终止函数。
2) 如果 return 是在 main 函数,表示终止 main 函数,也就是说终止程序。

函数

因为本人有一定基础,这里就不做太详细的解释,直接上干货,兄弟们!

基本语法

函数可以有返回值,也可以没有

1
2
3
4
func 函数名 [形参列表] [返回值列表]{
PASS
return [返回值列表]
}

快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

func calculate(n1 float64, n2 float64, operator byte) float64 {
var res float64
switch operator {
case '+':
res = n1 + n2
case '-':
res = n1 - n2
case '*':
res = n1 * n2
case '/':
res = n1 / n2
default:
fmt.Println("操作符号错误")
}
return res
}

func main() {
var n1 float64 = 1.2
var n2 float64 = 2.3
var operator byte = '+'
result := calculate(n1, n2, operator)
fmt.Println(result)
}

image-20220726105053724

  • 在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如 main.go 中,去使用 utils.go 文件中的函数,如何实现? => 包

  • 现在有两个程序员共同开发一个 Go 项目,程序员 xiaoming 希望定义函数 Cal ,程序员 xiaoqiang 也想定义函数也叫 Cal。两个程序员为此还吵了起来,怎么办? => 包

原理

包的本质实际上就是创建不同的文件夹,来存放程序文件。

image-20220726111732328

  • 说明: go 的每一个文件都是属于一个包的,也就是说 go 是以包的形式来管理文件和项目目录结构

三大作用

  • 区分相同名字的函数、变量等标识符
  • 当程序文件很多时,可以很好的管理项目
  • 控制函数、变量等访问范围,即作用域

说明

打包基本语法

1
package 包名

引入包的基本语法

1
import "包的路径"

快速入门

注意: golang在找包的时候,默认会在/usr/local/go/src/寻找,所以文件要放在golang的src目录下,我目前main.go的绝对路径是/usr/local/go/src/golang_study/08-hanshu/main,所以下面调用utils包时,只需要从golang_study开始写就可以了

  • 目录格式

image-20220726121022878

  • utils.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package utils

import "fmt"

func Calculate(n1 float64, n2 float64, operator byte) float64 {
var res float64
switch operator {
case '+':
res = n1 + n2
case '-':
res = n1 - n2
case '*':
res = n1 * n2
case '/':
res = n1 / n2
default:
fmt.Println("操作符号错误")
}
return res
}
  • main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"golang_study/08-hanshu/utils"
)


func main() {
var n1 float64 = 1.2
var n2 float64 = 2.3
var operator byte = '+'
result := utils.Calculate(n1, n2, operator)
fmt.Println(result)
}

注意事项和细节

  1. 在给一个文件打包时,该包对应一个文件夹,比如这里的 utils 文件夹对应的包名就是 utils, 文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
  2. 当一个文件要使用其它包函数或变量时,需要先引入对应的包
  • 引入方式 1:
1
import "包名"
  • 引入方式 2:
1
2
3
4
import ( 
"包名"
"包名"
)
  1. package 指令在 文件第一行,然后是 import 指令。
  2. 在 import 包时,路径从 $GOPATHsrc 下开始,不用带 src , 编译器会自动从 src 下开始引入
  3. 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言 的 public ,这样才能跨包访问。比如 utils.go 的Calculate

image-20220726132407437

  1. 在访问其它包函数,变量时,其语法是 包名.函数名, 比如这里的 main.go 文件中

image-20220726133137898

  1. 如果包名较长,Go 支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了

image-20220726133314963

  1. 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main .这个就
    是一个语法规范,如果你是写一个库 ,包名可以自定义

image-20220726134218663

打包生成可执行文件命令如下:

1
go build -o bin/my.exe golang_study/08-hanshu/main

image-20220726134749000

image-20220726134917258

函数的调用机制

为了让大家更好的理解函数调用过程, 看两个案例,并画出示意图,这个很重要

  • 传入一个数+1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
)

func test(n1 int) {
n1 = n1 + 1
fmt.Println("test n1=", n1)
}

func getSum(n1 int, n2 int) int {
sum := n1 + n2
fmt.Println("getSum sum=", sum)
return sum
}

func main() {
n1 := 10
test(n1)
fmt.Println("main() n1=", n1)

sum := getSum(10, 20)
fmt.Println("main() sum=", sum)
}

image-20220726142713706

为什么上面代码执行结果中,n1的值没有真正的改变呢?请看下面这个解释吧

image-20220726151204173

  1. 在调用一个函数时,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间 和其它的栈的空间区分开来
  2. 在每个函数对应的栈中,数据空间是独立的,不会混淆
  3. 当一个函数调用完毕(执行完毕)后,程序会销毁这个函数对应的栈空间

return语句

基本语法与说明

Golang函数支持返回多个值,这一点是其他编程语言没有的

1
2
3
4
func (形参列表) (返回值类型列表){
语句...
return 返回值列表
}

说明:

  1. 如果返回多个值时,在接收时,如果希望忽略某个值,则使用_符号表示占位忽略
  2. 如果返回值只有一个,(返回值类型列表)可以不写

案例: 计算两个数的和and差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

// 计算两个数的 和/查 值
func getSumAndSub(n1 int, n2 int) (int, int){
sum := n1 + n2
sub := n1 - n2
return sum, sub
}

func main() {
var n1 int = 30
var n2 int = 20
sum, sub := getSumAndSub(n1, n2)
fmt.Printf("sum=%d, sub=%d \n", sum, sub)
}

image-20220726161157132

函数的递归

一个函数在函数体内又调用了本身,我们称为递归调用

快速通过代码理解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"


func test(n int) {
if n > 2 {
n--
test(n)
}
fmt.Println("n=", n)
}

func main() {
test(4)
}

执行结果

image-20220726165814493

image-20220726170028404

???

为什么是 2 2 3 ???

递归说白了就是套娃,优先执行最里边的,然后由里到外依次执行,所以你会看到 2 2 3 这个结果

什么?还不明白?

那我给你解释一下上面的代码

  • test(4) – 第一步
    • 符合if判断条件
      • n– 此时n由4-1变为了3
      • test(3)
    • n=3
  • test(3) – 第二步
    • 符合if判断条件
      • n– 此时n由3-1变为了2
      • test(2)
    • n=2
  • test(2) – 第三步
    • 不符合if判断条件
    • n=2

由下到上(从里到外)输出n的结果,就是 2 2 3

原来如此

将上面的案例代码改变一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"


func test(n int) {
if n > 2 {
n--
test(n)
}else{ //增加了else判断分支
fmt.Println("n=", n)
}
}

func main() {
test(4)
}

请问,输出结果是什么?

经过我掐指一算,n=2

是的,结果只是n=2

总结

函数递归需要遵守的重要原则:

1) 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
2) 函数的局部变量是独立的,不会相互影响
3) 递归必须向退出递归的条件逼近,否则就是无限递归,死龟了:)
4) 当一个函数执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁

递归函数练习

  • 给出一个整数n,求斐波那契数列中的第n位是什么
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

// 计算斐波那契数,给出一个整数n(n为斐波那契数列中的第几位),输出第n位的数值
func f(n int) int {
if n == 1 || n == 2 {
return 1
} else {
return f(n-1) + f(n-2)
}
}

func main() {
fmt.Println("f(3)=", f(3))
fmt.Println("f(5)=", f(5))
}

image-20220727085416484

  • 猴子吃桃子问题:

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

    • 思路分析:

      • 第10 天只有一个桃子

      • 第9天有几个桃子 = (第10天桃子数量 + 1) * 2

      • 规律: 第 n 天的桃子数据 peach(n) = (peach(n+1) + 1) * 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

// - 有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一半,然后
// 再多吃一个。当到第十天时,想再吃时(还没吃),发现只有 1 个桃子了。问题:最初共多少个桃子?
// - 思路分析:
// - 第10 天只有一个桃子
// - 第9天有几个桃子 = (第10天桃子数量 + 1) * 2
// - 规律: 第 n 天的桃子数据 peach(n) = (peach(n+1) + 1) * 2

func peach(n int) int {
if n == 10 {
return 1
} else {
return (peach(n+1) + 1) * 2
}
}

func main() {
fmt.Println("第一天的桃子数量为", peach(1))
}

image-20220727091940161

函数的注意事项和细节

1) 函数的形参列表可以是多个,返回值列表也可以是多个。
2) 形参列表和返回值列表的数据类型可以是值类型引用类型
3) 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其
它包文件使用,类似 public , 首字母小写,只能被本包文件使用,其它包文件不能使用,类似 privat
4) 函数中的变量是局部的,函数外不生效
5) 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。可以参考函数的调用机制
6) 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func test(n *int) {
*n = *n + 10
fmt.Println("n=", *n)
}

func main() {
var n1 int = 20
test(&n1)
fmt.Println("n1=", n1)
}

结果为

1
2
n= 30
n1= 30
  1. Go函数不支持函数重载
image-20220727151013127
  1. 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func getSum(n1 int, n2 int) int {
return n1 + n2
}

func main() {
a := getSum
fmt.Printf("a的类型是%T, getSum的类型是%T \n", a, getSum)

n1 := 10
n2 := 20
fmt.Println("n1+n2=", a(n1, n2))
}

image-20220727152305861

  1. 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func getSum(n1 int, n2 int) int {
return n1 + n2
}

// 下面的funvar表示函数的变量名,例如int的变量名是num1和num2
func myFun(funvar func(int, int) int, num1 int, num2 int) int {
return funvar(num1, num2)
}

func main() {

n1 := 10
n2 := 20
fmt.Println("n1+n2=", myFun(getSum, n1, n2))
}

image-20220727153145866

  1. 为了简化数据类型定义,Go 支持自定义数据类型

基本语法:

  • type 自定义数据类型名 数据类型 // 理解: 相当于一个别名

案例1:

  • type myInt int // 这时 myInt 就等价 int 来使用了.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
type myInt int

var n1 myInt
var n2 int

n1 = 1
n2 = int(n1) //虽然能自定义,但是系统认为这是两个不同的变量类型

fmt.Println("n1=", n1, "n2=", n2)
}

案例2:

  • 自定义函数类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func getSum(n1 int, n2 int) int {
return n1 + n2
}

type myFunType func(int, int) int // 在刚才的代码上进行修改,增加此行

func myFun(funvar myFunType, num1 int, num2 int) int { // 直接使用myFunType类型定义funvar变量参数
return funvar(num1, num2)
}

func main() {

n1 := 10
n2 := 20
fmt.Println("n1+n2=", myFun(getSum, n1, n2))
}
  1. 支持对函数返回值命名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func getSumAndSub1(n1 int, n2 int) (sum int, sub int) {
sum = n1 + n2
sub = n1 - n2
return sum, sub
}

func main() {
sum, sub := getSumAndSub1(2, 1)
fmt.Printf("sum=%d, sub=%d \n", sum, sub)
}
  1. 使用 _ 标识符,忽略返回值
1
2
3
4
func main() {
sum, _ := getSumAndSub1(2, 1)
fmt.Printf("sum=%d \n", sum)
}
  1. Go支持可变参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func sum(n1 int, args ...int) int {
sum := n1
for i := 0; i < len(args); i++ {
sum += args[i]
}
return sum
}

func main() {
result := sum(1, 2, 3, 4)
fmt.Println("1+2+3+4=", result)
}

init函数

每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也 就是说 init 会在 main 函数前被调用。

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func init() {
fmt.Println("init()...")
}

func main() {
fmt.Println("main()...")
}

image-20220727161602326

init函数的注意事项和细节

  1. 如果一个文件同时包含全局变量定义init 函数main 函数,则执行的流程为:
  • 全局变量定义
  • init函数
  • main 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

// 定义全局变量
var age = test()

func test() int {
fmt.Println("test()...")
return 1
}

func init() {
fmt.Println("init()...")
}

func main() {
fmt.Println("main... num=", age)
}

image-20220727162716758

从上图可以看到,函数test()先被执行了,也就是说全局变量age首先被执行

  1. init函数最主要的作用,就是完成一些初始化的工作
  2. 分析下图的执行顺序

image-20220728103828158

匿名函数

Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考 虑使用匿名函数,匿名函数也可以实现多次调用。

  • 案例一

在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
// 直接调用,只能使用一次
result := func(n1 int, n2 int) int {
return n1 + n2
}(10, 20)

fmt.Println("n1+n2=", result)
}
  • 案例二

将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
// 直接调用,只能使用一次
result := func(n1 int, n2 int) int {
return n1 + n2
}(10, 20)

fmt.Println("n1+n2=", result)

// a的数据类型为函数类型,通过该变量来调用匿名函数
a := func(n1 int, n2 int) int {
return n1 + n2
}

res := a(20, 30)
fmt.Println("res=", res)

}

全局匿名函数

如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

// 声明全局变量
var (
Fun = func(n1 int, n2 int) int {
return n1 * n2
}
)

func main() {
res := Fun(3, 4)
fmt.Println("n1 * n2 =", res)
}

闭包

闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func AddUpper() func(int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}

func main() {
f := AddUpper()
fmt.Println(f(1))
fmt.Println(f(1))
fmt.Println(f(1))
}

image-20220728113811995

image-20220726170028404

我表示很懵逼啊,什么玩意儿???

  • 对上面代码的说明和总结

1) AddUpper 是一个函数,返回的数据类型是fun (int) int
2) 闭包的说明: 返回的是一个匿名函数, 但是这个匿名函数引用到函数外n ,因此这个匿名函数就和n 形成一个整体,构成闭包
3) 大家可以这样理解: 闭包是类,函数是操作n 是字段。函数和它使用到 n 构成闭包。
4) 当我们反复的调用f 函数时,因为 n 是初始化一次,因此每调用一次就进行累计。
5) 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。

  • 更改一下上面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func AddUpper() func(int) int {
var n int = 10
var str string = "hello"
return func(x int) int {
n = n + x
str += string(36) //ASCII码中,36对应的是 "$"
fmt.Println("str=", str)
return n
}
}

func main() {
f := AddUpper()
fmt.Println(f(1))
fmt.Println(f(1))
fmt.Println(f(1))
}

image-20220728131653658

闭包的最佳实践

  • 请编写一个程序,具体要求如下
    • 编写一个函数 makeSuffix(suffix string)可以接收一个文件后缀名(比如.jpg),并返回一个闭包
    • 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。
    • 要求使用闭包的方式完成
    • strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"strings"
)

// - 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包
// - 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。
// - 要求使用闭包的方式完成
// - strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。

func makeSuffix(suffix string) func(string) string {
return func(name string) string {
// 如果 name 没有指定后缀,则加上,否则就返回原来的名字
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}

func main() {
f := makeSuffix(".jpg")
fmt.Println("文件名处理后=", f("1"))
fmt.Println("文件名处理后=", f("2.jpg"))
}

image-20220728140441341

defer

在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。

快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func sum(n1 int, n2 int) int {
// 当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈)
// 当函数执行完毕后,再从defer栈,按照先入后出的方式出栈执行
defer fmt.Println("ok1 n1=", n1)
defer fmt.Println("ok2 n2=", n2)

res := n1 + n2
fmt.Println("ok3 res=", res)
return res
}

func main() {
res := sum(10, 20)
fmt.Println("res=", res)
}

image-20220728141829569

注意事项和细节

1) 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个中[我为了讲课方便,暂时称该栈为 defer 栈], 然后继续执行函数下一个语句。
2) 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈先入后出的机制)
3) 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。

  • 看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func sum(n1 int, n2 int) int {
// 当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈)
// 当函数执行完毕后,再从defer栈,按照先入后出的方式出栈执行
defer fmt.Println("ok1 n1=", n1)
defer fmt.Println("ok2 n2=", n2)

// 增加部分
n1++ // n1 = 11
n2++ // n2 = 21
res := n1 + n2 // res = 32
fmt.Println("ok3 res=", res)
return res
}

func main() {
res := sum(10, 20)
fmt.Println("res=", res)
}

看执行结果

image-20220728142928572

当执行两个defer语句的时候,这两行被放入了defer栈中,所以它并没有参与到下面的计算之中,等函数执行完毕后,才会根据先入后出的原则依次执行,所以我们会看到这样的结果

defer 的最佳实践

defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。

模拟代码:

1
2
3
4
5
6
func test() {
// 关闭文件资源
file = openfile(文件名)
defer file.close()
// 其他代码
}
  • 使用defer的好处:

1) 在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是
锁资源), 可以执行 defer file.Close() defer connect.Close()
2) 在 defer 后,可以继续使用创建资源.
3) 当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源.
4) 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。

字符串常用系统函数

统计字符串长度: len()

1
2
var str string = "Hello"
fmt.Println("str len", len(str))

字符串遍历

1
2
3
4
5
6
str := "Hello北京"
// 字符串遍历,同时处理有中文的问题 r := []rune(str)
r := []rune(str)
for i := 0; i< len(r); i++ {
fmt.Printf("字符=%c \n", r[i])
}

字符串转整数

1
2
3
4
5
6
n, err := strconv.Atoi("hello")
if err != nil {
fmt.Println("转换错误", err)
}else{
fmt.Println("转成的结果是", n)
}

整数转字符串

1
2
str = strconv.Itoa(12345)
fmt.Printf("str=%v, str=%T \n", str, str)

字符串转[]byte

1
2
var bytes = []byte("Hello go")
fmt.Println("bytes=%v \n", bytes)

[]byte转字符串

1
2
str = string([]byte{12,34,56})
fmt.Printf("str=%v \n", str)

十进制转其它进制

1
2
3
4
str = strconv.FormatInt(123, 2)
fmt.Printf("123对应的二进制是:%v \n", str)
str = strconv.FormatInt(123, 16)
fmt.Printf("123对应的十六进制是:%v \n", str)

查找子串是否在指定的字符串中

1
2
b := strings.Contains("seafood", "foo")
fmt.Printf("b=%v \n", b)

统计子串个数

1
2
num := strings.Count("ceheese", "e")
fmt.Printf("num=%v \n", num)

不区分大小写的字符串比较

1
2
3
b = strings.EqualFold("abc", "Abc")
fmt.Printf("b=%v \n", b) // true
fmt.Println("结果", "abc" == "Abc") // false, 区分大小写

获取指定字符的索引

1
2
3
// 返回子串在字符串第一次出现的 index 值,如果没有返回-1
index := strings.Index("NLT_abcabcabc", "abc") //4
fmt.Printf("index=%v \n", index)
1
2
3
// 返回子串在字符串最后一次出现的 index,如没有返回-1
index := strings.LastIndex("go golang", "go") //3
fmt.Printf("index=%v \n", index)

字符串的替换

1
2
3
4
5
// strings.Replace("go go hello", "go", "go 语言", n) 
// n 可以指定你希望替换几个,如果 n=-1 表示全部替换
str1 = "go go hello"
str2 = strings.Replace(str1, "go", "北京", -1)
fmt.Printf("str1=%v str2=%v \n", str1, str2)

分隔字符串

1
2
3
4
5
6
// 按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组
strArr := strings.Split("Hello,world,ok", ",")
for i := 0; i < len(strArr); i++ {
fmt.Printf("str[%v]=%v \n", i, strArr[i])
}
fmt.Printf("strArr=%v \n", strArr)

大小写转换

1
2
3
4
str = "golang Hello"
str = strings.ToLower(str)
str = strings.toUpper(str)
fmt.Printf("str=%v \n", str) //golang hello

去除两边空格

1
2
str = strings.TrimSpace(" a b c d ")
fmt.Printf("str=%v \n", str)

去除两遍指定字符

1
2
3
4
str = strings.Trim("! a b c d! ", " !")
fmt.Printf("str=%v \n", str)
// 将字符串左边指定的字符去掉 : strings.TrimLeft("! hello! ", " !")
// 将字符串右边指定的字符去掉 :strings.TrimRight("! hello! ", " !")

判断字符串开头/结束内容

  • 判断字符串是否以指定的字符串开头: strings.HasPrefix(“ftp://192.168.10.1", “ftp”) //true
  • 判断字符串是否以指定的字符串结束: strings.HasSuffix(“NLT_abc.jpg”, “abc”) //false

时间和日期相关函数

在编程中,程序员会经常使用到日期相关的函数,比如:统计某段代码执行花费的时间等等。

  • 获取当前时间
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"time"
)

func main() {
//看看日期和时间相关函数和方法使用
//获取当前时间
now := time.Now()
fmt.Printf("now=%v now-type=%T \n", now, now)
}

结果为

image-20220729131319622
  • 获取其他日期信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"time"
)

func main() {
//看看日期和时间相关函数和方法使用
//获取当前时间
now := time.Now()
fmt.Printf("now=%v now-type=%T \n", now, now)

//通过now可以获取到年月日时分秒
fmt.Printf("年=%v \n", now.Year())
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())
}

结果为

image-20220729132021387

  • 时间格式化输出

Printf 或者 Sprintf

1
2
3
4
5
6
//格式化输出
//Printf
fmt.Printf("%d-%d-%d %d:%d:%d \n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
//SPrintf
date := fmt.Sprintf("%d-%d-%d %d:%d:%d \n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
fmt.Printf("date=%v", date)
image-20220729132844121

time.Format()

1
2
3
4
5
6
7
// Format(),里面必须写成 2006-01-02 15:04:05 !
fmt.Printf(now.Format("2006-01-02 15:04:05"))
fmt.Println()
fmt.Printf(now.Format("2006-01-02"))
fmt.Println()
fmt.Printf(now.Format("15:04:05"))
fmt.Println()
  • 时间的常量
1
2
3
4
5
6
7
8
const ( 
Nanosecond Duration = 1 //纳秒
Microsecond = 1000 * Nanosecond //微秒
Millisecond = 1000 * Microsecond //毫秒
Second = 1000 * Millisecond //秒
Minute = 60 * Second //分钟
Hour = 60 * Minute //小时
)

常量的作用:

在程序中可用于获取指定时间单位的时间,比如想得到 100 毫秒

1
100 * time.Millisecond

也可以结合Sleep使用

1
time.Sleep(time.Millisecond * 100)	//等待0.1秒
  • 时间戳
1
fmt.Printf("unix时间戳=%v, unixnano时间戳=%v \n", now.Unix(), now.UnixNano())

返回结果:

unix时间戳=1659074372, unixnano时间戳=1659074372349450000

内置函数

文档地址: https://studygolang.com/pkgdoc

  • len: 用来求长度,比如stringarrayslicemapchannel

  • new: 用来分配内存,主要用来分配值类型,比如intfloat32, struct…返回的是指针

new的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
num1 := 100
fmt.Printf("num1的类型为%T, num1的值为%v, num1的地址为%v \n", num1, num1, &num1)

num2 := new(int) //*int
//num2的类型%T => *int
//num2的值 = 系统分配的地址 0xc0000140a8
//num2的地址%v = 系统分配的地址 0xc00000e030
//num2指向的值 = 100
*num2 = 100
fmt.Printf("num2的类型=%T, num2的值=%v, num2的地址=%v, num2指针指向的值=%v \n", num2, num2, &num2, *num2)
}

结果为

num1的类型为int, num1的值为100, num1的地址为0xc0000140a0
num2的类型=*int, num2的值=0xc0000140a8, num2的地址=0xc00000e030, num2指针指向的值=100

  • make

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

错误处理

Go 中可以抛出一个panic 的异常,然后在 defer 中通过 recover捕获这个异常,然后正常处理

defer+recover 处理错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"time"
)

func test() {
//使用 defer + recover 来捕获和处理异常
defer func(){
err := recover() //recover()内置函数,可以捕获到异常
if err != nil { //说明捕获到错误
fmt.Println("err=", err)
//可以将错误信息通过邮箱发送给管理员
fmt.Println("发送邮件给管理员")
}
}()

num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("res=", res)
}

func main() {
test()
for {
fmt.Println("main()下面的代码...")
time.Sleep(time.Second)
}
}

image-20220729142547680

自定义错误

Go 程序中,也支持自定义错误, 使用 errors.New 和 panic 内置函数

1) errors.New(“错误说明”) , 会返回一个 error类型的值,表示一个错误
2) panic 内置函数 ,接收一个 interface{} 类型的值(也就是任何值了)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"errors"
"fmt"
)

//函数去读取配置文件init.conf的信息
//如果文件名传入不正确,我们就返回一个自定义的错误
func readConf(name string) (err error) {
if name == "config.ini" {
//读取...
return nil
} else {
//返回一个自定义错误
return errors.New("读取文件错误..")
}
}

func test02() {
err := readConf("config2.ini")
if err != nil {
//如果读取文件发送错误,panic就输出这个错误,并终止程序
panic(err)
}
fmt.Println("test02继续执行...")
}

func main() {
test02()
fmt.Println("main下面的代码")
}

image-20220729151859997

因为传入test02中的配置文件名为config2.ini,触发了if判断,所以panic已经输出并终止程序,所以不会看到下面的内容了

数组与切片

数组

数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型。

快速入门

  • 一个养鸡场有 6 只鸡,它们的体重分别是 3kg,5kg,1kg,3.4kg,2kg,50kg 。请问这六只鸡的总体重是
    多少?平均体重是多少? 请你编一个程序。使用数组解决!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func main() {
//1.定义一个数组
var hens [7]float64
//2.给数组的每个元素赋值,元素的下标是从0开始的 0-5
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 //增加一只鸡...鸡你太美
//3.遍历数组求出总体重
totalWeight2 := 0.0
for i := 0; i < len(hens); i++ {
totalWeight2 += hens[i]
}

//4.求出平均体重
avgWeight2 := fmt.Sprintf("%.2f", totalWeight2/float64(len(hens)))
fmt.Printf("totalWeight2=%v, avgWeight2=%v \n", totalWeight2, avgWeight2)
}

image-20220729160135788

数组定义与内存布局

  • 数组的定义

var 数组名 [数组大小]数据类型

example:

​ var a [5]int

  • 数组的内存布局(重要)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var intArr [3]int //int占8个字节
//当我们定义完数组后,其实数组的各个元素有默认值 0
fmt.Println(intArr)
intArr[0] = 10
intArr[1] = 20
intArr[2] = 30
fmt.Println(intArr)
fmt.Printf("intArr的地址=%p, intArr[0]地址=%p, intArr[1]地址=%p, intArr[2]地址=%p \n", &intArr, &intArr[0], &intArr[1], &intArr[2])
}

image-20220729162749243

对上图的总结:
1) 数组的地址可以通过数组名来获取 &intArr
2) 数组的第一个元素的地址,就是数组的首地址,后面依次加n个字节
3) 数组的各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8, int32->4…

数组的使用

  • 从终端循环输入5个成绩,保存到float64数组并输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {

var score [5]float64

for i := 0; i < len(score); i++ {
fmt.Printf("请输入第%d个元素的值 \n", i+1)
fmt.Scanln(&score[i])
}

//输出
for i := 0; i < len(score); i++ {
fmt.Printf("score[%d]=%v \n", i, score[i])
}
}

image-20220729165647184

  • 四种初始化数组的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {

//传统方式
var numArr01 [3]int = [3]int{1, 2, 3}
fmt.Println("numArr01=", numArr01)
//简化方式
var numArr02 = [3]int{5, 6, 7}
fmt.Println("numArr02=", numArr02)
//缺省方式
var numArr03 = [...]int{8, 9, 10}
fmt.Println("numArr03=", numArr03)
//缺省+索引方式
var numArr04 = [...]int{1: 800, 0: 900, 2: 999}
fmt.Println("numArr04=", numArr04)

//类型推导
strArr05 := [...]string{1: "tom", 0: "jack", 2: "mary"}
fmt.Println("strArr05=", strArr05)
}

image-20220729172625047

数组的遍历

常规遍历

见前面的数组的使用

for-range结构遍历

这是 Go 语言一种独有的结构,可以用来遍历访问数组的元素。

  • 语法结构

for index, value := range array01 {

​ …

}

  • 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
)

func main() {
heroes := [...]string{"宋江", "吴用", "卢俊义"}

for i, v := range heroes {
fmt.Printf("i=%v, v=%v \n", i, v)
fmt.Printf("heroes[%d]=%v \n", i, heroes[i])
}

for _, v := range heroes {
fmt.Printf("元素的值=%v \n", v)
}
}

数组的注意事项和细节

  • 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化
  • var arr []int 这时 arr 就是一个 slice 切片,切片后面专门讲解,不急哈.
  • 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。
  • 数组创建后,如果没有赋值,有默认值(零值)
    • 数值类型数组:默认值为 0
    • 字符串数组: 默认值为 “”
    • bool 数组: 默认值为 false
  • 使用数组的步骤
    • 1 声明数组并开辟空间
    • 2 给数组各个元素赋值(默认零值)
    • 3 使用数组
  • 数组的下标是从 0 开始的
  • 数组下标必须在指定范围内使用,否则报 panic:数组越界,比如 var arr [5]int 则有效下标为 0-4
  • Go的数组属值类型,在默认情况下是值传递,因此会进行值拷贝。数组间不会相互影响(就是说如果一个函数直接更改数组中的某一个值,只要不直接修改地址的值,原值就不会改变)
1
2
3
4
5
6
func test(arr [3]int) {
arr[0] = 88
}

arr := [3]int{11, 22, 33}
test(arr)
  • 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
1
2
3
4
5
6
func test(arr *[3]int) {
*arr[0] = 88
}

arr := [3]int{11, 22, 33}
test(&arr)
  • 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度

image-20220729174551635

数组应用案例

  • 案例一
    • 创建一个 byte 类型的 26 个元素的数组,分别 放置’A’-‘Z‘。使用 for 循环访问所有元素并打印
      出来。
    • 提示:字符数据运算 ‘A’+1 -> ‘B’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
)

func main() {
var myChars [26]byte
for i := 0; i < 26; i++ {
myChars[i] = 'A' + byte(i)
}

for i := 0; i < 26; i++ {
fmt.Printf("%c ", myChars[i])
}
}

image-20220730091542976

  • 案例二

    • 请求出一个数组的最大值,并得到对应的下标
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package main

    import "fmt"

    func main() {
    var intArr = [...]int{1, -1, 9, 90, 11, 33}
    maxVal := intArr[0]
    maxValIndex := 0

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

    image-20220730092529783

  • 案例三

    • 请求出一个数组的和和平均值。for-range
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var intArr = [...]int{1, -1, 9, 90, 11, 33}

//求数组的平均值
sum := 0
for _, value := range intArr {
sum += value
}

fmt.Printf("sum=%v, 平均值为=%v \n", sum, float64(sum)/float64(len(intArr)))
}
  • 案例四
    • 要求:随机生成五个数,并将其反转打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"math/rand"
"time"
)

//随机生成5个数,rand.Intn()函数
//当我们得到随机数后,就放到一个int数组中
//反转打印,交换的次数是 len / 2, 倒数第一个和第一个元素交换,倒数第二个和第二个元素交换

func main() {
var intArr [5]int
len := len(intArr)

rand.Seed(time.Now().UnixNano())
for i := 0; i < len; i++ {
intArr[i] = rand.Intn(100) // 0 <= n < 100
}

fmt.Println("交换前~=", intArr)
//反转打印,交换的次数是 len / 2
temp := 0
for i := 0; i < len/2; i++ {
temp = intArr[len-1-i]
intArr[len-1-i] = intArr[i]
intArr[i] = temp
}
fmt.Println("交换后~=", intArr)
}

image-20220730101927926

切片

先看一个需求:我们需要一个数组用于保存学生的成绩,但是学生的个数是不确定的,请问怎么办?解决方案 => 使用切片。

基本介绍

1) 切片的英文是 slice
2) 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
3) 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
4) 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
5) 切片定义的基本语法:var 切片名 []类型

快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
var intArr [5]int = [...]int{1, 22, 33, 66, 99}
//声明一个切片
//slice := intArr[1:3]
//1.slice就是切片名
//2.intArr[1:3] 表示 slice 引用到intArr这个数组
//3.引用intArr数组的起始下标为1,最后的下标为3
slice := intArr[1:3]
fmt.Println("intArr", intArr)
fmt.Println("slice 的元素是:", slice)
fmt.Println("slice的元素个数=", len(slice))
fmt.Println("slice 的容量=", cap(slice)) //切片的容量可以动态变化,根据目前切片的大小自动分配调整
}

image-20220730114248953

切片在内存中形式(重要)

为了让大家更加深入的理解切片,我们画图分析一下切片在内存中是如何布局的,这个是一个非常重要的知识点:(以前面的案例来分析)

  • 画出前面的切片内存布局

image-20220730132646416

总结:

  1. slice 是一个引用类型,改变切片中的内容,对应的数组内容也会发生改变,因为指向的是同一个地址
  2. slice 从底层来说,其实就是一个数据结构(struct 结构体)
1
2
3
4
5
type slice struct {
ptr *[2]int
len int
cap int
}

如果更改切片的内容,对应的数组内容也会发生改变,因为指向的是同一个地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
var intArr [5]int = [...]int{1, 22, 33, 66, 99}

//更改前
slice := intArr[1:3]
fmt.Println("更改前的intArr=", intArr)
fmt.Println("更改后的slice=", slice)

//更改后
slice[1] = 100 //将切片的第一个元素的值改为 100
fmt.Println("更改后的intArr=", intArr)
fmt.Println("更改后的slice=", slice)

}

image-20220730133506159

切片的使用

方式一

第一种方式:定义一个切片,然后让切片去引用一个已经创建好的数组,比如前面的案例就是这样的。

1
2
var intArr [5]int = [...]int{1, 22, 33, 66, 99}		//创建数组
slice := intArr[1:3] //切片引用

方式二

通过 make 来创建切片.
基本语法: var 切片名 []type = make([]type, len, [cap])

参数说明:

  • type: 就是==数据类型==
  • len : 大小
  • *cap : *指定切片容量(可选, 如果你分配了 cap,则要求 cap>=len)
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
var slice []float64 = make([]float64, 5, 10)
slice[1] = 10
slice[2] = 20

fmt.Println(slice)
fmt.Println("slice的size=", len(slice))
fmt.Println("slice的cap=", cap(slice))
}

image-20220730135011354

小结:

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

方式三

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

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {

var strSlice []string = []string{"tom", "jack", "mary"}
fmt.Println("strSlice=", strSlice)
fmt.Println("strSlice size=", len(strSlice))
fmt.Println("strSlice cap=", cap(strSlice))
}

image-20220730140105588

方式一与方式二的区别

image-20220730140158207

切片的遍历

切片的遍历和数组一样,也有两种方式:

  • for 循环常规方式遍历
  • for-range 结构遍历切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

func main() {
var arr [5]int = [...]int{10, 20, 30, 40, 50}
slice := arr[1:4]

//常规遍历
for i := 0; i < len(slice); i++ {
fmt.Printf("slice[%v]=%v \n", i, slice[i])
}

//for-range遍历
for i, v := range slice {
fmt.Printf("i=%v, v=%v \n", i, v)
}
}

image-20220730145058698

切片的注意事项和细节

1) 切片初始化时 var slice = arr[startIndex:endIndex]
说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])。
2) 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长.

  • var slice = arr[0:end] 可以简写 var slice = arr[:end]
  • var slice = arr[start:len(arr)] 可以简写: var slice = arr[start:]
  • var slice = arr[0:len(arr)] 可以简写: var slice = arr[:]
  1. cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素。

  2. 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用

  3. 切片可以继续切片

  4. 用 append 内置函数,可以对切片进行动态追加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
)

func main() {
var slice []int = []int{100, 200, 300}
//通过append直接给slice追加具体的元素
slice = append(slice, 400, 500, 600)
fmt.Println("slice=", slice)

//通过append将切片slice追加给slice,没错,我追加我自己
slice = append(slice, slice...)
fmt.Println("slice=", slice)
}

image-20220730151746567

  1. 切片的拷贝操作

切片使用 copy 内置函数完成 拷贝,举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
)

func main() {
var slice []int = []int{100, 200, 300}
var sliceCopy = make([]int, 10)
copy(sliceCopy, slice)
fmt.Println("sliceCopy=", sliceCopy)
fmt.Println("slice=", slice)
}

image-20220730152614994

  1. 切片是引用类型,所以在传递时,遵守引用传递机制。看两段代码,并分析底层原理

image-20220730152755854

string 和 slice

  • string底层是一个byte数组,因此string也可以进行切片处理案例演示:
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
//string底层是一个byte数组,因此string也可以进行切片处理
str := "hello@atguigu"
//使用切片获取到 atguigu
slice := str[6:]
fmt.Println("slice=", slice)
}

image-20220730153726089

  • string 和切片在内存的形式,以 “abcd” 画出内存示意图

image-20220730153855974

  • string 是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串
  • 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
//string底层是一个byte数组,因此string也可以进行切片处理
str := "hello@atguigu"

//字符串不能直接修改
arr1 := []byte(str)
arr1[0] = 'z'
str = string(arr1)
fmt.Println("str=", str)

//上面的写法不能赋值中文
//下面的可以
arr2 := []rune(str)
arr2[0] = '北'
str = string(arr2)
fmt.Println("str=", str)
}

排序和查找

冒泡排序

image-20220730165539742

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func BubbleSort(arr *[5]int) {
fmt.Println("排序前arr=", (*arr))
temp := 0

//冒泡排序
for i := 0; i < len(*arr)-1; i++ {
for j := 0; j < len(*arr)-1-i; j++ {
if (*arr)[j] > (*arr)[j+1] {
temp = (*arr)[j]
(*arr)[j] = (*arr)[j+1]
(*arr)[j+1] = temp
}
}
}
fmt.Println("排序后arr=", *arr)
}

func main() {
//定义数组
arr := [5]int{24, 69, 80, 57, 13}
//将数组传递给一个函数,完成排序
BubbleSort(&arr)

fmt.Println("main arr=", arr)
}

image-20220802115219645

查找

顺序查找

  • 案例

有一个数列:白眉鹰王、金毛狮王、紫衫龙王、青翼蝠王

猜数游戏:从键盘中任意输入一个名称,判断数列中是否包含此名称【顺序查找】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import "fmt"

func main() {
// 有一个数列:白眉鹰王、金毛狮王、紫衫龙王、青翼蝠王
// 猜数游戏:从键盘中任意输入一个名称,判断数列中是否包含此名称【顺序查找】

names := [4]string{"白眉鹰王", "金毛狮王", "紫衫龙王", "青翼蝠王"}
var heroName = ""
fmt.Println("请输入要查找的人名: ")
fmt.Scanln(&heroName)

//顺序查找: 方式一
// for i := 0; i < len(names); i++ {
// if heroName == names[i] {
// fmt.Printf("找到%v, 下标为%v \n", heroName, i)
// break
// } else if i == (len(names) - 1) {
// fmt.Printf("没有找到%v \n", heroName)
// }
// }

//顺序查找: 方式二(推荐)
index := -1

for i := 0; i < len(names); i++ {
if heroName == names[i] {
index = i //将找到的值对应的下标赋给 index
break
}
}

if index != -1 {
fmt.Printf("找到%v, 下标%v \n", heroName, index)
} else {
fmt.Println("没有找到", heroName)
}
}

image-20220802153817660

二分查找

二分查找就是查找每次将值与中位数进行对比,不断进行判断缩小范围

  • 思路分析

image-20220802154212526

  • 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

func BinaryFind(arr *[7]int, leftIndex int, rightIndex int, findVal int) {
// 判断 leftIndex 是否大于 rightIndex
if leftIndex > rightIndex {
fmt.Println("找不到")
return
}

// 先找到中间的下标
middle := (leftIndex + rightIndex) / 2

if (*arr)[middle] > findVal {
// 说明我们要查找的数,应该在 leftIndex - middle-1
BinaryFind(arr, leftIndex, middle-1, findVal)
} else if (*arr)[middle] < findVal {
// 说明我们要查找的数,应该在 middle+1 - rightIndex
BinaryFind(arr, middle+1, rightIndex, findVal)
} else {
// 找到了
fmt.Printf("找到了,下标为%v\n", middle)
}
}

func main() {
arr := [7]int{1, 8, 15, 39, 128, 333, 1111}
//BinaryFind(&arr, 0, len(arr)-1, 39)
BinaryFind(&arr, 0, len(arr)-1, 1112)
}

image-20220802161600143

二维数组

快速入门

  • 案例

image-20220802162123996

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
// 声明二维数组
var arr [4][6]int
//赋初值
arr[1][2] = 1
arr[2][1] = 2
arr[2][3] = 3

for i := 0; i < 4; i++ {
for j := 0; j < 6; j++ {
fmt.Print(arr[i][j], " ")
}
fmt.Println()
}
}

image-20220802162634448

使用方式

  • 方式一: 先声明再赋值

语法: var 数组名 [大小][大小]类型

比如: var arr [2][3]int , 再赋值。

使用演示

二维数组在内存的存在形式(重点)

image-20220802163333574
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//详细的解释一下上面的图片
//比如arr2[2]
//因为是arr2[2],所以就会在一个区域内划分出两个指针
//这两个指针分别对应第一行和第二行
//也就是说,arr2[0]的起始地址 = arr2[0][0]
//arr2[1]的起始地址 = arr2[1][0]
package main

import "fmt"

func main() {
// 声明二维数组
var arr [4][6]int
//赋初值
arr[1][2] = 1
arr[2][1] = 2
arr[2][3] = 3

for i := 0; i < 4; i++ {
for j := 0; j < 6; j++ {
fmt.Print(arr[i][j], " ")
}
fmt.Println()
}

var arr2 [2][3]int
fmt.Println(arr2)

fmt.Printf("arr2[0]的地址%p\n", &arr2[0])
fmt.Printf("arr[1]的地址%p\n", &arr2[1])

fmt.Printf("arr2[0][0]的地址%p\n", &arr2[0][0])
fmt.Printf("arr2[1][0]的地址%p\n", &arr2[1][0])
}
image-20220802173559774

从上图可以看出,arr2[0]arr2[1]两个地址相差了24个字节(底层都是使用16进制,满16进一位,150+24=168)

而且arr2[0]arr2[0][0]的地址是相同的

  • 方式二: 直接初始化

声明:

1
var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}

赋值(有默认值,比如 int 类型的就是 0)

1
2
var arr3 [2][3]int = [2][3]int{{1, 2, 3}, {4, 5, 6}}
fmt.Println("arr3=", arr3)

说明:

二维数组在声明时也对应有四种写法[和一维数组类似]

1
2
3
4
var 数组名 大小类型 = 大小类型{{初值..},{初值..}} 
var 数组名 大小类型 = ...类型{{初值..},{初值..}}
var 数组名 = 大小类型{{初值..},{初值..}}
var 数组名 = ...类型{{初值..},{初值..}}

二维数组的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
var arr = [2][3]int{{1, 2, 3}, {4, 5, 6}}

//普通遍历
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ {
fmt.Printf("%v\t", arr[i][j])
}
fmt.Println()
}

//for-range遍历二维数组
for i, v := range arr {
for j, v2 := range v {
fmt.Printf("arr[%v][%v]=%v \t", i, j, v2)
}
fmt.Println()
}
}

image-20220803152126615

map

map 是 key-value 数据结构,又称为字段或者关联数组。类似其它编程语言的集合,在编程中是经常使用到。

map的声明

基本语法

1
var map 变量名 map[keytype]valuetype
  • key 可以是什么类型

    golang 中的 map,的 key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只 包含前面几个类型的接口, 结构体, 数组

    通常 ==key== 为 intstring

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

  • valuetype 可以是什么类型

    valuetype 的类型和 key 基本一样,这里我就不再赘述了

    ==通常为:== 数字(整数,浮点数), string, map, struct

map 声明的举例

1
2
3
4
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string

注意: 声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用。

案例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var a map[string]string
//在使用map前,需要先make,make的作用就是给map分配数据空间
a = make(map[string]string, 10)
a["no1"] = "宋江"
a["no2"] = "吴用"
a["no1"] = "武松"
a["no3"] = "吴用"
fmt.Println(a)
}

对上面代码的说明:

1) map在使用前一定要make
2) map的key是不能重复,如果重复了,则以最后这个key-value为准
3) map的value是可以相同的.
4) map 的 key-value 是无序
5) make内置函数数目

map的使用

  • 方式一
1
2
3
4
5
6
7
8
9
10
11
//第一种方式
var a map[string]string
a = make(map[string]string, 10)
a["no1"] = "宋江"
a["no2"] = "吴用"
a["no1"] = "武松"
a["no3"] = "吴用"
fmt.Println(a)

//返回结果
map[no1:武松 no2:吴用 no3:吴用]
  • 方式二
1
2
3
4
5
6
7
8
9
//第二种方式
cities := make(map[string]string)
cities["no1"] = "北京"
cities["no2"] = "天津"
cities["no3"] = "上海"
fmt.Println(cities)

//返回结果
map[no1:北京 no2:天津 no3:上海]
  • 方式三
1
2
3
4
5
6
7
8
9
10
11
//第三种方式
heroes := map[string]string{
"hero1": "宋江",
"hero2": "卢俊义",
"hero3": "吴用",
}
heroes["heros"] = "林冲"
fmt.Println("heroes=", heroes)

//返回结果
heroes= map[hero1:宋江 hero2:卢俊义 hero3:吴用 heros:林冲]

案例

演示一个 key-value 的 value 是 map 的案例

比如:我们要存放 3 个学生信息, 每个学生有 name 和 sex 信息

思路: map[string]map[string]string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
studentMap := make(map[string]map[string]string)

studentMap["stu01"] = make(map[string]string, 3)
studentMap["stu01"]["name"] = "tom"
studentMap["stu01"]["sex"] = "男"
studentMap["stu01"]["address"] = "北京长安街~"

studentMap["stu02"] = make(map[string]string, 3)
studentMap["stu02"]["name"] = "mary"
studentMap["stu02"]["sex"] = "女"
studentMap["stu02"]["address"] = "上海黄浦江~"

fmt.Println(studentMap)
fmt.Println(studentMap["stu02"])
fmt.Println(studentMap["stu02"]["address"])
}

map的增删改查

map的增加和更新

map增加和更新:
map[“key”] = value //如果 key 还没有,就是增加,如果 key 存在就是修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
cities := make(map[string]string)
cities["no1"] = "北京"
cities["no2"] = "天津"
cities["no3"] = "上海"
fmt.Println(cities)

//因为 no3 这个key已经存在,因此下面的这句话就是修改
cities["no3"] = "上海~"
fmt.Println(cities)
}

image-20220804111447689

map的删除

说明:
delete(map,”key”) ,delete 是一个内置函数,如果 key 存在,就删除该 key-value,如果 key 不存在, 不操作,但是也不会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
cities := make(map[string]string)
cities["no1"] = "北京"
cities["no2"] = "天津"
cities["no3"] = "上海"
fmt.Println(cities)

//因为 no3 这个key已经存在,因此下面的这句话就是修改
cities["no3"] = "上海~"
fmt.Println(cities)

//演示删除
delete(cities, "no1")
fmt.Println(cities)
//当delete指定的key不存在时,删除不会操作,也不会报错
delete(cities, "no4")
fmt.Println(cities)
}

image-20220804140113501

细节说明

如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以遍历一下 key, 逐个删除

或者 map = make(...),make 一个新的,让原来的成为==垃圾==,被 gc 回收

  • 方式一: 遍历删除
1
2
3
for k, _ := range caties {
delete(cities, k)
}
  • 方式二: 重新make暴力删除
1
2
cities = make(map[string]string)
fmt.Println(cities)

map的查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import "fmt"

func main() {
cities := make(map[string]string)
cities["no1"] = "北京"
cities["no2"] = "天津"
cities["no3"] = "上海"
fmt.Println(cities)

//因为 no3 这个key已经存在,因此下面的这句话就是修改
cities["no3"] = "上海~"
fmt.Println(cities)

//演示删除
delete(cities, "no1")
fmt.Println(cities)
//当delete指定的key不存在时,删除不会操作,也不会报错
delete(cities, "no4")
fmt.Println(cities)

//演示map的查找
val, ok := cities["no1"]
if ok {
fmt.Printf("有no1 key 值为%v\n", val)
} else {
fmt.Printf("没有no1 key\n")
}

cities = make(map[string]string)
fmt.Println(cities)
}

image-20220804143801687

map的遍历

案例演示相对复杂的 map 遍历:该 map 的 value 又是一个 map

说明:map 的遍历使用 for-range 的结构遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
cities := make(map[string]string)
cities["no1"] = "北京"
cities["no2"] = "天津"
cities["no3"] = "上海"
fmt.Println(cities)

for k, v := range cities {
fmt.Printf("k=%v v=%v\n", k, v)
}
}

image-20220804151245088

遍历一个复杂的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func main() {
studentMap := make(map[string]map[string]string)

studentMap["stu01"] = make(map[string]string, 3)
studentMap["stu01"]["name"] = "tom"
studentMap["stu01"]["sex"] = "男"
studentMap["stu01"]["address"] = "北京长安街~"

studentMap["stu02"] = make(map[string]string, 3)
studentMap["stu02"]["name"] = "mary"
studentMap["stu02"]["sex"] = "女"
studentMap["stu02"]["address"] = "上海黄浦江~"

//for-range遍历一个结构复杂的map
for k1, v1 := range studentMap {
fmt.Println("k1=", k1)
for k2, v2 := range v1 {
fmt.Printf("\t k2=%v v2=%v \n", k2, v2)
}
fmt.Println()
}

// fmt.Println(studentMap)
// fmt.Println(studentMap["stu02"])
// fmt.Println(studentMap["stu02"]["address"])
}

image-20220804151146272

map切片

切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化了。

  • 案例

要求:使用一个 map 来记录 monster 的信息 name 和 age, 也就是说一个 monster 对应一个 map,并且妖怪的个数可以动态的增加=>map 切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

func main() {
//演示map切片的使用

//1.声明一个map切片
var monsters []map[string]string
monsters = make([]map[string]string, 2)
//2.增加一个妖怪的信息
if monsters[0] == nil {
monsters[0] = make(map[string]string)
monsters[0]["name"] = "牛魔王"
monsters[0]["age"] = "500"
}

if monsters[1] == nil {
monsters[1] = make(map[string]string, 2)
monsters[1]["name"] = "玉兔精"
monsters[1]["age"] = "400"
}

//下面这个写法越界
// if monsters[1] == nil {
// monsters[1] = make(map[string]string, 2)
// monsters[1]["name"] = "狐狸精"
// monsters[1]["age"] = "400"
// }

//这里我们需要使用到切片的append函数,可以动态的增加monster
//1.先定义一个monster信息
newMonster := map[string]string{
"name" : "新妖怪-火云邪神",
"age" : "200",
}
monsters = append(monsters, newMonster)

fmt.Println(monsters)
}

image-20220804155733247

map的排序

  • golang中没有一个专门的方法针对map的key进行排序
  • golang中的map默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样.
  • golang中map的排序,是先将key进行排序,然后根据key值遍历输出即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"sort"
)

func main() {
//map的排序
map1 := make(map[int]int, 10)
map1[10] = 100
map1[1] = 13
map1[4] = 56
map1[8] = 90

fmt.Println(map1)

//如果按照map的key的顺序进行排序输出
//1.先将map的key放入到切片中
//2.对切片排序
//3.遍历切片,然后按照key来输出map的值

var keys []int
for k, _ := range map1 {
keys = append(keys, k)
}

//排序
sort.Ints(keys)
fmt.Println(keys)

for _, k := range keys {
fmt.Printf("map1[%v]=%v \n", k, map1[k])
}
}

image-20220804162634091

map的使用细节

  • map是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的 map
  • map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长键值对(key-value)
  • map 的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面 value 是一个 map 更好),比如 value 为 Student 结构体

练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func modifyUser(users map[string]map[string]string, name string) {

//判断 users 中是否有 name //v , ok := users[name]
if users[name] != nil {
//有这个用户
users[name]["pwd"] = "888888"
} else {
//没有这个用户
users[name] = make(map[string]string, 2)
users[name]["pwd"] = "888888"
users[name]["nickname"] = "昵称~" + name //示意
}
}

func main() {
users := make(map[string]map[string]string, 10)
users["smith"] = make(map[string]string, 2)
users["smith"]["pwd"] = "999999"
users["smith"]["nickname"] = "小花猫"

modifyUser(users, "tom")
modifyUser(users, "mary")
modifyUser(users, "smith")

fmt.Println(users)
}

image-20220804165636270

面向对象编程

激动人心的面向对象编程他来喽~

说明

  • Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。

  • Golang没有(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。

  • Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等

  • Golang仍然有面向对象编程继承封装多态的特性,只是实现的方式和其它OOP语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。

  • Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(typesystem)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。

结构体

快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

func main() {

//定义一个Cat结构体,将Cat的各个字段/属性信息,放入到Cat结构体进行管理
type Cat struct {
Name string
Age int
Color string
Hobby string
}

//创建一个Cat的变量
var cat1 Cat
cat1.Name = "cat1"
cat1.Age = 3
cat1.Color = "white"
cat1.Hobby = "eat"
fmt.Println("cat1=", cat1)
fmt.Println("cat1的详细信息如下:")
fmt.Println("name=", cat1.Name)
fmt.Println("Age=", cat1.Age)
fmt.Println("Color=", cat1.Color)
fmt.Println("hobby=", cat1.Hobby)
}

image-20220805143305506

结构体的声明

结构体是一个值类型,并不是引用类型(引用类型指的是通过地址找到值),这一点需要注意

1
2
3
4
type 结构体名称 struct {
field1 type
field2 type
}

字段/属性

  • 基本介绍

1) 从概念或叫法上看: 结构体字段 = 属性 = field (即授课中,统一叫字段)
2) 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体的Name string 就是属性

注意事项和细节说明

  1. 字段声明语法同变量,示例:字段名 字段类型

  2. 字段的类型可以为: 基本类型数组引用类型

  3. 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:

    • 布尔类型是 false ,数值是 0 ,字符串是 ""

    • 数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]

    • 指针,slice,和 map 的零值都是 nil ,即还没有分配空间。

  • 演示案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

type Person struct {
Name string
Age int
scores [5]float64
ptr *int //指针
slice []int //切片
map1 map[string]string //map
}

func main() {
//定义结构体变量
var p1 Person
fmt.Println(p1)

if p1.ptr == nil {
fmt.Println("ptr is nil")
}

if p1.map1 == nil {
fmt.Println("map1 is nil")
}

if p1.slice == nil {
fmt.Println("slice is nil")
}

//使用slice,再次说明,一定要make
p1.slice = make([]int, 10)
p1.slice[0] = 100

//使用map,一定要先make
p1.map1 = make(map[string]string)
p1.map1["key1"] = "tom~"

fmt.Println(p1)
}

image-20220805150635987

  1. 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。

创建结构体变量和访问结构体字段

  • 方式一: 直接声明
1
2
var person Person
//上面已经演示过
  • 方式二: { }
1
2
3
p2 := Person{"poker", "26"}
//p2.Name = "tom"
//p2.Age = 26
  • 方式三: &
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//var person *Person = new (Person)

var p3 *Person = new (Person)
/*
因为p3是一个指针,因此标准的给字段赋值方式
(*p3).Name = "smith" 也可以这样写=> p3.Name = "smith"
原因: go的设计者,为了程序员使用方便,底层会对 p3.Name = "smith" 进行处理
会给 p3 加上取值运算 (*p3).Name = "smith"
*/
(*p3).Name= "smith"
p3.Name = "john"

(*p3).Age = 30
p3.Age = 100
fmt.Println(*p3)
  • 方式四: &{ }
1
2
3
4
5
6
7
8
9
//var person *Person = &Person{}

var p4 *Person = &Person{}
(*p4).Name = "scott"
p4.Name = "scott~"

(*p4).Age = 88
p4.Age = 10
fmt.Println(*p4)
  • 说明
    • 3 种和第 4 种方式返回的是结构体指针
    • 结构体指针访问字段的标准方式应该是: (*结构体指针).字段名 ,比如 (*person).Name = “tom”
    • 但 go 做了一个简化,也支持结构体指针.字段名, 比如 person.Name = “tom”。更加符合程序员使用的习惯,go 编译器底层对 person.Name 做了转化 (*person).Name。

struct 类型的内存分配机制

前面提到过,struct结构体的类型为值类型,当我们拷贝struct的一个实例时,会划分出一个新的地址空间存放值,所以两者不会影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

type Person struct {
Name string
Age int
scores [5]float64
ptr *int //指针
slice []int //切片
map1 map[string]string //map
}

func main() {
var p1 Person
p1.Name = "tom"
p1.Age = 10

//复制实例p1
var p2 Person = p1
fmt.Println("p2.Age=", p2.Age)
fmt.Println("p2.Name=", p2.Name)
//修改p2的值,观察p1的值是否会被修改
p2.Name = "jack"
fmt.Printf("p2.Name=%v, p1.Name=%v\n", p2.Name, p1.Name)
}

image-20220805171000477

从上面的结果来看,p1与p2根本毫无关联,谁修改谁都不会变化

那么如何做才能产生关联呢?请看下面的代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

type Person struct {
Name string
Age int
scores [5]float64
ptr *int //指针
slice []int //切片
map1 map[string]string //map
}

func main() {
var p1 Person
p1.Name = "tom"
p1.Age = 10

var p2 *Person = &p1
fmt.Println("p2.Age=", (*p2).Age)
fmt.Println("p2.Name=", p2.Name)
p2.Name = "jack"
fmt.Printf("p2.Name=%v, p1.Name=%v\n", p2.Name, p1.Name)
fmt.Printf("p1的地址为%p\n", &p1)
fmt.Printf("p2的地址为%p, p2的值为%p\n", &p2, p2)
}

image-20220805172148956

可以看到,p1.Namep2.Name同时发生了变化,因为两者指向了同一个内存地址

结构体的注意事项与细节说明

  1. 结构体的所有字段在内存中是连续的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import "fmt"

type Point struct {
x int
y int
}

//矩形的左上角和右下角,两个点的坐标
type Rect struct {
leftUp, rightDown Point
}

type Rect2 struct {
leftUp, rightDown *Point
}

func main() {
r1 := Rect{Point{1, 2}, Point{3, 4}}

//r1有四个int, 在内存中是连续分布
//打印地址
fmt.Printf("r1.ledtUp.x 地址=%p, r1.leftUp.y 地址=%p, r1.rightDown.x 地址=%p, r1.rr1.rightDown.yr1 地址=%p \n", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)

//r2有两个 *Point 类型, 这两个*Point类型的本身地址也是连续的
//但是他们指向的地址不一定是连续
r2 := Rect2{&Point{10, 20}, &Point{30, 40}}

fmt.Printf("r2.leftUp 本身地址=%p, r2.rightDown 本身地址=%p \n", &r2.leftUp, &r2.rightDown)
//他们指向的地址不一定是连续,这个要看系统在运行时是如何分配的
fmt.Printf("r2.leftUp 指向地址=%p, r2.rightDown 指向地址=%p \n", r2.leftUp, r2.rightDown)
}

image-20220805174144136

可以看到,上面结果中第一行和第二行的地址是连续的,但是第三行并不一定是连续的,a0和b0在十六进制上差了16个字节

  1. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type A struct {
Num int
}

type B struct {
Num int
}

func main() {
var a A
var b B
a = A(b) //强制转换类型,但是结构体的字段必须要保证一样(名字 个数 类型)
fmt.Println(a, b)
}

image-20220806112433067

  1. 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Student struct {
Name string
Age int
}

type Stu Student

func main() {
var s1 Student
var s2 Stu
//s1 = s2, 这是不对的
s2 = Stu(s1) //这样是正确的
fmt.Println(s1, s2)
}
  1. struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"encoding/json"
"fmt"
)

type Monster struct {
Name string `json:"name"` //打标签,注意冒号后面不要加空格
Age int `json:"age"`
Skill string `json:"skill"`
}

func main() {
monster := Monster{"牛魔王", 500, "芭蕉扇"}
//将monster变量序列化为 json 格式字符串
//json.Marshal 函数中使用反射,这个讲解反射时,再详细介绍
jsonStr, err := json.Marshal(monster)
if err != nil {
fmt.Println("json 处理错误", err)
}
fmt.Println("jsonStr", jsonStr) //输出byte
fmt.Println("jsonStr", string(jsonStr)) //byte转字符串
}

image-20220806140617419

方法

在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名..),Person 结构体还有一些行为比如:可以说话、跑步…, 通过学习,还可以做算术题。这时就要用方法才能完成。

Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。

小试牛刀

1
2
3
4
5
6
7
8
type A struct {
Num int
}

func (a A) test()
{
fmt.Println(a.Num)
}
  • 说明
    • func (a A) test() {} 表示 A 结构体有一方法,方法名为 test
    • (a A) 体现 test 方法是和 A 类型绑定的

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Person struct {
Name string
}

//给Person类型绑定一个方法
func (p Person) test() {
fmt.Println("test() name=", p.Name)
}

func main() {
var p Person
p.Name = "tom"
p.test() //调用方法
}

image-20220806144248481

  • 说明

1) test方法和Person类型绑定
2) test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
3) func (p Person) test() {}… p 表示哪个 Person 变量调用,这个 p 就是它的副本, 这点和函数传参非 常相似。
4) p 这个名字,有程序员指定,不是固定, 比如修改成 person 也是可以

方法快速入门

  • 给 Person 结构体添加 speak 方法,输出 xxx 是一个好人
1
2
3
func (p Person) speak() {
fmt.Println(p.Name, "是一个好人")
}
  • 给 Person 结构体添加 jisuan 方法,可以计算从 1+..+1000 的结果, 说明方法体内可以函数一样, 进行各种运算
1
2
3
4
5
6
7
func (p Person) jisuan() {
res := 0
for i := 1; i <= 1000; i++ {
res += i
}
fmt.Println(p.Name, "计算的结果是=", res)
}
  • 给 Person 结构体 jisuan2 方法,该方法可以接收一个数 n,计算从 1+…+n 的结果
1
2
3
4
5
6
7
func (p Person) jisuan2(n int) {
res := 0
for i := 1; i <= n; i++ {
res += i
}
fmt.Println(p.Name, "计算的结果是=", res)
}
  • 给 Person 结构体添加 getSum 方法,可以计算两个数的和,并返回结果
1
2
3
func (p Person) getSum(n1 int, n2 int) int {
return n1 + n2
}

main主体调用方法

1
2
3
4
5
6
7
8
9
func main() {
var p Person
p.Name = "tom"
p.test() //调用方法
p.jisuan()
p.jisuan2(50)
res := p.getSum(10, 20)
fmt.Println("res=", res)
}

image-20220808093800846

方法的调用和传参机制原理

方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。下面我们举例说明。

案例一

==画出前面 getSum 方法的执行过程+说明==

image-20220806173314395

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

案例二

请编写一个程序,要求如下:
1) 声明一个结构体 Circle, 字段为 radius
2) 声明一个方法 area 和 Circle 绑定,可以返回面积。
3) 提示: 画出 area 执行过程+说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.0
res := c.area()
fmt.Println("面积是=", res)
}

image-20220808094632707

方法的声明(定义)

1
2
3
4
func (recevier type) methodName(参数列表) (返回值列表) { 
方法体
return 返回值
}

参数说明:

1) 参数列表:表示方法输入
2) recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
3) receiver type : type 可以是结构体,也可以其它的自定义类型
4) receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
5) 返回值列表:表示返回的值,可以多个
6) 方法主体:表示为了实现某一功能代码块
7) return 语句不是必须的。

方法的注意事项和细节

  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  2. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理(这里可能不是太好理解)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

type Circle struct {
radius float64
}

//为了提高效率,通常我们方法和结构体的指针类型绑定
func (c *Circle) area2() float64 {
//因为c是指针,因此我们标准的访问其字段的方式是(*c)
//return 3.14 * (*c).radius * (*c).radius
// (*c).radius 等价 c.radius
c.radius = 10 //这里为指针,直接指向main中的c的地址,相当于直接将main中c.radius的地址对应的值给修改掉了
//下面使用 fmt 打印指针c的地址, 验证func方法中指针c是否指向main中c的地址
fmt.Printf("func方法中的c.radius =%v, c的地址为%p \n", c.radius, c) //这里取c的地址时,不需要加 & 符号,因为c本身就是一个指针
return 3.14 * c.radius * c.radius
}

func main() {
var c Circle
c.radius = 7.0 //此时会在内存中生成一个地址
//res2 := (&c).area2()
//编译器底层做了优化 (&c).area2() 等价 c.area()
//因为编译器会自动的给加上 &c
fmt.Printf("调用方法前, main中c.radius =%v, c的地址为%p \n", c.radius, &c)
res2 := c.area2() //调用方法
//当执行上面方法的时候,指针是指向方法中的c的地址,而不是main中的 c.radius = 7.0
fmt.Printf("调用方法后, main中c.radius =%v, c的地址为%p \n", c.radius, &c)
fmt.Println("面积是=", res2)
}

image-20220808111403865

image-20220808111435136

  1. Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法, 而不仅仅是 struct, 比如 int , float32 等都可以有方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type integer int

func (i integer) print() {
fmt.Println("i=", i)
}

//编写一个方法,可以改变i的值
func (i *integer) change() {
*i = *i + 1
}

func main() {
var i integer = 10
i.print()
i.change()
fmt.Println("i=", i)
}

image-20220808113804128

  1. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问; 方法首字母大写,可以在本包和其它包访问。
  2. 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

type Student struct {
Name string
Age int
}

//给 *String 实现方法String()
func (stu *Student) String() string {
str := fmt.Sprintf("Name=[%v], Age=[%v]", stu.Name, stu.Age)
return str
}

func main() {
//定义一个Student变量
stu := Student{
Name: "tom",
Age: 20,
}

//如果实现了 *Student 类型的 String 方法,就会自动调用
fmt.Println(&stu)
}

image-20220808115426710

方法练习

  • 编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个 10*8 的矩形,在 main 方法中调用该方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type MethodUtils struct {
//字段
}

func (mu MethodUtils) Print() {
for i := 1; i <= 10; i++ {
for j := 1; j <= 8; j++ {
fmt.Print("*")
}
fmt.Println()
}
}

func main() {
var mu MethodUtils
mu.Print()
}

image-20220808121630190

  • 编写一个方法,提供 m 和 n 两个参数,方法中打印一个 m*n 的矩形
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type MethodUtils struct {
//字段
}

func (mu MethodUtils) Print(m int, n int) {
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
fmt.Print("*")
}
fmt.Println()
}
}

func main() {
var mu MethodUtils
mu.Print(4, 10)
}
  • 定义小小计算器结构体(Calcuator),实现加减乘除四个功能 实现形式
    • 实现形式1: 分四个方法完成:
    • 实现形式2: 用一个方法搞定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"fmt"
)

type Calcuator struct {
Num1 float64
Num2 float64
}

//实现形式1
func (calcuator *Calcuator) getSum() float64 {
return calcuator.Num1 + calcuator.Num2
}

func (calcuator *Calcuator) getSub() float64 {
return calcuator.Num1 - calcuator.Num2
}

//实现形式2
func (calcuator *Calcuator) getRes(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() {
c := Calcuator {
Num1 : 10,
Num2 : 20,
}

//使用方式二计算乘法
res := c.getRes('*')
fmt.Println("res=", res)
}

image-20220808133851814

方法和函数区别

  • 调用方式不一样

    • 函数的调用方式: 函数名(实参列表)

    • 方法的调用方式: 变量.方法名(实参列表)

  • 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

type Person struct {
Name string
}

func test01(p Person) {
fmt.Println(p.Name)
}

func test02(p *Person) {
fmt.Println(p.Name)
}

func main() {
p := Person{"tom"}
test01(p)
test02(&p)
}

image-20220808135003870

  • 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
)

type Person struct {
Name string
}

func (p Person) test03() {
p.Name = "jack"
fmt.Println("test03()=", p.Name)
}

func (p *Person) test04() {
p.Name = "mary"
fmt.Println("test04()=", p.Name)
}

func main() {
p := Person{"tom"}
test01(p)
test02(&p)

p.test03()
fmt.Println("main() p.Name=", p.Name)
(&p).test03() //形式上是传入地址,但是本质仍然是值拷贝
fmt.Println("main() p.name=", p.Name)

(&p).test04()
fmt.Println("main() p.Name=", p.Name)
p.test04() //等价 (&p).test04, 从形式上是传入值类型,但是本质上仍然是地址拷贝
}

image-20220808140731655

总结:

1) 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
2) 如果是和值类型,比如 (p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则
是地址拷贝。

面向对象编程应用实例

步骤

1) 声明(定义)结构体,确定结构体名
2) 编写结构体的字段
3) 编写结构体的方法

学生案例

1) 编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、 int、float64 类型。
2) 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
3) 在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import "fmt"

/*
1) 编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、 int、float64 类型。
2) 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
3) 在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出
*/

type Student struct {
Name string
Gender string
Age int
Id int
Score float64
}

func (stu *Student) say() string {
res := fmt.Sprintf("Name=%v\nGender=%v\nAge=%v\nId=%v\nScore=%v", stu.Name, stu.Gender, stu.Age, stu.Id, stu.Score)
return res
}

func main() {
var stu = Student{
"poker",
"man",
26,
01,
99.99,
}

res := stu.say()
fmt.Println(res)
}

创建结构体变量时指定字段值

方式一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

type Stu struct {
Name string
Age int
}

func main() {

//方式一
//在创建结构体变量时,就直接指定字段的值
var stu1 = Stu{"小明", 19} //stu1, 结构体数据空间

stu2 := Stu{"小明", 20}

//在创建结构体变量时, 把字段名和字段值写在一起, 就不会依赖字段的定义顺序
var stu3 = Stu{
Name: "jack",
Age: 20,
}
stu4 := Stu{
Age: 30,
Name: "mary",
}

fmt.Println(stu1, stu2, stu3, stu4)
}

方式二

1
2
3
4
5
6
7
8
9
10
11
12
13
//方式二, 返回结构体的指针类型
var stu5 *Stu = &Stu{"小王", 29}
stu6 := &Stu{"小王", 39}

var stu7 = &Stu {
Name : "小李",
Age : 49,
}
stu8 := &Stu {
Age : 59,
Name : "小李~",
}
fmt.Println(*stu5, *stu6, *stu7, *stu8)

工厂模式

Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。

为什么会有工厂模式

例如现在有一个结构体

1
2
3
4
5
6
package model

type Student struct {
Name string
Age int
}

因为这个结构体的名称的首字母为大写,所以我们可以在外部进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"golang_study/object_oriented_programming/factory/model"
)


func main() {
var stu model.Student
stu.Name = "tom"
stu.Age = 26

fmt.Println(stu)
}

那么如果这个结构体的名称首字母为小写时,其他文件就没有办法进行引用

1
2
3
4
5
6
package model

type student struct { //s为小写,外部无法调用
Name string
Age int
}

所以就出现了”工厂模式”

工厂模式的使用方法

  • 案例

model外部模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package model

type student struct {
Name string
Score float64
}

//因为student结构体首字母是小写,因此只能在model使用
//我们通过工厂模式来解决

func NewStudent(n string, s float64) *student {
return &student{
Name: n,
Score: s,
}
}

main主函数进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"golang_study/object_oriented_programming/factory/model"
)

func main() {
var stu = model.NewStudent("tom", 99.8)

fmt.Println(*stu) //stu接受的是NewStudent返回的一个指针
fmt.Println("name", stu.Name, " score=", stu.Score)
}

image-20220809113632923

type student struct {
Name string
Score float64
}

如果这一段代码中的Score改为score的话,外部同样无法进行调用,这里我们应该如何处理呢?

model外部模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package model

type student struct {
Name string
score float64
}

//因为student结构体首字母是小写,因此只能在model使用
//我们通过工厂模式来解决

func NewStudent(n string, s float64) *student {
return &student{
Name: n,
score: s,
}
}

func (s *student) GetScore() float64 {
return s.score
}

main主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"golang_study/object_oriented_programming/factory/model"
)

func main() {
var stu = model.NewStudent("tom", 99.8)

fmt.Println(*stu) //stu接受的是NewStudent返回的一个指针
fmt.Println("name", stu.Name, " score=", stu.GetScore())
}

面向对象编程思想-抽象

我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。

案例

image-20220809135803801

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package main

import "fmt"

type Account struct {
AccountNo string
Pwd string
Balance float64
}

//方法
//存款
func (account *Account) SaveMoney(money float64, pwd string) {

//检查输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}

//价差输入的存款金额是否正确
if money <= 0 {
fmt.Println("输入的金额不正确!")
return
}

account.Balance += money
fmt.Println("存款成功")

}

//取款
func (account *Account) WithDraw(money float64, pwd string) {

//检查输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}

//价差输入的取款金额是否正确
if money <= 0 || money > account.Balance {
fmt.Println("输入的金额不正确!")
return
}

account.Balance -= money
fmt.Println("取款成功")
}

//查询
func (account *Account) QueryMoney(pwd string) {

if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
fmt.Printf("你的账号为%v 余额为%v \n", account.AccountNo, account.Balance)
}

func main() {
var account = Account{
AccountNo: "gs123456",
Pwd: "123456",
Balance: 100.0,
}

accountNo := ""
passwd := ""
fmt.Println("请输入账户: ")
fmt.Scanf("%v", &accountNo)
fmt.Println("请输入密码: ")
fmt.Scanf("%v", &passwd)

//创建变量 i ,当 i == 1 时,程序退出
i := 0

//相当于 while 循环
for {

//判断用户名与密码是否正确
if accountNo == account.AccountNo && passwd == account.Pwd {
operate := ""
fmt.Println("===============================")
fmt.Println("请选择您要进行的操作:[1|2|3|q] ")
fmt.Println("1. 查询余额\n2. 存款操作\n3. 取款操作\nq. 退出")
fmt.Println("===============================")
fmt.Scanf("%v", &operate)

switch operate {
case "1":
account.QueryMoney(account.Pwd)
case "2":
money := 0.00
fmt.Println("请输入存款金额: ")
fmt.Scanf("%v", &money)
account.SaveMoney(money, account.Pwd)
case "3":
money := 0.00
fmt.Println("请输入取款金额: ")
fmt.Scanf("%v", &money)
account.WithDraw(money, account.Pwd)
case "q":
fmt.Println("欢迎您下次光临~")
i = 1 //退出循环
default:
fmt.Println("请输入正确的选项!")
}
} else {
fmt.Println("您输入的账户或密码错误!")
break
}

if i == 1 {
break
}
}
}

测试

image-20220809154102839

面向对象编程三大特性-封装

Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,下面我们一一进行详细的讲解 Golang 的三大特性是如何实现的。

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

封装的好处

1) 隐藏实现细节
2) 提可以对数据进行验证,保证安全合理(Age)

如何实现封装

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

封装的步骤

1) 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
2) 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数。
3) 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值

1
2
3
4
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
//加入数据验证的业务逻辑
var.字段 = 参数
}
  1. 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
1
2
3
func (var 结构体类型名) GetXxx() {
return var.age;
}

快速入门案例

请大家看一个程序(person.go),不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证。设计: model 包(person.go) main 包(main.go 调用 Person 结构体)

model外部模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package model

import "fmt"

type person struct {
Name string
age int
salary float64
}

//写一个工厂模式的函数,相当于构造函数
func NewPerson(name string) *person {
return &person{
Name: name,
}
}

//为了访问age和sal 我们编写一对 Setxxx 的方法和Getxxx的方法
func (p *person) SetAge(age int) {
if age > 0 && age < 150 {
p.age = age
} else {
fmt.Println("年龄范围不正确!")
}
}

func (p *person) GetAge() int {
return p.age
}

func (p *person) SetSalary(salary float64) {
p.salary = salary
}

func (p *person) GetSalary() float64 {
return p.salary
}

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"golang_study/object_oriented_programming/FengZhuang/01/model"
)

func main() {
p := model.NewPerson("poker")
p.SetAge(26)
p.SetSalary(10000.0)
fmt.Println(p)
fmt.Println("Name=", p.Name, "age=", p.GetAge(), "salary=", p.GetSalary())
}

image-20220810121009618

练习案例

1) 创建程序,在 model 包中定义 Account 结构体:在 main 函数中体会 Golang 的封装性。
2) Account结构体要求具有字段:账号(长度在6-10之间)、余额(必须>20)、密码(必须是六
3) 通过 SetXxx 的方法给 Account 的字段赋值。(同学们自己完成
4) 在 main 函数中测试

model外部模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package model

import "fmt"

type account struct {
accountNo string
pwd string
balance float64
}

func NewAccount(accountNo string, pwd string, balance float64) *account {

//对输入的内容进行判断
if len(accountNo) < 6 || len(accountNo) > 10 {
fmt.Println("账号的长度不正确!")
return nil
}

if len(pwd) != 6 {
fmt.Println("账号的密码长度不正确!")
return nil
}

if balance < 20 {
fmt.Println("余额的数目不正确!")
}

return &account{
accountNo: accountNo,
pwd: pwd,
balance: balance,
}
}

//方法
//存款
func (account *account) SaveMoney(money float64, pwd string) {

//检查输入的密码是否正确
if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}

//价差输入的存款金额是否正确
if money <= 0 {
fmt.Println("输入的金额不正确!")
return
}

account.balance += money
fmt.Println("存款成功")

}

//取款
func (account *account) WithDraw(money float64, pwd string) {

//检查输入的密码是否正确
if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}

//价差输入的取款金额是否正确
if money <= 0 || money > account.balance {
fmt.Println("输入的金额不正确!")
return
}

account.balance -= money
fmt.Println("取款成功")
}

//查询
func (account *account) QueryMoney(pwd string) {

if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}
fmt.Printf("你的账号为%v 余额为%v \n", account.accountNo, account.balance)
}

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"golang_study/object_oriented_programming/FengZhuang/02/model"
)

func main() {
//创建一个 account 变量
account := model.NewAccount("jzh11111", "000000", 40)
if account != nil {
fmt.Println("创建成功=", account)
} else {
fmt.Println("创建失败")
}
}

image-20220810120940064

面向对象编程三大特性-继承

为什么使用继承

  • 通过一个案例来说明

model外部模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"golang_study/object_oriented_programming/JICheng/01/model"
)

func main() {
var pupil = &model.Pupil{
Name: "tom",
Age: 10,
}

pupil.Testing()
pupil.SetScore(90)
pupil.ShowInfo()

var graduate = &model.Graduate{
Name: "tom",
Age: 10,
}

graduate.Testing()
graduate.SetScore(90)
graduate.ShowInfo()
}

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package model

import "fmt"

//编写一个学生考试系统

//小学生
type Pupil struct {
Name string
Age int
Score int
}

//显示他的成绩
func (p *Pupil) ShowInfo() {
fmt.Printf("学生名=%v 年龄=%v 成绩=%v \n", p.Name, p.Age, p.Score)
}

func (p *Pupil) SetScore(score int) {
//业务判断
p.Score = score
}

func (p *Pupil) Testing() {
fmt.Println("小学生正在考试中...")
}

//大学生
type Graduate struct {
Name string
Age int
Score int
}

//显示他的成绩
func (p *Graduate) ShowInfo() {
fmt.Printf("学生名=%v 年龄=%v 成绩=%v \n", p.Name, p.Age, p.Score)
}

func (p *Graduate) SetScore(score int) {
//业务判断
p.Score = score
}

func (p *Graduate) Testing() {
fmt.Println("大学生正在考试中...")
}

image-20220810133941739

  • 对上面代码的总结

1) PupilGraduate 两个结构体的字段和方法几乎,但是我们却写了两份相同的代码,代码复用性不

2) 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展。
3) 解决方法: 通过继承方式来解决

继承的介绍

继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的Student),在该结构体中定义这些相同的属性和方法。

其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 Student 匿名结构体即可

image-20220810150710847

也就是说: 在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。

嵌套匿名结构体的基本语法

1
2
3
4
5
6
7
8
type Goods struct {
Name string
Price int
}

type Book struct {
Goods //这里就是嵌套匿名结构体 Goods Writer string
}

快速入门案例

model外部函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package model

import "fmt"

//编写一个学生考试系统
//实现继承
type Student struct {
Name string
Age int
Score int
}

//显示他的成绩
func (p *Student) ShowInfo() {
fmt.Printf("学生名=%v 年龄=%v 成绩=%v \n", p.Name, p.Age, p.Score)
}

func (p *Student) SetScore(score int) {
//业务判断
p.Score = score
}

//小学生
type Pupil struct {
Student
}

func (p *Pupil) Testing() {
fmt.Println("小学生正在考试中...")
}

//大学生
type Graduate struct {
Student
}

func (p *Graduate) Testing() {
fmt.Println("大学生正在考试中...")
}

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"golang_study/object_oriented_programming/JICheng/01/model"
)

func main() {

var pupil = &model.Pupil{}
pupil.Student.Name = "tom"
pupil.Student.Age = 10
pupil.Testing()
pupil.Student.SetScore(90)
pupil.Student.ShowInfo()

var graduate = &model.Graduate{}
graduate.Student.Name = "jack"
graduate.Student.Age = 11
graduate.Testing()
graduate.Student.SetScore(90)
graduate.Student.ShowInfo()
}

image-20220810152407939

继承的深入讨论

  • 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,
    都可以使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

type A struct {
Name string
age int
}

func (a *A) SayOk() {
fmt.Println("A SayOk", a.Name)
}

func (a *A) Hello() {
fmt.Println("A Hello", a.Name)
}

type B struct {
A
}

func main() {
var b B
b.A.Name = "tom"
b.A.age = 19
b.A.SayOk()
b.A.Hello()
}
  • 匿名结构体字段访问可以简化
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var b B
b.A.Name = "tom"
b.A.age = 19
b.A.SayOk()
b.A.Hello()

//上面的写法可以进行简化
b.Name = "jack"
b.age = 20
b.SayOk()
b.Hello()
}

对上面代码的总结:

(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找..如果都找不到就报错

  • 结构体匿名结构体相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import "fmt"

type A struct {
Name string
age int
}

func (a *A) SayOk() {
fmt.Println("A SayOk", a.Name)
}

func (a *A) Hello() {
fmt.Println("A Hello", a.Name)
}

type B struct {
A
}

func (a *B) SayOk() {
fmt.Println("B SayOk", a.Name)
}

func (a *B) Hello() {
fmt.Println("B Hello", a.Name)
}

func main() {
var b B
b.A.Name = "tom"
b.A.age = 19
b.A.SayOk()
b.A.Hello()

//测试就近原则
b.Name = "poker"
b.age = 26
b.SayOk()
b.Hello()
}

image-20220810171352523

  • 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import "fmt"

type A struct {
Name string
age int
}

func (a *A) SayOk() {
fmt.Println("A SayOk", a.Name)
}

func (a *A) Hello() {
fmt.Println("A Hello", a.Name)
}

type B struct {
Name string
score float64
}

func (a *B) SayOk() {
fmt.Println("B SayOk", a.Name)
}

func (a *B) Hello() {
fmt.Println("B Hello", a.Name)
}

//加入一个新的结构体
type C struct {
A
B
}

func main() {

var c C
c.A.Name = "jenny"
c.B.Name = "john"
//c.Name = "test" //错误写法
fmt.Println(c)
}

image-20220810172107115

  • 如果一个struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import "fmt"

type A struct {
Name string
age int
}

func (a *A) SayOk() {
fmt.Println("A SayOk", a.Name)
}

func (a *A) Hello() {
fmt.Println("A Hello", a.Name)
}

type B struct {
Name string
score float64
}

func (a *B) SayOk() {
fmt.Println("B SayOk", a.Name)
}

func (a *B) Hello() {
fmt.Println("B Hello", a.Name)
}

type D struct {
a A
}

//注意下面的写法
func main() {
var d D
d.a.Name = "tom"
}
  • 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import "fmt"

type A struct {
Name string
age int
}

func (a *A) SayOk() {
fmt.Println("A SayOk", a.Name)
}

func (a *A) Hello() {
fmt.Println("A Hello", a.Name)
}

type B struct {
Name string
score float64
}

func (a *B) SayOk() {
fmt.Println("B SayOk", a.Name)
}

func (a *B) Hello() {
fmt.Println("B Hello", a.Name)
}

type C struct {
A
B
}

func main() {
//方式一
c1 := C{A{"tom", 10}, B{"jack", 100.0}}
//方式二: 不需要担心顺序问题
c2 := C{
A{
Name: "tom1",
age: 20,
},
B{
Name: "jack1",
score: 99.0,
},
}

fmt.Println("c1=", c1)
fmt.Println("c2=", c2)
}

image-20220810175137486

  • 下面这段代码,会输出什么结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Monster struct {
Name string
Age int
}

type E struct {
Monster
int
n int
}

func main() {
var e E
e.Name = "kkk"
e.Age = 300
e.int = 20
e.n = 40
fmt.Println("e=", e)
}

image-20220810175650660

说明:

1) 如果一个结构体有 int 类型的匿名字段,就不能第二个
2) 如果需要有多个 int 的字段,则必须给 int 字段指定名字

面向对象编程三大特性-多重继承

多重继承其实在前面的案例中已经体现过了

如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。

  • 案例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type A struct {
Name string
age int
}

func (a *A) SayOk() {
fmt.Println("A SayOk", a.Name)
}

func (a *A) Hello() {
fmt.Println("A Hello", a.Name)
}

type B struct {
Name string
score float64
}

func (a *B) SayOk() {
fmt.Println("B SayOk", a.Name)
}

func (a *B) Hello() {
fmt.Println("B Hello", a.Name)
}

//下面这就是多重继承
type C struct {
A
B
}

接口

按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在 Golang 中多态特性主要是通过接口来体现的。

接口快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import "fmt"

//定义一个接口
type Usb interface {
//声明两个没有实现的方法
Start()
Stop()
}

type Phone struct{}

//让 Phone实现 Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作...")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作...")
}

type Camera struct{}

//让 Camera 实现 Usb接口的方法
func (c Camera) Start() {
fmt.Println("相机开始工作...")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作...")
}

type Computer struct{}

func (c Computer) Working(usb Usb) {
usb.Start()
usb.Stop()
}

func main() {
computer := Computer{}
phone := Phone{}
camera := Camera{}

computer.Working(phone)
computer.Working(camera)
}

image-20220811144826030

理解接口是什么东西了吗?

我的理解就是,如果一个结构体想要用其它结构体的函数,就需要声明一个接口,接口中写入跟被使用结构体同名的函数名称,然后在目前结构体方法中声明接口的一个实例,然后使用该实例来调用被使用结构体方法,可能理解起来比较抽象

interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)

基本语法

image-20220811151327049

小结说明

1) 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
2) Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字

注意事项和细节

  1. 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type Interface interface {
SayOk()
}

type Stu struct {
Name string
}

func (s Stu) SayOk() {
fmt.Println("Stu SayOk()")
}

func main() {
var stu Stu
var a Interface = stu //相当与stu实例将它的 SayOk 方法的具体实现,传递给了接口类型变量 a
a.SayOk() //然后 a 就可以自由使用 SayOk 方法了
}
  1. 接口中所有的方法都没有方法体,即都是没有实现的方法

  2. 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。

  3. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型

  4. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
)

type Interface interface {
SayOk()
}

type integer int

func (i integer) SayOk() {
fmt.Println("integer SayOk i =", i)
}

func main() {
var i integer = 10
var b Interface = i
b.SayOk()
}
  1. 一个自定义类型可以实现多个接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
)

type InterfaceA interface {
SayOk()
}

type InterfaceB interface {
SayHello()
}

type Stu struct {
Name string
}

func (s Stu) SayOk() {
fmt.Println("Stu SayOk()")
}

func (s Stu) SayHello() {
fmt.Println("Stu SayHello()")
}

func main() {
var stu Stu
var a InterfaceA = stu
var b InterfaceB = stu
a.SayOk()
b.SayHello()
}
  1. Golang接口中不能有任何变量

  2. 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法也全部实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
)

type InterfaceA interface {
SayA()
}

type InterfaceB interface {
SayB()
}

type InterfaceC interface {
InterfaceA
InterfaceB
SayC()
}

//如果需要实现InterfaceA, 就需要将 InterfaceB 与 InterfaceC 的方法都实现
type Stu struct {
Name string
}

func (s Stu) SayA() {
fmt.Println("Stu SayA()")
}

func (s Stu) SayB() {
fmt.Println("Stu SayB()")
}

func (s Stu) SayC() {
fmt.Println("Stu SayC()")
}

func main() {
var stu Stu
var a InterfaceA = stu
a.SayA()
}
  1. interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil

  2. 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口。

接口编程的最佳实践

实现对 Hero 结构体切片的排序: sort.Sort(data Interface)

根据官网提供的sort方法我们可以知道:

1.接口类型已经在Sort包中定义好了

2.调用接口中抽象方法的函数已经在Sort自己的包中定义好了(sort.Sort(接口实例)这个就是)

3.需要我们自己实现 Len() Less() Swap() 三个方法来实现Sort接口中的抽象类

4.我们只需要直接使用 sort.Sort() 传入自己结构体的实例即可

官网链接: https://pkg.go.dev/sort#Interface

image-20220812150811059

image-20220812142509163

https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/sort/sort.go;l=14

下面是官网 sort.Sort() 中的一个例子,我们可以进行套用

image-20220812143608564

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import (
"fmt"
"math/rand"
"sort"
)

type Hero struct {
Name string
Age int
}

//声明一个数组结构体
type HeroSlice []Hero

func (x HeroSlice) Len() int {
return len(x)
}

func (x HeroSlice) Less(i, j int) bool {
//按照Age进行升序排序
return x[i].Age < x[j].Age //这里小于号当做是升序排列,大于号当做是降序排列
//按照Name进行升序排序
//return x[i].Name < x[j].Name
}

func (x HeroSlice) Swap(i, j int) {
x[i], x[j] = x[j], x[i]
}

func main() {
var heroes HeroSlice //HeroSlice 是一个数组结构体

//循环进行初始化赋值操作
for i := 0; i < 10; i++ {
hero := Hero{
Name: fmt.Sprintf("英雄|%d", rand.Intn(100)),
Age: rand.Intn(100),
}
//将初始化好的新一条数据加入到heroes数组中
heroes = append(heroes, hero)
}

fmt.Println("对Age字段排序前")
for _, v := range heroes {
fmt.Println("Name=", v.Name, "Age=", v.Age)
}

//对数组 heroes 进行排序
sort.Sort(heroes)

fmt.Println("对Age字段排序后")
for _, v := range heroes {
fmt.Println("Name=", v.Name, "Age=", v.Age)
}

}

image-20220812150502416

实现接口 vs 继承

现在有一只猴子,它只会爬树,但是他想学会飞翔和游泳

请使用接口实现这个案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import "fmt"

type Skill interface {
Flying()
Swimming()
}

//基础结构体
type Monkey struct {
Name string
}

func (m *Monkey) climbing() {
fmt.Println(m.Name, "猴子生来会爬树...")
}

//实现抽象方法
type SuperMonkey struct {
Monkey //继承
}

func (s *SuperMonkey) Flying() {
fmt.Println("你现在可以飞了!")
}

func (s *SuperMonkey) Swimming() {
fmt.Println("你现在可以游泳了!")
}

//继承Monkey
type LittleMonkey struct {
Monkey //继承
}

//调用接口的方法
func (l *LittleMonkey) getSkill(skill Skill) {
skill.Flying()
skill.Swimming()
}

func main() {
monkey := LittleMonkey{Monkey{"悟空"}}
superMonkey := SuperMonkey{Monkey{"卡卡罗特"}}
monkey.climbing()
monkey.getSkill(&superMonkey)
}

image-20220812173524727

说明:

  • 继承的价值主要在于:解决代码的复用性和可维护性。
  • 接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。

面向对象编程-多态

变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。

快速入门

在前面的 Usb 接口案例,Usb usb ,既可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口多态特性

详情参考本文:接口快速入门

接口体现多态的两种形式

  • 多态参数

在前面的 Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口多态。

  • 多态数组

演示一个案例:给 Usb 数组中,存放 Phone 结构体 和 Camera 结构体变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import "fmt"

//定义一个接口
type Usb interface {
//声明两个没有实现的方法
Start()
Stop()
}

type Phone struct {
name string
}

//让 Phone实现 Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作...")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作...")
}

type Camera struct {
name string
}

//让 Camera 实现 Usb接口的方法
func (c Camera) Start() {
fmt.Println("相机开始工作...")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作...")
}

type Computer struct{}

func (c Computer) Working(usb Usb) {
usb.Start()
usb.Stop()
}

func main() {

//定义一个Usb接口数组,可以存放 Phone 和 Camera 的结构体变量
//这里就体现出多态数组
var arrUsb [3]Usb
arrUsb[0] = Phone{"iphone"}
arrUsb[1] = Phone{"vivo"}
arrUsb[2] = Camera{"尼康"}

fmt.Println(arrUsb)
}

类型断言

image-20220813103548917

基本介绍

类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
var x interface{}
var b2 float32 = 1.1
x = b2 //空接口,可以接收任何类型
y := x.(float32)
fmt.Printf("y的类型是 %T, 值是 %v\n", y, y)
}

image-20220813104344257

说明:

  • 在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型
  • 如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
var x interface{}
var b2 float32 = 6.5
x = b2 //空接口,可以接收任何类型

//类型断言(带检测)
if y, ok := x.(float32); ok {
fmt.Println("convert success")
fmt.Printf("y的类型是 %T , 值是=%v\n", y, y)
} else {
fmt.Println("convert fail")
}
fmt.Println("继续执行...")
}

image-20220813105121739

最佳实践一

在前面的 Usb 接口案例做改进:
给 Phone 结构体增加一个特有的方法 call(), 当 Usb 接口接收的是 Phone 变量时,还需要调用 call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import "fmt"

//定义一个接口
type Usb interface {
//声明两个没有实现的方法
Start()
Stop()
}

type Phone struct {
name string
}

//让 Phone实现 Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作...")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作...")
}

//只有Phone拥有此方法
func (p Phone) Call() {
fmt.Println("手机正在打电话")
}

type Camera struct {
name string
}

//让 Camera 实现 Usb接口的方法
func (c Camera) Start() {
fmt.Println("相机开始工作...")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作...")
}

type Computer struct{}

func (c Computer) Working(usb Usb) {
usb.Start()
//如果 usb 是指向 Phone 结构体变量, 则还需要调用Call方法
if phone, ok := usb.(Phone); ok {
phone.Call()
}

usb.Stop()
}

func main() {

//定义一个Usb接口数组,可以存放 Phone 和 Camera 的结构体变量
//这里就体现出多态数组
var arrUsb [3]Usb
arrUsb[0] = Phone{"iphone"}
arrUsb[1] = Phone{"vivo"}
arrUsb[2] = Camera{"尼康"}

//遍历 arrUsb 数组
var computer Computer
for _, v := range arrUsb {
computer.Working(v)
fmt.Println()
}
}

image-20220813133022782

最佳实践二

写一函数,循环判断传入参数的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "fmt"

func TypeJudge(items ...interface{}) {
for index, x := range items {
switch x.(type) {
case bool:
fmt.Printf("第%v个参数是 bool 类型, 值是%v\n", index, x)
case float32:
fmt.Printf("第%v个参数是 float32 类型, 值是%v\n", index, x)
case float64:
fmt.Printf("第%v个参数是 float64 类型, 值是%v\n", index, x)
case int, int32, int64:
fmt.Printf("第%v个参数是 整数类型, 值是%v\n", index, x)
case string:
fmt.Printf("第%v个参数是 string 类型, 值是%v\n", index, x)
default:
fmt.Printf("第%v个参数是 不确定的类型, 值是%v\n", index, x)

}
}
}

func main() {
var v1 float32 = 1.1
var v2 float64 = 1.2
var v3 int32 = 30
var name string = "tom"
address := "北京"
v4 := 300

TypeJudge(v1, v2, v3, name, address, v4)
}

image-20220813134806710

项目

家庭收支记账软件项目

项目开发流程说明

image-20220815170739823

项目需求说明

1) 模拟实现基于文本界面的《家庭记账软件》
2) 该软件能够记录家庭的收入、支出,并能够打印收支明细表

项目代码实现-面向过程

目前只实现其过程,等完成这个项目需求之后,我们再将其改为面向对象方式实现

目前只在一个main.go中实现功能

功能一:实现主菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

func main() {

//key接收用户输入的操作
key := ""
//loop控制是否退出
loop := true

//显示主菜单
for {
fmt.Println("----------------家庭收支记账软件----------------")
fmt.Println("1. 收支明细")
fmt.Println("2. 登记收入")
fmt.Println("3. 登记支出")
fmt.Println("q. 退出软件")

fmt.Println("请选择1|2|3|q: ")
fmt.Scanln(&key)

switch key {
case "1":
fmt.Println("----------------当前收支明细记录----------------")
case "2":
fmt.Println("登记收入")
case "3":
fmt.Println("登记支出")
case "q":
loop = false
default:
fmt.Println("请输入正确的选项!")
}
if !loop {
break
}
}
fmt.Println("您已退出[家庭记账软件]")
}

image-20220815174102141

功能二:显示明细和登记收入

  • 定义变量来记录余额(balance)、每次收支的金额(money), 每次收支的说明(note)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//key接收用户输入的操作
key := ""
//loop控制是否退出
loop := true

//定义账户的余额
balance := 10000.0
//每次每次收支的金额
money := 0.0
//每次收支的说明
note := ""

//收支的详情使用字符串来记录
//当有收支时,只需要对details进行拼接处理即可
title := "收支\t账户金额\t收支金额\t说 明" //收支明细的标题
details := ""
  • 收入明细实现
1
2
3
4
5
6
7
8
9
10
11
12
switch key {
case "1":
fmt.Println("----------------当前收支明细记录----------------")
fmt.Println(details)
case "2":
fmt.Println("请输入收入金额:")
fmt.Scanln(&money)
balance += money
fmt.Println("本次收入说明:")
fmt.Scanln(&note)
//将这个收入情况拼接到details变量
details += fmt.Sprintf("\n收入\t%v\t\t%v\t\t%v", balance, money, note)

功能三:登记支出的功能

1
2
3
4
5
6
7
8
9
10
11
12
case "3":
fmt.Println("请输入支出金额:")
fmt.Scanln(&money)
if money > balance {
fmt.Println("余额不足")
break
}
balance -= money
fmt.Println("本次支出说明:")
fmt.Scanln(&note)
//将这个收入情况拼接到details变量
details += fmt.Sprintf("\n支出\t%v\t\t%v\t\t%v", balance, money, note)

image-20220815180202077

代码改进

  • 用户输入 q退出时,给出提示确定要退出吗?(y/n),必须输入正确的 y/n ,否则循环输入指令,直到输入 y 或者 n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
case "q":
exit_key := ""
fmt.Println("确定要退出吗?(y/n)")
fmt.Scanln(&exit_key)
for {
if exit_key == "y" {
loop = false
break
} else if exit_key == "n" {
break
} else {
fmt.Println("你的输入有误,请重新输入!(y/n)")
}
}
  • 当没有任何收支明细时,提示 当前没有收支明细... 来一笔吧!

下面是完整的一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package main

import "fmt"

func main() {

//key接收用户输入的操作
key := ""
//loop控制是否退出
loop := true

//定义账户的余额
balance := 10000.0
//每次每次收支的金额
money := 0.0
//每次收支的说明
note := ""

//收支的详情使用字符串来记录
//当有收支时,只需要对details进行拼接处理即可
title := "收支\t账户金额\t收支金额\t说 明" //收支明细的标题
details := ""

//显示主菜单
for {
fmt.Println("----------------家庭收支记账软件----------------")
fmt.Println("1. 收支明细")
fmt.Println("2. 登记收入")
fmt.Println("3. 登记支出")
fmt.Println("q. 退出软件")

fmt.Println("请选择1|2|3|q: ")
fmt.Scanln(&key)

switch key {
case "1":
fmt.Println("----------------当前收支明细记录----------------")
if details == "" {
fmt.Println("当前没有收支明细... 来一笔吧!")
} else {
fmt.Println(title)
fmt.Println(details)
}
case "2":
fmt.Println("请输入收入金额:")
fmt.Scanln(&money)
balance += money
fmt.Println("本次收入说明:")
fmt.Scanln(&note)
//将这个收入情况拼接到details变量
details += fmt.Sprintf("收入\t%v\t\t%v\t\t%v", balance, money, note)

case "3":
fmt.Println("请输入支出金额:")
fmt.Scanln(&money)
if money > balance {
fmt.Println("余额不足")
break
}
balance -= money
fmt.Println("本次支出说明:")
fmt.Scanln(&note)
//将这个收入情况拼接到details变量
details += fmt.Sprintf("支出\t%v\t\t%v\t\t%v", balance, money, note)
case "q":
exit_key := ""
fmt.Println("确定要退出吗?(y/n)")
fmt.Scanln(&exit_key)
for {
if exit_key == "y" {
loop = false
break
} else if exit_key == "n" {
break
} else {
fmt.Println("你的输入有误,请重新输入!(y/n)")
}
}
default:
fmt.Println("请输入正确的选项!")
}
if !loop {
break
}
}
fmt.Println("您已退出[家庭记账软件]")
}

项目代码实现-面向对象

实现思路:

  1. 更改上面面向过程的代码来实现
  2. 创建新的结构体,将各个功能分割为该结构体的方法
  3. 将各个方法存放到各自单独的文件中,由main主函数进行调用
  • 总体代码

自行将不同的部分放入不同的文件中即可(需要自己引入包),也可以都放入main.go中执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package main

import "fmt"

//model模块
type FamilyAccount struct {
key string //key接收用户输入的操作
loop bool //loop控制是否退出
balance float64 //定义账户的余额
money float64 //每次每次收支的金额
note string //每次收支的说明
//收支的详情使用字符串来记录
title string //收支明细的标题
details string //当有收支时,只需要对details进行拼接处理即可
}

func NewFmailyAccount() *FamilyAccount {
return &FamilyAccount{
key: "",
loop: true,
balance: 10000.0,
money: 0.0,
note: "",
title: "收支\t账户金额\t收支金额\t说 明",
details: "",
}
}

func (f *FamilyAccount) showDetails() {
fmt.Println("----------------当前收支明细记录----------------")
if f.details == "" {
fmt.Println("当前没有收支明细... 来一笔吧!")
} else {
fmt.Print(f.title)
fmt.Println(f.details)
}
}

func (f *FamilyAccount) income() {
fmt.Println("请输入收入金额:")
fmt.Scanln(&f.money)
f.balance += f.money
fmt.Println("本次收入说明:")
fmt.Scanln(&f.note)
//将这个收入情况拼接到details变量
f.details += fmt.Sprintf("\n收入\t%v\t\t%v\t\t%v", f.balance, f.money, f.note)
}

func (f *FamilyAccount) pay() {
flag := true //判断余额是否足够
fmt.Println("请输入支出金额:")
fmt.Scanln(&f.money)
if f.money > f.balance {
fmt.Println("余额不足")
flag = false
}
//如果余额不足,则跳过下面的步骤
if flag == true {
f.balance -= f.money
fmt.Println("本次支出说明:")
fmt.Scanln(&f.note)
//将这个收入情况拼接到details变量
f.details += fmt.Sprintf("\n支出\t%v\t\t%v\t\t%v", f.balance, f.money, f.note)
}
}

func (f *FamilyAccount) exit() {
exit_key := ""
fmt.Println("确定要退出吗?(y/n)")
fmt.Scanln(&exit_key)
for {
if exit_key == "y" {
f.loop = false
break
} else if exit_key == "n" {
break
} else {
fmt.Println("你的输入有误,请重新输入!(y/n)")
}
}
}

func (f *FamilyAccount) MainMenu() {
for {
fmt.Println("----------------家庭收支记账软件----------------")
fmt.Println("1. 收支明细")
fmt.Println("2. 登记收入")
fmt.Println("3. 登记支出")
fmt.Println("q. 退出软件")

fmt.Println("请选择1|2|3|q: ")
fmt.Scanln(&f.key)

switch f.key {
case "1":
f.showDetails()
case "2":
f.income()
case "3":
f.pay()
case "q":
f.exit()
default:
fmt.Println("请输入正确的选项!")
}
if !f.loop {
break
}
}
}

//主函数
func main() {

var p1 = NewFmailyAccount()

p1.MainMenu()
}

客户信息关系系统项目

项目需求分析

1) 模拟实现基于文本界面的《客户信息管理软件》。
2) 该软件能够实现对客户对象的插入、修改和删除(用切片实现),并能够打印客户明细表

项目界面设计

  • 主菜单界面

image-20220816114416811

  • 添加客户界面

image-20220816114459544

  • 修改客户界面

image-20220816114535614

  • 删除客户界面

image-20220816114614195

  • 客户列表界面

image-20220816114645413

客户关系管理系统的程序框架图

image-20220816151616530

功能实现

显示主菜单和退出软件功能

  • model/customer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package model

//声明一个customer结构体,表示一个客户信息
type Customer struct {
Id int
Name string
Gender string
Age int
Phone string
Email string
}

//使用工程模式,返回一个Customer的实例
func NewCustomer(id int, name string, gender string, age int, phone string, email string) Customer {
return Customer{
Id: id,
Name: name,
Gender: gender,
Age: age,
Phone: phone,
Email: email,
}
}
  • service/customerService.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package service

import (
"golang_study/object_oriented_programming/KeHuGuanXiGuanLiXiTong/model"
)

//该 customerService,完成对 customer 的操作,包括
//增删改查

type customerService struct {
customers []model.Customer
//声明一个字段,表示当前切片含有多少个客户
//该字段后面,还可以作为新客户的 id+1
customerNum int
}
  • view/customerView.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import "fmt"

type CustomerView struct {
//定义必要字段
key string //接收用户信息
loop bool //表示是否循环显示主菜单
}

//显示主菜单
func (c *customerView) mainMenu() {
for {
fmt.Println("-----------------客户信息管理软件-----------------")
fmt.Println(" 1 添 加 客 户")
fmt.Println(" 2 修 改 客 户")
fmt.Println(" 3 删 除 客 户")
fmt.Println(" 4 客 户 列 表")
fmt.Println(" 5 退 出")
fmt.Println("-----------------------------------------------")
fmt.Print("请选择(1-5):")
fmt.Scanln(&c.key)
switch c.key {
case "1":
fmt.Println("添 加 客 户")
case "2":
fmt.Println("修 改 客 户")
case "3":
fmt.Println("删 除 客 户")
case "4":
fmt.Println("客 户 列 表")
case "5":
c.loop = false
default:
fmt.Println("你的输入有误,请重新输入...")
}
if !c.loop {
break
}
}
fmt.Println("你退出了客户关系管理系统...")
}

func main() {
//在main中,创建一个customerView,并运行显示主菜单
customerView := customerView {
key: "",
loop: true,
}
//显示主菜单
customerView.mainMenu()
}

显示客户列表的功能

  • model/customer.go
1
2
3
4
5
//返回用户的信息,格式化的字符串
func (c *Customer) GetInfo() string {
info := fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v", c.id, c.name, c.age, c.gender, c.phone, c.email)
return info
}
  • service/customerService.go
1
2
3
4
5
6
7
8
9
10
11
12
13
//编写一个函数,可以返回 *CustomerService
func NewCustomerService() *CustomerService {
//为了能够看到有客户在切片中,我们初始化一个客户
CustomerService := &CustomerService{}
CustomerService.customerNum = 1
customer := model.NewCustomer(1, "张三", "男", 20, "112", "syz15822409222@126.com")
CustomerService.customers = append(CustomerService.customers, customer)
return CustomerService
}

func (c *CustomerService) List() []model.Customer {
return c.customers
}
  • view/customerView.go
1
2
3
4
5
6
7
8
9
10
11
//显示所有客户的信息
func (c *customerView) list() {
//首先获取到当前所有的客户信息(在切片中)
customers := c.customerService.List()
fmt.Println("---------------------客户列表--------------------")
fmt.Println("编号\t姓名\t性别\t年龄\t电话\t邮箱")
for i := 0; i < len(customers); i++ {
fmt.Println(customers[i].GetInfo())
}
fmt.Println("-------------------客户列表完成-------------------")
}

image-20220817181101019

添加客户的功能

  • 实现思路

image-20220817180245661

  • model/customer.go

增加下面的内容(不手动指定id号,让其自动累加)

1
2
3
4
5
6
7
8
9
10
//第二种创建Customer实例方法,不带id
func NewCustomer2(name string, gender string, age int, phone string, email string) Customer {
return Customer{
Name: name,
Gender: gender,
Age: age,
Phone: phone,
Email: email,
}
}
  • service/customerService.go
1
2
3
4
5
6
7
8
//添加客户端到customer切片
func (c *CustomerService) Add(customer model.Customer) bool {
//我们确定一个分配id的规则,就是添加的顺序
c.customerNum++ //这样我们就不用写id了
customer.Id = c.customerNum
c.customers = append(c.customers, customer)
return true
}
  • view/customerView.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//得到用户的输入,信息构建新的客户
func (c *customerView) add() {
fmt.Println("---------------------添加客户--------------------")
fmt.Println("姓名:")
name := ""
fmt.Scanln(&name)
fmt.Println("性别")
gender := ""
fmt.Scanln(&gender)
fmt.Println("年龄")
age := 0
fmt.Scanln(&age)
fmt.Println("电话")
phone := ""
fmt.Scanln(&phone)
fmt.Println("邮件")
email := ""
fmt.Scanln(&email)

//构建一个新的Customer实例
//注意id号,没有让用户输入,id是唯一的,需要系统分配
customer := model.NewCustomer2(name, gender, age, phone, email)
//调用
if c.customerService.Add(customer) {
fmt.Println("---------------------添加完成--------------------")
} else {
fmt.Println("---------------------添加失败--------------------")
}
}

image-20220817180934415

删除客户的功能

  • 实现思路

image-20220818095556737

  • model/customer.go(没有变化)
  • service/customerService.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//根据客户的id删除客户的信息
//首先实现查找客户的id是否存在
func (c *CustomerService) FindById(id int) int {
index := -1
//遍历 c.customers 切片
for i := 0; i < len(c.customers); i++ {
if c.customers[i].Id == id {
index = i //找到
}
}
return index
}

//如果id存在,那么删除该用户
func (c *CustomerService) Delete(id int) bool {
index := c.FindById(id)
//如果 index = -1, 说明没有这个客户
if index == -1 {
return false
}
//从切片中删除一个元素
c.customers = append(c.customers[:index], c.customers[index+1:]...) //注意这里的写法
return true
}

view/customerView.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//得到用户的id后,删除该id对应的客户
func (c *customerView) delete() {
fmt.Println("---------------------删除客户--------------------")
fmt.Println("请输入待删除客户的id编号(-1退出):")
id := -1
fmt.Scanln(&id)
if id == -1 {
return //放弃删除操作
}
fmt.Println("确认是否删除(Y/N):")
for {
//定义一个变量,接收下面的for循环是否退出
flag := false
choice := ""
fmt.Scanln(&choice)
switch choice {
case "y", "Y":
if c.customerService.Delete(id) {
fmt.Println("---------------------删除完成--------------------")
} else {
fmt.Println("---------------------删除失败--------------------")
}
flag = true
case "n", "N":
flag = true
default:
fmt.Println("您的输入有误,请输入正确的选项!(Y/N)")
}
//如果 flag == true, 则退出for循环
if flag {
break
}
}
}

image-20220818104008935

完善退出确认功能

  • view/customerView.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//退出软件
func (c *customerView) exit() int {

n := 0 //给主函数返回一个变量用于判断是否退出
for {
//定义一个变量,接收下面的for循环是否退出
flag := true //true代表退出循环
fmt.Println("确认是否退出(Y/N):")
choice := ""
fmt.Scanln(&choice)
switch choice {
case "y", "Y":
fmt.Println("感谢您的使用,下次再见~")
n = 1
case "n", "N":
fmt.Println("感谢您的喜欢与支持~")
default:
fmt.Println("您的输入有误,请输入正确的选项!(Y/N)")
flag = false
}
//如果 flag == true, 则退出for循环
if flag {
break
}
}
return n
}

image-20220818113759580

修改客户信息

修改客户信息,需要在customerServicecustomerView中进行编写

  • service/customerService.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//根据客户提供的id,对客户的相关个人信息进行修改
func (c *CustomerService) Alter(id int, name string, gender string, age int, phone string, email string) bool {
index := c.FindById(id)
//如果 index = -1, 说明没有这个客户
if index == -1 {
return false
}

//修改客户信息,按下回车表示不修改
if c.customers[index].Name != "\n" {
c.customers[index].Name = name
}
if c.customers[index].Gender != "\n" {
c.customers[index].Gender = gender
}
if c.customers[index].Age != 0 { //0表示没有修改
c.customers[index].Age = age
}
if c.customers[index].Phone != "\n" {
c.customers[index].Phone = phone
}
if c.customers[index].Email != "\n" {
c.customers[index].Email = email
}
return true
}
  • view/customerView.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//修改客户信息
func (c *customerView) alter() {
fmt.Println("---------------------修改客户--------------------")
fmt.Println("请输入待删除客户的id编号(-1退出):")
id := -1
fmt.Scanln(&id)
if id == -1 {
return //放弃修改操作
}

index := c.customerService.FindById(id) //获取对应id的数组的索引
if index == -1 { //如果index == -1,说明没有这个用户
fmt.Println("-----------该id不存在,请检查后重新尝试-----------")
return
}
customers := c.customerService.List() //获取数组的所有信息

fmt.Printf("姓名(%v):", customers[index].Name)
name := customers[index].Name
fmt.Scanln(&name)

fmt.Printf("性别(%v):", customers[index].Gender)
gender := customers[index].Gender
fmt.Scanln(&gender)

fmt.Printf("年龄(%v):", customers[index].Age)
age := customers[index].Age
fmt.Scanln(&age)

fmt.Printf("电话(%v):", customers[index].Phone)
phone := customers[index].Phone
fmt.Scanln(&phone)

fmt.Printf("邮箱(%v):", customers[index].Email)
email := customers[index].Email
fmt.Scanln(&email)

fmt.Println("确认是否修改(Y/N):")
for {
//定义一个变量,接收下面的for循环是否退出
flag := false
choice := ""
fmt.Scanln(&choice)
switch choice {
case "y", "Y":
if c.customerService.Alter(id, name, gender, age, phone, email) {
fmt.Println("---------------------修改完成--------------------")
} else {
fmt.Println("---------------------修改失败--------------------")
}
flag = true
case "n", "N":
flag = true
default:
fmt.Println("您的输入有误,请输入正确的选项!(Y/N)")
}
//如果 flag == true, 则退出for循环
if flag {
break
}
}
}

image-20220818153232485

至此,客户信息关系系统完成~

文件操作

打开/关闭文件用到的方法

func Open

1
func Open(name string) (*File, error)

打开指定文件进行读取。如果成功,则可以使用返回文件上的方法进行读取;关联的文件描述符具有O_RDONLY模式。如果有错误,它的类型将是* pathror。

func (*File) Close

1
func (f *File) Close() error

关闭关闭文件,使其无法用于I/O。在支持SetDeadline的文件中,任何等待的I/O操作将被取消,并立即返回ErrClosed错误。如果Close已经被调用,将返回一个错误。

演示案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"os"
)

func main() {
//打开文件
/*
概念说明: file的叫法
1. file 叫 file 对象
2. file 叫 file 指针
3. file 叫 file 文件句柄
*/
file, err := os.Open("/usr/local/go/src/golang_study/14-File/filedemo/demo.txt")
if err != nil {
fmt.Println("Open file err=", err)
}
//通过输出结果可以得知,file就是一个指针
fmt.Printf("file=%v\n", file)
//关闭文件
err = file.Close()
if err != nil {
fmt.Println("Close file err=", err)
}
}

执行结果

1
file=&{0xc0000ae120}

读文件操作应用实例

  • 读取文件的内容并显示在终端(带缓冲区的方式)

使用 os.Open, file.Close, bufio.NewReader(), reader.ReadString 函数和方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"bufio"
"fmt"
"io"
"os"
)

func main() {
//打开文件
file, err := os.Open("/usr/local/go/src/golang_study/14-File/filedemo/demo.txt")
if err != nil {
fmt.Println("Open file err=", err)
}
//当函数退出时, 要及时关闭file
defer file.Close() //要及时关闭file句柄, 否则会有内存泄漏

//创建一个 *Reader, 是带缓冲的
/*
const (
defaultBufSize = 4096 //默认的缓冲区为 4096
)
*/
reader := bufio.NewReader(file)
//循环的读取文件的内容
for {
str, err := reader.ReadString('\n') //读到一个换行就结束
if err == io.EOF { //io.EOF 表示文件的末尾
break
}
fmt.Print(str)
}
fmt.Println("文件读取结束...")
}

执行结果

1
2
3
4
123123
456456
789789
文件读取结束...
  • 读取文件的内容并显示在终端(使用 ioutil 一次将整个文件读入到内存中),这种方式适用于文件不大的情况

相关方法和函数: ioutil.ReadFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"io/ioutil"
)

func main() {
//使用ioutil.ReadFile一次性将文件读取到位
file := "/usr/local/go/src/golang_study/14-File/filedemo/demo.txt"
content, err := ioutil.ReadFile(file)
if err != nil {
fmt.Printf("read file err=%v", err)
}
//将读取到的内容显示到终端
//fmt.Println("%v", content) //[]byte
fmt.Printf("%v", string(content)) //byte

//我们没有显示的Open文件,因此也不需要显示的Close文件
//因为, 文件的Open和Close被封装到 ReadFile 函数内部
}

执行结果

1
2
3
123123
456456
789789

写文件操作应用实例

func OpenFile

1
func OpenFile(name string, flag int, perm FileMode) (*File, error)

OpenFile是广义的open调用;大多数用户会使用Open或Create。它打开指定标志(O_RDONLY等)的命名文件。如果文件不存在,并且传递了O_CREATE标志,则使用perm模式(在umask之前)创建它。如果成功,就可以使用返回File上的方法进行I/O操作。如果有错误,它的类型将是* pathror。

  • 第一个参数: 表示文件的路径
  • 第二个参数: 选项如下(可组合使用)
1
2
3
4
5
6
7
8
9
10
11
12
const (  
// O_RDONLY、O_WRONLY或O_RDWR必须指定一个。
O_RDONLY int = sycall.O_RDONLY //只读模式打开文件。
O_WRONLY int = sycall.O_WRONLY //只写模式打开文件
O_RDWR int = sycall.O_RDWR //读写模式打开文件
//剩下的值可以输入或输入以控制行为。
O_APPEND int = sycall.O_APPEND //写入文件时追加数据。
O_CREATE int = sycall.O_CREAT //如果文件不存在,则创建一个新文件。
O_EXCL int = syscall.O_EXCL //与 O_CREATE 一起使用,文件必须不存在。
O_SYNC int = sycall.O_SYNC //打开用于同步I/O。
O_TRUNC int = sycall.O_TRUNC //如果可能,打开时清空文件

  • 第三个参数: 权限控制(同Linux)
1
2
3
r => 4
w => 2
x => 1

方式一

  • 创建一个新文件,写入内容 5 句 “hello, Gardon”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"bufio"
"fmt"
"os"
)

func main() {
//创建一个新文件,写入内容 5句 "hello, Gardon"
//1. 打开文件 /usr/local/go/src/golang_study/14-File/filedemo3/demo.txt
filePath := "/usr/local/go/src/golang_study/14-File/filedemo3/demo.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
//及时关闭file句柄
defer file.Close()
//准备写入5句 "hello, Gardon"
str := "hello, Gardon\n"
//写入时, 使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
/*
因为weiter是带缓存, 因此在调用WriterString方法时,
其实内容是先写入到缓存的,所以需要调用Flush方法,
将缓冲的数据真正写入到文件中, 否则文件中会没有数据
*/
writer.Flush()
}
  • 打开一个存在的文件,将原来的内容覆盖成新的内容 10 句 “你好,世界!”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"bufio"
"fmt"
"os"
)

func main() {
//打开已经存在的文件
filePath := "/usr/local/go/src/golang_study/14-File/filedemo3/demo.txt"
//O_TRUNC //如果可能,打开时清空文件
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
//及时关闭file句柄
defer file.Close()
//准备写入10句 "你好,世界"
str := "你好,世界\r\n" // \r\n 表示换行
//写入时, 使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString(str)
}
writer.Flush()
}
  • 打开一个存在的文件,在原来的内容追加内容 ‘ABC! ENGLISH!’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"bufio"
"fmt"
"os"
)

func main() {
//打开已经存在的文件
filePath := "/usr/local/go/src/golang_study/14-File/filedemo3/demo.txt"
//O_APPEND //写入文件时追加数据
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
//及时关闭file句柄
defer file.Close()
//准备写入10句 "你好,世界"
str := "ABC! ENGLISH!\r\n" // \r\n 表示换行
//写入时, 使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString(str)
}
writer.Flush()
}
  • 打开一个存在的文件,将原来的内容读出显示在终端,并且追加 5 句”hello,北京!”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"bufio"
"fmt"
"io"
"os"
)

func main() {
//打开文件
filePath := "/usr/local/go/src/golang_study/14-File/filedemo3/demo.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
//及时关闭file句柄
defer file.Close()

//读
//先读取原来的文件内容,并显示在终端
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n') // \n在这里表示 reader 读到哪里结束
if err == io.EOF {
break
}
//显示到终端
fmt.Print(str)
}

//写
//准备写入10句 "hello,北京!"
str := "hello,北京!\r\n" // \r\n 表示换行
//写入时, 使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString(str)
}
writer.Flush()
}

方式二

  • 编程一个程序,将一个文件的内容,写入到另外一个文件。
  • 注:这两个文件已经存在了

说明: 使用 ioutil.ReadFile / ioutil.WriteFile 完成写文件的任务.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"io/ioutil"
)

func main() {
//将a.txt文件内容导入到b.txt
//1. 首先将 a.txt 内容读取到内存
//2. 将读取到的内容写入 b.txt(如果b.txt文件存在, 则直接覆盖)
file1Path := "/usr/local/go/src/golang_study/14-File/filedemo5/main/a.txt"
file2Path := "/usr/local/go/src/golang_study/14-File/filedemo5/main/b.txt"

data, err := ioutil.ReadFile(file1Path)
if err != nil {
//说明读取文件有错误
fmt.Printf("read file err=%v\n", err)
return
}
err = ioutil.WriteFile(file2Path, data, 0666)
if err != nil {
fmt.Printf("write file error=%v\n", err)
}
}

判断文件是否存在

golang判断文件文件夹是否存在的方法为 os.Stat() 函数返回的错误值进行判断:

  1. 如果返回的错误为nil, 说明文件或文件夹存在
  2. 如果返回的错误类型使用 os.IsNotExist() 判断为true, 说明文件或文件夹不存在
  3. 如果返回的错误为其他类型,则不确定是否存在
1
2
3
4
5
6
7
8
9
10
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil { //文件或者目录存在
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

文件编程应用实例

拷贝文件

说明: 将一个文件拷贝到另外一个文件

所需函数: func Copy(dst Writer, src Reader) (written int64, err error)

注意: Copy 函数是 io 包提供的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"bufio"
"fmt"
"io"
"os"
)

//自己编写一个函数, 接收两个文件的路径 (srcFileName, dstFileName)
func CopyFile(dstFileName string, srcFileName string) (written int64, err error) {
srcFile, err := os.Open(srcFileName)
if err != nil {
fmt.Printf("open file err=%v\n", err)
}
defer srcFile.Close()

//通过srcfile, 获取到 Reader
reader := bufio.NewReader(srcFile)

//先创建目标文件, 打开 dstFileName
dstFile, err := os.OpenFile(dstFileName, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}

//通过dstFile, 获取到 Writer,将原文件的内容写入到目标文件中
writer := bufio.NewWriter(dstFile)
defer dstFile.Close()
return io.Copy(writer, reader)
}

func main() {
//将 a.txt 文件拷贝到 b.txt
//调用CopyFile 完成文件拷贝
srcFile := "/usr/local/go/src/golang_study/14-File/filedemo6/main/a.txt"
dstFile := "/usr/local/go/src/golang_study/14-File/filedemo6/main/b.txt"
_, err := CopyFile(dstFile, srcFile)
if err == nil {
fmt.Printf("拷贝完成\n")
} else {
fmt.Printf("拷贝错误 err=%v\n", err)
}
}

统计英文、数字、空格和其他字符数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
"bufio"
"fmt"
"io"
"os"
)

//定义一个结构体, 用于保存统计结果
type CharCount struct {
ChCount int //记录英文个数
NumCount int //记录数字的个数
SpaceCount int //记录空格的个数
OtherCount int //记录其他字符的个数
}

func main() {
//思路: 打开一个文件,创建一个 Reader
//每读取一行, 就去统计该行有多少个 英文 数字 空格 和 其他字符
//然后将结果保存到一个结构体
fileName := "/usr/local/go/src/golang_study/14-File/filedemo7/main/char.txt"
file, err := os.Open(fileName)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
defer file.Close()

//定义个 CharCount 实例
var count CharCount
//创建一个 Reader
reader := bufio.NewReader(file)

//开始循环的读取 fileName 的内容
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
//为了兼容中文字符, 可以将 str 转成 []rune
str1 := []rune(str)
//遍历 str, 进行统计
for _, v := range str1 {
switch {
case v >= 'a' && v <= 'z':
fallthrough //穿透
case v >= 'A' && v <= 'Z':
count.ChCount++
case v == ' ' || v == '\t':
count.SpaceCount++
case v >= '0' && v <= '9':
count.NumCount++
default:
count.OtherCount++
}
}
}

//输出统计的结果
fmt.Printf("字符的个数=%v, 数字的个数=%v, 空格的个数=%v, 其他字符个数=%v\n", count.ChCount, count.NumCount, count.SpaceCount, count.OtherCount)
}

命令行参数

os.Args 是一个 string 的切片,用来存储所有的命令行参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"os"
)

func main() {
fmt.Println("命令行的参数有", len(os.Args))
//遍历os.Args切片,就可以得到所有的命令行输入参数值
for i, v := range os.Args {
fmt.Printf("args[%v]=%v\n", i, v)
}
}

执行结果

image-20220822093121250

flag 包用来解析命令行参数

说明: 前面的方式是比较原生的方式,对解析参数不是特别的方便,特别是带有指定参数形式的命令行。

比如: cmd>main.exe -f a.txt -p 200 -u root 这样的形式命令行,go 设计者给我们提供了 flag 包,可以方便的解析命令行参数,而且参数顺序可以随意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"flag"
"fmt"
)

func main() {
//定义几个变量,用于接收命令行的参数值
var user string
var pwd string
var host string
var port int

//&user 就是接收用户命令中输入的 -u 后面的参数值
//"u" 就是 -u 指定参数
//"" 默认值
//"用户名,默认为空" 说明
flag.StringVar(&user, "u", "", "用户名,默认为空")
flag.StringVar(&pwd, "pwd", "", "密码,默认为空")
flag.StringVar(&host, "h", "localhost", "主机名, 默认为localhost")
flag.IntVar(&port, "port", 3306, "端口号,默认为3306")
//这里有一个非常重要的操作, 转换, 必须调用该方法
flag.Parse()
//输出结果
fmt.Printf("user=%v pwd=%v host=%v port=%v\n", user, pwd, host, port)
}

image-20220822095246719

json

基本介绍

JSON易于机器解析和生成,并有效的提升网络传输效率,通常程序在网络传输时,会先将数据(结构体/map等)序列化成json字符串,到接收方得到json字符串时,在反序列化恢复成原来的数据类型(结构体/map等)

image-20220822100033091

json数据格式说明

在json语言中,一切都是对象.因此,任何的数据类型都可以通过json来表示,例如字符串,数字,对象,数组,map,结构体等

1
[{"key1":val1, "key2":val2},{"key1":val1, "key2":val2, "key3":[val3, val4]}]

json的序列化

json 序列化是指,将有 key-value 结构的数据类型(比如结构体、map、切片)序列化成 json 字符串的操作。

  • 应用案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package main

import (
"encoding/json"
"fmt"
)

//定义一个结构体
type Monster struct {
Name string
Age int
Birthday string
Sal float64
Skill string
}

func testStruct() {
//演示
monster := Monster{
Name: "牛魔王",
Age: 500,
Birthday: "2022-08-23",
Sal: 8000.0,
Skill: "牛魔拳",
}

//将 monster 序列化
data, err := json.Marshal(&monster)
if err != nil {
fmt.Printf("序列号错误 err=%v\n", err)
}

//输出序列化后的结果
fmt.Printf("monster 序列化后=%v\n", string(data))
}

//将 map 进行序列化
func testMap() {
//定义一个map
var a map[string]interface{}
//使用 map,需要 make
a = make(map[string]interface{})
a["name"] = "红孩儿"
a["age"] = 30
a["address"] = "洪崖洞"
//将 a 这个map进行序列化
//将 monster 序列化
data, err := json.Marshal(a)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("a map 序列化后=%v\n", string(data))
}

//演示对切片进行序列化,我们这个切片 []map[string]interface{}
func testSlice() {
var slice []map[string]interface{}
var m1 map[string]interface{}
//使用 map 前, 需要先 make
m1 = make(map[string]interface{})
m1["name"] = "jack"
m1["age"] = "7"
m1["address"] = "北京"
slice = append(slice, m1)

var m2 map[string]interface{}
//使用 map 前, 需要先 make
m2 = make(map[string]interface{})
m2["name"] = "tom"
m2["age"] = "20"
m2["address"] = [2]string{"墨西哥", "夏威夷"}
slice = append(slice, m2)

//将切片进行序列化操作
data, err := json.Marshal(slice)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("slice 序列化后=%v\n", string(data))
}

//对基本数据类型序列化, 对基本数据类型进行序列化意义不大
func testFloat64() {
var num1 float64 = 2345.67
//对 num1 进行序列化
data, err := json.Marshal(num1)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("num1 序列化后=%v\n", string(data))
}

func main() {
//演示将结构体,map,切片进行序列号
testStruct()
testMap()
testSlice() //演示对切片的序列化
testFloat64() //演示对基本数据类型的序列化
}

image-20220822113243892

  • 注意事项

对于结构体的序列化,如果我们希望序列化后的 key 的名字,又我们自己重新制定,那么可以给 struct 指定一个 tag 标签.

1
2
3
4
5
6
7
8
//定义一个结构体
type Monster struct {
Name string `json:"monster_name"`
Age int `json:"monster_age"`
Birthday string
Sal float64
Skill string
}

json的反序列化

json 反序列化是指,将 json 字符串反序列化成对应的数据类型(比如结构体、map、切片)的操作

  • 应用案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
"encoding/json"
"fmt"
)

//定义一个结构体
type Monster struct {
Name string
Age int
Birthday string
Sal float64
Skill string
}

//演示将json字符串,反序列化为 struct
func unmarshalStruct() {
//说明: str 在项目开发中,是通过网络传输获取到..或者是读取到文件获取到
str := "{\"Name\":\"牛魔王\", \"Age\":500, \"Birthday\":\"2022-08-23\", \"Sal\":8000, \"Skill\":\"牛魔拳\"}"

//定义一个Monster实例
var monster Monster
err := json.Unmarshal([]byte(str), &monster)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 monster=%v monster.Name=%v\n", monster, monster.Name)
}

//演示将json字符串,反序列化成 map
func unmarshalMap() {
str := "{\"address\":\"洪崖洞\", \"age\":30, \"name\":\"红孩儿\"}"

//定义一个 map
var a map[string]interface{}

//反序列化
//注意: 反序列化 map, 不需要make, 因为 make 操作被封装到 Unmarshal 函数
err := json.Unmarshal([]byte(str), &a)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 a=%v\n", a)
}

//演示将 json 字符串, 反序列化成切片
func unmarshalSlice() {
str := "[{\"address\":\"北京\", \"age\":\"7\", \"name\":\"jack\"}," + " {\"address\":[\"墨西哥\", \"夏威夷\"], \"age\":\"20\", \"name\":\"tom\"}]"

//定义一个slice
var slice []map[string]interface{}
//反序列化, 不需要 make, 因为 make 操作被封装到 Unmarshal 函数
err := json.Unmarshal([]byte(str), &slice)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 slice=%v\n", slice)
}

func main() {
unmarshalStruct()
unmarshalMap()
unmarshalSlice()
}

image-20220822134302705

对上面代码的说明
1) 在反序列化一个json字符串时,要确保反序列化后的数据类型和原来序列化前的数据类型一致。
1) 如果 json 字符串是通过程序获取到的,则不需要再对 " 转义处理。

单元测试

在工作中,我们经常会确认一个函数或模块的执行结果是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func addUpper(n int) int {
res := 0
for i := 1; i <= n; i++ {
res += i
}
return res
}

func main() {
res := addUpper(10)
fmt.Println("res=", res)
}

传统方式

在 main 函数中,调用 addUpper 函数,看看实际输出的结果是否和预期的结果一致,如果一致,则说明函数正确,否则函数有错误,然后修改错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func addUpper(n int) int {
res := 0
for i := 1; i <= n; i++ {
res += i
}
return res
}

func main() {
res := addUpper(10)
if res != 55 {
fmt.Printf("addUpper错误 返回值=%v 期望值=%v\n", res, 55)
} else {
fmt.Printf("addUpper正确 返回值=%v 期望值=%v\n", res, 55)
}
fmt.Println("res=", res)
}

image-20220822141141425

传统方式的缺点

  • 不方便, 我们需要在 main 函数中去调用,这样就需要去修改 main 函数,如果现在项目正在运行,就可能去停止项目
  • 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在 main 函数,不利于我们管理和清晰我们思路
  • 引出单元测试。=> testing 测试框架可以很好解决问题。

单元测试基本介绍

Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试,testing 框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题:

  • 确保每个函数是可运行,并且运行结果是正确的
  • 确保写出来的代码性能是好的
  • 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定

快速入门

  • 文件目录结构

image-20220822154411867

  • utils/cal.go
1
2
3
4
5
6
7
8
9
package main

func AddUpper(n int) int {
res := 0
for i := 1; i <= n; i++ {
res += i
}
return res
}
  • utils/cal_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"testing"
)

func TestAddUpper(t *testing.T) {
res := AddUpper(10)
if res != 55 {
// fmt.Printf("AddUpper(10) 执行错误, 期望值=%v 实际值=%v\n", 55, res)
t.Fatalf("AddUpper(10) 执行错误, 期望值=%v 实际值=%v\n", 55, res)
}

//如果正确,记录日志
t.Logf("AddUpper(10) 执行正确")
}

image-20220822154545558

  • 运行原理

image-20220822155748554

总结

1) 测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go , cal 不是固定的。
2) 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper
3) TestAddUpper(t tesing.T) 的形参类型必须是 `testing.T`,可以参考: 手册(需要科学上网)
4) 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub
5) 运行测试用例指令

  • cmd> go test [如果运行正确,无日志,错误时,会输出日志]

  • cmd> go test -v [运行正确或是错误,都输出日志]

  1. 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序
  2. t.Logf 方法可以输出相应的日志
  3. 测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处[原理图参考上面的图片]
  4. PASS 表示测试用例运行成功FAIL 表示测试用例运行失败
  5. 测试单个文件,一定要带上被测试的原文件
1
go test -v cal_test.go cal.go

11) 测试单个方法

1
go test -v -test.run TestAddUpper

综合案例

image-20220822165051757

  • monster/monster.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package monster

import (
"encoding/json"
"fmt"
"io/ioutil"
)

type Monster struct {
Name string
Age int
Skill string
}

//给 Monster 绑定方法 Store, 可以将一个 Monster 变量(对象), 序列化后保存到文件中

func (m *Monster) Store() bool {
//先序列化
data, err := json.Marshal(m)
if err != nil {
fmt.Println("marshal err=", err)
return false
}

//保存到文件
filePath := "/usr/local/go/src/golang_study/15-test/testdemo2/monster.ser"
err = ioutil.WriteFile(filePath, data, 0666)
if err != nil {
fmt.Println("write file err=", err)
return false
}
return true
}

//给 Monster 绑定方法 ReStore, 可以将一个序列化的 Monster, 从文件中读取
//并反序列化为 Monster 对象, 检查反序列化, 名字正确
func (m *Monster) ReStore() bool {
//1.先从文件中,读取序列化的字符串
filePath := "/usr/local/go/src/golang_study/15-test/testdemo2/monster.ser"
data, err := ioutil.ReadFile(filePath)
if err != nil {
fmt.Println("ReadFile err=", err)
return false
}

//2.使用读取到 data []byte, 对反序列化
err = json.Unmarshal(data, m)
if err != nil {
fmt.Println("Unmarshal err=", err)
return false
}
return true
}
  • monster/monster_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package monster

import (
"testing"
)

//测试用例, 测试 Store 方法
func TestStore(t *testing.T) {
//先创建一个 Monster 实例
monster := &Monster{
Name: "红孩儿",
Age: 10,
Skill: "吐火~",
}
res := monster.Store()
if !res {
t.Fatalf("monster.Store() 错误, 希望为=%v 实际为=%v", true, res)
}
t.Logf("monster.Store() 测试成功!")
}

func TestReStore(t *testing.T) {
//先创建一个 Monster 实例, 不需要指定字段的值
var monster = &Monster{}
res := monster.ReStore()
if !res {
t.Fatalf("monster.ReStore() 错误, 希望为=%v 实际为%v", true, res)
}

//进一步判断
if monster.Name != "红孩儿" {
t.Fatalf("monster.ReStore() 错误, 希望为=%v 实际为=%v", "红孩儿", monster.Name)
}
t.Logf("monster.ReStore() 测试成功!")
}

image-20220822165312506

执行后会在指定位置生成monster.ser文件

image-20220822165346821

文件内容如下

image-20220822165436440

goroutine 和 channel

goroutine(协程)

进程与线程

  • 进程就是程序在操作系统中的一次执行过程, 是系统进行资源分配和调度的基本单位
  • 线程是进程的一个执行实例, 是程序执行的基本单位
  • 一个进程可以任意创建和销毁多个线程,同一个进程中的多个线程可以并发执行
  • 一个程序至少有一个进程,一个进程至少有一个线程

程序、进程和线程的关系

1
2
3
程序 =>  进程1 => {线程1, 线程2...}
进程2 => {线程1, 线程2...}
...

并发与并行

  • 多线程程序在单核上运行,就是并发
  • 多线程程序在多核上运行,就是并行

image-20220823110638774

Go协程与Go主线程

  • Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]。

  • Go协程的特点

1) 有独立的栈空间
2) 共享程序堆空间
3) 调度由用户控制
4) 协程是轻量级的线程

image-20220823112203518

goroutine快速入门

请编写一个程序,完成如下功能:
1) 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 “hello,world”
2) 在主线程中也每隔一秒输出”hello,golang”, 输出 10 次后,退出程序
3) 要求主线程和 goroutine 同时执行.
4) 画出主线程和协程执行流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"strconv"
"time"
)

func test() {
for i := 0; i <= 10; i++ {
fmt.Println("test() hello world " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}

func main() {
go test() //开启一个协程
for i := 1; i <= 10; i++ {
fmt.Println("main() hello golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}

main 主线程和 test 协程同时执行

image-20220823131311170

执行流程图

image-20220823132846539

  • 小结

1) 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
2) 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
3) Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了

goroutine的调度模型

MPG 模式-状态一

image-20220824115702034

  1. 当前程序有三个M,如果三个M都在一个CPU运行,就是并发,如果在不同的CPU运行就是并行
  2. M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有三个,M3协程队列有两个
  3. 从上图可以看到: Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程
  4. 其他程序c/java的多线程,往往是内核态的,比较重量级,千个线程可能耗光CPU

MPG模式-状态二

image-20220824135604314

  1. 分成两个部分来看
  2. 原来的情况是 M0 主线程正在执行G0协程,另外有三个协程在队列等待
  3. 如果G0协程阻塞,比如读取文件或者数据库等
  4. 这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写
  5. 这样的MPG调度模式,可以既让G0执行,同时也不会让队列的其他协程一直阻塞,仍然可以并发/并行执行
  6. 等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒

设置golang运行的CPU数量

为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"runtime"
)

func main() {
//获取当前系统CPU的数量
num := runtime.NumCPU()
//设置num-1的cpu运行go程序
runtime.GOMAXPROCS(num)
fmt.Println("num=", num)
}

image-20220824140716803

  1. go1.8后: 默认让程序运行在多个核上,可以不用设置了
  2. go1.8前: 还是要设置一下,可以更高效的利用CPU

channel(管道)

  • 先看一个需求

需求: 现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中, 最后显示出来, 要求使用 toroutine 完成

*思路:*

*1.编写一个函数, 来计算各个数的阶乘,放入到 map 中*

*2.我们启动的协程多个,统计的将结果放入到 map 中*

*3.map 应该做出一个全局的*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"time"
)

//需求: 现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中
//最后显示出来, 要求使用 toroutine 完成

/*
思路:
1.编写一个函数, 来计算各个数的阶乘,放入到 map 中
2.我们启动的协程多个,统计的将结果放入到 map 中
3.map 应该做出一个全局的
*/

var (
myMap = make(map[int]int, 10)
)

// test 函数就是计算n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}

// 这里将 res 放入到 myMap
myMap[n] = res
}

func main() {
// 开启多个协程完成这个任务
for i := 1; i <= 200; i++ {
go test(i)
}

// 休眠5秒钟(第二个问题)
time.Sleep(time.Second * 5)

//输出结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}

此时运行会报错

image-20220824150446308

报错的原因是资源竞争,因为我的笔记本电脑只有8个CPU,当200个协程同时进行写操作时,就会出现这个问题

image-20220824154421473

不同 goroutine 之间如何通讯

  • 全局变量的互斥锁
  • 使用管道 channel 来解决

全局变量加锁同步

  • 因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes

  • 解决方案: 加入互斥锁

  • 我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"sync"
"time"
)

//需求: 现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中
//最后显示出来, 要求使用 toroutine 完成

/*
思路:
1.编写一个函数, 来计算各个数的阶乘,放入到 map 中
2.我们启动的协程多个,统计的将结果放入到 map 中
3.map 应该做出一个全局的
*/

var (
myMap = make(map[int]int, 10)
/*
声明一个全局的互斥锁
lock 是一个全局的互斥锁
sync 是包: synchornized 同步
Mutex: 是互斥
*/
lock sync.Mutex
)

// test 函数就是计算n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}

// 这里将 res 放入到 myMap
//加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
}

func main() {
// 开启多个协程完成这个任务
for i := 1; i <= 200; i++ {
go test(i)
}

// 休眠5秒钟(第二个问题)
time.Sleep(time.Second * 5)

//输出结果
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()
}

再次运行

image-20220824160348389

此时会发现,阶乘的结果会有0负数

因为我们给定的200太大了,会越界,改小一些即可

channel的意义

  1. 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美

  2. 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。

  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁

  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。

  5. 上面种种分析都在呼唤一个新的通讯机制: channel

channel的基本介绍

  1. channle 本质就是一个数据结构-队列

  2. 数据是先进先出【FIFO : first in first out】

  3. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的

  4. channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。

image-20220824164017967

channel的声明

1
2
3
4
var intChan chan int (intChan 用于存放 int 数据)
var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
var perChan chan Person
var perChan2 chan *Person

channel 是引用类型

channel 必须初始化才能写入数据, 即 make 后才能使用

管道是有类型的,intChan 只能写入 整数 int

channel应用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import "fmt"

func main() {
//演示一下管道的使用
//1. 创建一个可以存放 3 个 int 类型的管道
var intChan chan int
intChan = make(chan int, 3)

//2. 看看 intChan 是什么
fmt.Printf("intChan 的值=%v intChan 本身的地址=%p\n", intChan, &intChan)

//3. 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- 50
//intChan <- 98 //注意点, 当我们给管道写入数据时,不能超过其容量

//4. 看看管道的长度和cap(容量)
fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan))

//5. 从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2) //先进先出,所以是10
fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan)) //2, 3

//6. 在没有使用协程的情况下, 如果我们的管道数据已经全部取出,再取就会报告 deadlock
num3 := <-intChan
num4 := <-intChan
num5 := <-intChan

fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
}

image-20220824170435168

当我们从intChan取出第一个数据时, intChan的长度就 -1

正如我们所料, 当程序执行到num3时,就报错了

channel的注意事项

  1. channel中只能存放指定的数据类型

  2. channle的数据放满后,就不能再放入了

  3. 如果从 channel 取出数据后,可以继续放入

  4. 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock

读写channel案例演示

  • 创建一个intChan, 最多可以存放3个int, 演示存3个数据到intChan, 然后再取出这三个int
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
var intChan chan int
intChan = make(chan int, 3)
intChan <- 10
intChan <- 20
intChan <- 10
//因为 intChan 的容量为3, 再存放会报告 dead lock
//intChan <- 50
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
//因为 intChan 这时已经没有数据了, 再取就会报告 deadlock
//num4 := <- intChan
fmt.Printf("num1=%v num2=%v num3=%v\n", num1, num2, num3)

}
  • 创建一个mapChan,最多可以存放10个map[string]string, 的 key-value,演示写入和读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

func main() {
var mapChan chan map[string]string

mapChan = make(chan map[string]string, 10)
m1 := make(map[string]string, 20)
m1["city1"] = "北京"
m1["city2"] = "天津"

m2 := make(map[string]string, 20)
m2["hero1"] = "宋江"
m2["hero2"] = "武松"

//写入channel
mapChan <- m1
mapChan <- m2

//定义一个数组接收channel数值
var arr []map[string]string = []map[string]string{}
arr = make([]map[string]string, 10)

l := len(mapChan) //提前记录好channel的长度,因为每一次循环,channel的长度都会发生变化
//循环输出
for i := 0; i < l; i++ {
arr[i] = <-mapChan
fmt.Printf("第 %v 个值为 %v \n", i+1, arr[i])
}
}

image-20220824180451127

  • 创建一个catChan,最多可以存放10个Cat结构体变量,演示写入和读取的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type Cat struct {
Name string
}

func main() {
var catChan chan Cat
catChan = make(chan Cat, 10)

cat1 := Cat{Name: "tom"}
cat2 := Cat{Name: "jerry"}
catChan <- cat1
catChan <- cat2

cat11 := <-catChan
cat22 := <-catChan

fmt.Println(cat11, cat22)
}

image-20220825134857715

  • 创建一个catChan,最多可以存放10个 *Cat 结构体变量,演示写入和读取的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type Cat struct {
Name string
}

func main() {
var catChan chan *Cat
catChan = make(chan *Cat, 10)

cat1 := Cat{Name: "tom"}
cat2 := Cat{Name: "jerry"}
catChan <- &cat1
catChan <- &cat2

cat11 := <-catChan
cat22 := <-catChan

fmt.Println(cat11, cat22)
}

image-20220825135202335

  • 创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

type Cat struct {
Name string
}

func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)

cat1 := Cat{Name: "tom"}
cat2 := Cat{Name: "jerry"}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "fuck"

cat11 := <-allChan
cat22 := <-allChan
v1 := <-allChan
v2 := <-allChan

fmt.Println(cat11, cat22, v1, v2)
}

image-20220825141042792

  • 观察以下代码,会出现什么问题?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

type Cat struct {
Name string
}

func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)

cat1 := Cat{Name: "tom"}
cat2 := Cat{Name: "jerry"}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "fuck"

cat11 := <-allChan

fmt.Println(cat11.Name) //直接调用会出现问题
}

代码改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type Cat struct {
Name string
}

func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)

cat1 := Cat{Name: "tom"}
cat2 := Cat{Name: "jerry"}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "fuck"

cat11 := <-allChan
name := cat11.(Cat)

fmt.Println(name)
}

image-20220825145447755

channel的遍历和关闭

channel的关闭

使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
close(intChan)
//这时不能够再写入数到 channel
//intChan <- 300
fmt.Println("ok~")
//当管道关闭后,可以读取数据
n1 := <-intChan
fmt.Println("n1=", n1)
}

channel的遍历

channel 支持 for–range 的方式进行遍历,请注意两个细节:

  1. 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误

  2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

  • 演示案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
intChan := make(chan int, 100)
//for i := 0; i < len(intChan); i++ {} //遍历管道不能使用普通的 for 循环
for i := 0; i < 100; i++ {
intChan <- i * 2 //放入100个数据到管道
}

//在遍历时,如果channel没有关闭, 则会出现deadlock的错误
//在遍历时,如果channel已经关闭, 则会正常遍历数据, 遍历完后, 就会退出遍历
close(intChan)
for v := range intChan {
fmt.Println("v=", v)
}
}
  • 应用实例1

image-20220825160432841

请完成goroutine和channel协同工作的案例,具体要求:

1) 开启一个writeData协程,向管道intChan中读取writeData写入的数据
2) 开启一个readData协程,从管道intChan中读取writeData写入的数据
3) 注意: writeData和readData操作的是同一个管道
4) 主线程需要等待writeData和readData协程都完成工作才能退出管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import "fmt"

//写数据
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
//放入数据
intChan <- i
fmt.Println("writeData ", i)
}
close(intChan) //关闭
}

//读数据
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
fmt.Printf("readData 读到数据=%v\n", v)
}
//readData 读取完数据后, 即任务完成
exitChan <- true
close(exitChan)
}

func main() {
//创建两个管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)

go writeData(intChan)
go readData(intChan, exitChan)

for {
_, ok := <-exitChan
if !ok {
break
}
}
}

image-20220825165832881

  • 应用实例2-阻塞

如果将上面代码中的readData协程注释掉,不进行读操作,会怎么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
//创建两个管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)

go writeData(intChan)
//go readData(intChan, exitChan)

for {
_, ok := <-exitChan
if !ok {
break
}
}
}

这样做的话,程序只是向管道内写入数据,而没有进行读取,就会出现阻塞deadlock, 原因是intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在 writeData 的 channel <- i

  • 应用实例3

要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有 goroutine 和 channel 的知识后,就可以完成了 [测试数据: 80000]

传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。

使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine 去完成,完成任务时间短。

image-20220825172254870

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package main

import (
"fmt"
"time"
)

//向 intChan 放入 1-8000个数
func putNum(intChan chan int) {
for i := 1; i <= 8000; i++ {
intChan <- i
}

//关闭 intChan
close(intChan)
}

//从 intChan 取出数据, 并判断是否为素数,如果是, 就放入到 primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {

var flag bool

//循环读取数据,进行素数的判断
for {
time.Sleep(time.Millisecond * 10)

num, ok := <-intChan //读取数据
if !ok { //intChan取不到
break
}

flag = true

//判断 num 是不是素数
//取余数,除了1和其本身以外,结果都不能为0的话,那么这个数就为素数
for i := 2; i < num; i++ {
if num%i == 0 {
flag = false
break
}
}

if flag {
//将这个数放入到 primeChan
primeChan <- num
}
}

fmt.Println("有一个 primeNum 协程因为取不到数据, 退出")
//这里我们还不能关闭 primeChan
//向 exitChan 写入 true
exitChan <- true
}

func main() {
//声明管道
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000) //放入结果
exitChan := make(chan bool, 4) //因为有4个协程

//开启一个协程, 向 intChan 放入 1-8000 个数
go putNum(intChan)

//开启 4 个协程, 从 intChan 取出数据, 并判断是否为素数, 如果是,则放入到 primeChan
//4 个协程都是使用的同一个管道
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}

//这里我们主线程, 进行处理
//这里利用协程go去跑(相当于在后台跑),我们就可以马上在下面输出素数的结果
go func() {
for i := 0; i < 4; i++ {
<-exitChan //当管道 exitChan 没有数据时,这个循环就会一直处于等待状态
}

//当我们从 exitChan 取出了 4 个结果, 就可以放心的关闭 primeChan
close(primeChan)
}()

fmt.Println("开始输出素数...")
//遍历我们的 primeChan, 把结果取出
for {
res, ok := <-primeChan
if !ok {
break
}

//将结果输出
fmt.Printf("素数=%d\n", res)
}

fmt.Println("main 线程退出")
}

执行结果:

image-20220826132011866

channel注意事项

  • channel可以声明为只读,或者只写性质
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
//1.在默认情况下, 管道时双向的
//var chan1 chan int //可读可写
//2.声明为只写
chan2 := make(chan<- int, 3)
chan2 <- 20
//num := <-chan2 //error
fmt.Println("chan2=", chan2)

//3.声明为只读
var chan3 <-chan int
num2 := <-chan3
//chan3<- 30 //error
fmt.Println("num2=", num2)
}
  • channel只读和只写的最佳实践案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import "fmt"

//只写
func send(ch chan<- int, exitChan chan struct{}) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
var a struct{}
exitChan <- a
}

//只读
func recv(ch <-chan int, exitChan chan struct{}) {
for {
v, ok := <-ch
if !ok {
break
}
fmt.Println(v)
}
var a struct{}
exitChan <- a
}

func main() {

ch := make(chan int, 10)
exitChan := make(chan struct{}, 2)
go send(ch, exitChan)
go recv(ch, exitChan)

var total = 0
for range exitChan {
total++
if total == 2 {
break
}
}
fmt.Println("结束...")
}

image-20220826141350828

  • 使用 select 可以解决从管道取数据的阻塞问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
"time"
)

func main() {
//使用 select 可以解决从管道取数据的阻塞问题

//1.定义一个管道; 10个数据 int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}

//2.定义一个管道; 5个数据 string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}

//传统的方法在遍历管道时, 如果不关闭会阻塞而导致 deadlock
//问题: 在实际开发中, 可能我们不好确定什么时候关闭该管道
//可以使用 select 方式来解决
for {
select {
//注意, 如果 intChan 一直没有关闭, 不会一直阻塞而 deadlock
//会自动到下一个 case 匹配
case v := <-intChan:
fmt.Printf("从 intChan 读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("从 stringChan 读取的数据%s\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("都取不到了, 不玩儿了, 程序员可以加入逻辑\n")
time.Sleep(time.Second)
return
}
}
}

image-20220826143741097

  • goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题

如果我们启动了一个协程, 但是这个协程出现了panic, 如果我们没有捕获到这和panic,就会造成整个程序崩溃,这时我们可以在 goroutine 中使用 recover 来捕获 panic, 进行处理,这样即使这个协程发生了问题,但是主线程仍然不受影响,可以继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"time"
)

func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello world")
}
}

func test() {
//这里我们可以使用 defer + recover
defer func() {
//捕获 test 抛出的 panic
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()

//定义一个 map
var myMap map[int]string
// myMap = make(map[int]string, 0)
myMap[0] = "golang" //error
}

func main() {
go sayHello()
go test()

for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}

image-20220826150308756

反射

反射的基本介绍

1) 反射可以在运行时动态获取变量的各种信息, 比如变量的类型(type),类别(kind)
2) 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
3) 通过反射,可以修改变量的值,可以调用关联的方法。
4) 使用反射,需要 import (“reflect”)

包反射实现运行时反射,允许程序操作具有任意类型的对象。典型的用法是使用静态类型接口{}获取一个值,并通过调用TypeOf提取其动态类型信息,它返回一个type。

对ValueOf的调用将返回一个表示运行时数据的Value。Zero接受一个Type并返回一个Value,表示该类型的0值。

image-20220826155226051

应用场景

常见的反射应用场景有以下两种

  1. 不知道接口调用哪个函数,根据传入参数在运行时确定调用的具体接口,这种需要对函数方法反射,例如以下这种桥接模式,比如我前面提出问题
1
func bridge(funcPtr interface{}, args ...interface{})

第一个参数 funcPtr 以接口的形式传入函数指针,函数参数 args 以可变参数的形式传入,bridge 函数中可以用反射来动态执行 funcPtr 函数

  1. 对结构体序列化时, 如果结构体有指定Tag,也会使用到反射生成对应的字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"encoding/json"
"fmt"
)

type Monster struct {
Name string `json:"name"` //打标签,注意冒号后面不要加空格
Age int `json:"age"`
Skill string `json:"skill"`
}

func main() {
monster := Monster{"牛魔王", 500, "芭蕉扇"}
//将monster变量序列化为 json 格式字符串
//json.Marshal 函数中使用反射,这个讲解反射时,再详细介绍
jsonStr, err := json.Marshal(monster)
if err != nil {
fmt.Println("json 处理错误", err)
}
fmt.Println("jsonStr", jsonStr) //输出byte
fmt.Println("jsonStr", string(jsonStr)) //byte转字符串
}

反射重要的函数和概念

  1. reflect.TypeOf(变量名), 获取变量名的类型,返回reflect.Type类型
  2. reflect.ValueOf(变量名), 获取变量的值,返回reflect.Value类型;reflect.Value 是一个结构体类型,通过 reflect.Value, 可以获取到关于该变量的很多信息
  3. 变量、interface{} 和 reflect.Value 是可以相互转换的,这点在实际开发中,会经常使用到

image-20220826163907646

image-20220826164820453

反射的快速入门

请编写一个案例,演示对(结构体类型、interface{}、reflect.Value)进行反射的基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package main

import (
"fmt"
"reflect"
)

//演示反射
func reflectTest01(b interface{}) {
//通过反射获取的传入的变量的 type, kind 值
//1. 先获取到 reflect.Type
rType := reflect.TypeOf(b)
fmt.Println("rType=", rType)

//2. 获取到 reflect.Value
rVal := reflect.ValueOf(b)

n2 := 2 + rVal.Int()
fmt.Println("n2=", n2)

fmt.Printf("rVal=%v rVal type=%T\n", rVal, rVal)

//下面我们将 rVal 转成 interface{}
iV := rVal.Interface()
//将 interface{} 通过断言转成需要的类型
num2 := iV.(int)
fmt.Println("num2=", num2)
}

//演示反射(对结构体的反射)
func reflectTest02(b interface{}) {
//通过反射获取的传入的变量 type kind 值
//1. 先获取到 reflect.Type
rType := reflect.TypeOf(b)
fmt.Println("rType=", rType)

//2. 获取到 reflect.Value
rVal := reflect.ValueOf(b)

//下面我们将 rVal 转成 interface{}
iV := rVal.Interface()
fmt.Printf("iv=%v iv type=%T\n", iV, iV)
//将 interface{} 通过断言转成 需要的类型
//这里,我们就简单使用了一带检测的类型断言
//可以使用 switch 的断言形式来做的更加的灵活
stu, ok := iV.(Student)
if ok {
fmt.Printf("stu.Name=%v\n", stu.Name)
}
}

type Student struct {
Name string
Age int
}

type Monster struct {
Name string
Age int
}

func main() {
//编写一个案例
//演示对(基本数据类型/interface{}/reflect.Value)进行反射的基本操作

//1.先定义一个 int
//var num int = 0
//reflectTest01(num)

//2.定义一个 Student 的实例
stu := Student{
Name: "tom",
Age: 20,
}

reflectTest02(stu)
}

image-20220826172718494

可以参考文章golang反射案例

反射的注意事项

  1. reflect.Value.Kind,获取变量的类别,返回的是一个常量

image-20220827105639952

  1. Type 和 Kind 的区别

Type 是类型, Kind 是类别, Type 和 Kind 可能是相同的,也可能是不同的.
比如(相同): var num int = 10, num的Type是int , Kind也是int
比如(不同): var stu Student, stu 的 Type 是 pkg1.Student , Kind 是 struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"reflect"
)

func main() {
var n int64 = 10
nType := reflect.TypeOf(n)
nKind := reflect.Kind(n)

fmt.Printf("n的Type=%v n的Kind=%v\n", nType, nKind)
}

执行结果

1
n的Type=int64 n的Kind=uint32
  1. 通过反射可以在让变量在 interface{}reflect.Value 之间相互转换

  2. 使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如 x 是 int类型,那么就应该使用 reflect.Value(x).Int(), 而不能使用其他的,否则报panic

  3. 通过反射的来修改变量, 注意当使用 SetXxx 方法来设置需要通过对应的指针类型来完成, 这样才能改变传入的变量的值, 同时需要使用到 reflect.Value.Elem()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"reflect"
)

func testInt(b interface{}) {
val := reflect.ValueOf(b)
fmt.Printf("val type=%T\n", val)
val.Elem().SetInt(110) //修改传入的值
fmt.Printf("val=%v\n", val)
}

func main() {
var num int = 20
testInt(&num) //指向地址
fmt.Println("num=", num)
}

执行结果

1
2
3
val type=reflect.Value
val=0xc0000140a0
num= 110
  1. reflect.Value.Elem() 应该如何理解?
  • Elem() 用于获取指针指向变量

反射最佳实践

使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package main

import (
"fmt"
"reflect"
)

//定义一个 Monster 结构体
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32 `json:"成绩"`
Sex string `json:"性别"`
}

//方法: 返回两个数的和
func (s Monster) GetSum(n1, n2 int) int {
return n1 + n2
}

//方法: 接收四个值,给 s 赋值
func (s Monster) Set(name string, age int, score float32, sex string) {
s.Name = name
s.Age = age
s.Score = score
s.Sex = sex
}

//方法: 显示 s 的值
func (s Monster) Print() {
fmt.Println("----start----")
fmt.Println(s)
fmt.Println("-----end-----")
}

func TestStruct(a interface{}) {
//获取 reflect.Type 的类型
typ := reflect.TypeOf(a)
//获取 reflect.Value 的类型
val := reflect.ValueOf(a)
//获取到 a 对应的类别
kd := val.Kind()
//如果传入的不是struct,就退出
if kd != reflect.Struct {
fmt.Println("expect struct")
return
}

//获取到该结构体有几个字段
num := val.NumField()

fmt.Printf("struct has %d fields\n", num) //4
//变量结构体的所有字段
for i := 0; i < num; i++ {
fmt.Printf("Field %d: 值为=%v\n", i, val.Field(i))
//获取到 struct 标签, 注意需要通过 reflect.Type 来获取 tag 标签的值
tagVal := typ.Field(i).Tag.Get("json")
//如果该字段于 tag 标签就显示, 否则就不显示
if tagVal != "" {
fmt.Printf("Field %d: tag为=%v\n", i, tagVal)
}
}

//获取到该结构体有多少个方法
numOfMethod := val.NumMethod()
fmt.Printf("struct has %d method\n", numOfMethod)

//var params []reflect.Value
//方法的排序默认是按照 函数名 的排序(ASCII码)
val.Method(1).Call(nil) //获取到第二个方法,调用它

//调用结构体的第一个方法 Method(0)
var params []reflect.Value //声明了 []reflect.Value
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(40))
res := val.Method(0).Call(params) //传入的参数是 []reflect.Value, 返回 []reflect.Value
fmt.Println("res=", res[0].Int()) //返回结果, 返回的结果是 []reflect.Value*

}

func main() {
//创建一个 Monster 实例
var a Monster = Monster{
Name: "黄皮子",
Age: 400,
Score: 30.8,
}
//将 Monster 实例传递给 TestStruct 函数
TestStruct(a)
}

image-20220827155409764

个人练习

要求:

  1. 编写一个 Cal 结构体,有两个字段 Num1和Num2
  2. Cal的方法 GetSub(name string)
  3. 使用反射遍历 Cal 结构体所有的字段信息
  4. 使用反射机制完成对 GetSub 的调用, 输出形式为 “tom完成了减法运行, 8 - 3 = 5”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"fmt"
"reflect"
)

type Cal struct {
Num1 int `json:"num1"`
Num2 int `json:"num2"`
}

//方法: 两数相减
func (c Cal) GetSub(name string) {
res := c.Num1 - c.Num2
fmt.Printf("%v完成了减法运行, %d - %d = %d\n",name, c.Num1, c.Num2, res)
}

func TestReflect(inter interface{}) {
interType := reflect.TypeOf(inter)
interValue := reflect.ValueOf(inter)
interKind := interValue.Kind()
if interKind != reflect.Struct {
fmt.Println("不是结构体!")
return
}

num := interValue.NumField()
fmt.Printf("struct has %d fields\n", num)
for i := 0; i < num; i++ {
fmt.Printf("Field %d: 值为=%v\n", i, interValue.Field(i))
//获取到 struct 标签, 注意需要通过 reflect.Type 来获取 tag 标签的值
tagVal := interType.Field(i).Tag.Get("json")
//如果该字段于 tag 标签就显示, 否则就不显示
if tagVal != "" {
fmt.Printf("Field %d: tag为=%v\n", i, tagVal)
}
}

//获取到该结构体有多少个方法
numOfMethod := interValue.NumMethod()
fmt.Printf("struct has %d method\n", numOfMethod)

var na []reflect.Value
na = append(na, reflect.ValueOf("tom"))
interValue.Method(0).Call(na)
}

func main() {
var a Cal = Cal{
Num1: 20,
Num2: 10,
}
TestReflect(a)
}

image-20220827164807228

TCP编程

Golang 的主要设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端程序必不可少也是至关重要的一部分。

网络编程有两种:

1) TCPsocket编程,是网络编程的主流。之所以叫Tcpsocket编程,是因为底层是基于Tcp/ip协
议的. 比如: QQ 聊天
2) b/s结构的http编程,我们使用浏览器去访问服务器时,使用的就是http协议,而http底层依
旧是用 tcp socket 实现的。 比如: 京东商城 【这属于 go web 开发范畴 】

TCP/IP协议

TCP/IP(Transmission Control Protocol/Internet Protocol)的简写,中文译名为传输控制协议/因特网互联协议,又叫网络通讯协议,这个协议是 Internet 最基本的协议、Internet 国际互联网络的基础,简单地说,就是由网络层的 IP 协议和传输层的 TCP 协议组成的。

OSI与TCP/IP参考模型

image-20220830095641831

image-20220830101617864

端口

  • 0号端口是保留端口
  • 1-1024是固定端口(程序员不要使用)
    • 22 => SSH远程登录协议
    • 23 => telnet
    • 21 => ftp
    • 25 => smtp
    • 80 => iis
    • 7 => echo
  • 1025-65535是动态端口(程序员可以使用)

TCP-SOCKET编程快速入门

服务端的处理流程

  1. 监听端口 8888
  2. 接收客户端的 TCP 连接, 建立客户端和服务器端的链接
  3. 创建 goroutine, 处理该链接的请求(通常客户端会通过链接发送请求包)

客户端的处理流程

  1. 建立与服务端的链接
  2. 发送请求数据(终端), 接收服务器端返回的结果数据
  3. 关闭链接

流程示意图

image-20220830103724207

代码实现

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
"fmt"
"net"
)

func process(conn net.Conn) {

defer conn.Close()

for {

//创建一个新的切片
buf := make([]byte, 1024)
//1.等待客户端通过 conn 发送信息
//2.如果客户端没有 write[发送],那么协程就阻塞在这里
fmt.Printf("服务器在等待客户端 %s 发送信息\n", conn.RemoteAddr().String())
n, err := conn.Read(buf) //从 conn 读取
if err != nil {
fmt.Printf("客户端退出 err=%v\n", err)
return
}
//3.显示客户端发送的内容到服务器的终端
fmt.Print(string(buf[:n]))
}
}

func main() {
fmt.Println("服务器开始监听...")
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Printf("listen err=%v\n", err)
return
}
defer listen.Close() //延时关闭listen

//循环等待客户端来链接我
for {
//等待客户端链接
fmt.Println("等待客户端来连接...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err=", err)
} else {
fmt.Printf("Accept() suc con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
}

//这里准备一个协程, 为客户端服务
go process(conn)

}

//fmt.Printf("listen suc=%v\n", listen)

}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"bufio"
"fmt"
"net"
"os"
"strings"
)

func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("client dial err=", err)
return
}
//功能一: 客户端可以发送单行数据, 然后就退出
reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]

for {
//从终端读取一行用户数据, 并准备发送给服务器
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}

//如果用户输入的是 exit 就退出
line = strings.Trim(line, "\r\n")
if line == "exit" {
fmt.Println("客户端退出...")
break
}

//再将 line 发送给服务器
_, err = conn.Write([]byte(line + "\n"))
if err != nil {
fmt.Println("conn write err=", err)
}
}
}

海量用户即时通讯系统

开发流程

  1. 需求分析
  2. 设计阶段
  3. 编码实现
  4. 测试阶段
  5. 实施

需求分析

  • 用户注册

  • 用户登录

  • 显示在线用户列表 4) 群聊(广播)

  • 点对点聊天

  • 离线留言

    界面设计

image-20220831101000696

数据库选择

项目要保存用户信息和消息数据,因此需要使用到数据库,这里我们选择使用 Redis 数据库

image-20220831101620024

实现功能

客户端登录菜单

  • 功能: 显示客户端菜单
  • 界面
image-20220831101842927
  • 二级菜单

    • 显示在线用户列表
    • 发送信息
    • 信息列表
    • 退出系统
  • 代码实现

client/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
"fmt"
)

//用户ID
var userId string

//用户密码
var userPwd string

func main() {
//接收用户的选择
var key string
//判断是否继续显示菜单
var loop = true

for {
fmt.Println("--------------------欢迎登录多人聊天系统--------------------")
fmt.Println("\t\t\t1 登录聊天室")
fmt.Println("\t\t\t2 注册用户")
fmt.Println("\t\t\tq 退出系统")
fmt.Println("请选择进行的操作[1|2|q]:")
fmt.Scanf("%v\n", &key)
switch key {
case "1":
fmt.Println("登录聊天室")
case "2":
fmt.Println("注册用户")
case "q":
fmt.Println("退出系统")
loop = false
default:
fmt.Println("输入有误, 请重新输入!")
}
if !loop {
break
}

if key == "1" { //用户登录
fmt.Println("请输入用户的Id:")
fmt.Scanf("%v\n", &userId)
fmt.Println("请输入用户的密码:")
fmt.Scanf("%v\n", &userPwd)

err := login(userId, userPwd)
if err != nil {
panic(err)
} else {
fmt.Println("登录成功")
}
}
}

}

client/login.go

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
)

func login(userId string, userPwd string) (err error) {
fmt.Printf("userId=%v, userPwd=%v\n", userId, userPwd)
return nil
}

image-20220902132309821

客户端用户登录

步骤一

思路分析:

  1. 先确定消息 Message 的格式和结构

  2. 然后根据上图的分析完成代码

image-20220902154840174

server/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
"encoding/binary"
"fmt"
"net"
"redis/src/golang_study/18-tcp/tcpdemo/common/message"
)


// 处理客户端的通讯
func proc(conn net.Conn) {
//延时关闭conn
defer conn.Close()

buf := make([]byte, 4096)

//循环接收客户端发送的信息
for {
fmt.Println("读取客户端发送的数据...")
n, err := conn.Read(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Read err=", err)
return
}
fmt.Println("读取到的buf=", buf[:4])
}
}

func main() {
fmt.Println("服务器在 8889 端口监听中...")
listen, err := net.Listen("tcp", "0.0.0.0:8889")
if err != nil {
fmt.Println("net.Listen err=", err)
return
}
defer listen.Close()

//一旦监听成功, 就等待客户端来链接服务器
for {
fmt.Println("等待客户端来链接服务器...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("net.Accept err=", err)
}

//一旦链接成功, 就启动一个协程和客户端保持通讯
go proc(conn)
}

}

common/message/message.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package message

const (
LoginMesType = "LoginMes"
LoginResMesType = "LoginResMes"
)

type Message struct {
Type string `json:"type"` //消息类型
Data string `json:"data"` //消息的类型
}

type LoginMes struct {
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
}

type LoginResMes struct {
Code int `json:"code"` // 返回状态码 500 表示该用户未注册, 200 表示登录成功
Error string `json:"error"` //返回错误信息
}

client/main.go
和前面的代码一样,没有修改

client/login.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"redis/src/golang_study/18-tcp/tcpdemo/common/message"
)

func login(userId int, userPwd string) (err error) {
// fmt.Printf("userId=%v, userPwd=%v\n", userId, userPwd)
// return nil

// 1. 链接到服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()

// 2. 准备通过 conn 发送消息给服务
var mes message.Message
mes.Type = message.LoginMesType

// 3. 创建一个 LoginMes 结构体
var loginMes message.LoginMes
loginMes.UserId = userId
loginMes.UserPwd = userPwd

// 4. 将 loginMes 序列化
data, err := json.Marshal(loginMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}

// 5. 把data 赋值给 mes.Data 字段
mes.Data = string(data)

// 6. 将 mes 进行序列化操作
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}

// 7. 到这个时候, data 就是我们要发送的信息
// 先把 data 的长度发送给服务器
// 先获取到 data 的长度 => 转成一个表示长度的 byte 切片
pkgLen := uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[:4], pkgLen)

// 发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

fmt.Printf("客户端, 发送消息的长度=%d 内容=%s", len(data), string(data))
return

}

步骤二

接下来需要完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes), 判断用户的合法性,并返回相应的 LoginResMes
思路分析:

  1. 让客户端发送消息本身
  2. 服务器端接受到消息, 然后反序列化成对应的消息结构体.
  3. 服务器端根据反序列化成对应的消息, 判断是否登录用户是合法, 返回 LoginResMes
  4. 客户端解析返回的 LoginResMes,显示对应界面
  5. 这里我们需要做函数的封装
  • 代码实现:

修改client/login.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package main

import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"redis/src/golang_study/18-tcp/tcpdemo/common/message"
"time"
)

func login(userId int, userPwd string) (err error) {
// fmt.Printf("userId=%v, userPwd=%v\n", userId, userPwd)
// return nil

// 1. 链接到服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()

// 2. 准备通过 conn 发送消息给服务
var mes message.Message
mes.Type = message.LoginMesType

// 3. 创建一个 LoginMes 结构体
var loginMes message.LoginMes
loginMes.UserId = userId
loginMes.UserPwd = userPwd

// 4. 将 loginMes 序列化
data, err := json.Marshal(loginMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}

// 5. 把data 赋值给 mes.Data 字段
mes.Data = string(data)

// 6. 将 mes 进行序列化操作
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}

// 7. 到这个时候, data 就是我们要发送的信息
// 先把 data 的长度发送给服务器
// 先获取到 data 的长度 => 转成一个表示长度的 byte 切片
pkgLen := uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[:4], pkgLen)

// 发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

//发送消息本身
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

// 休眠 20 秒
time.Sleep(20 * time.Second)
fmt.Println("休眠了 20 秒")

// fmt.Printf("客户端, 发送消息的长度=%d 内容=%s", len(data), string(data))
return

}

代码解释:

下图为 login.go 中修改/添加的部分

image-20220903145227916

修改 server/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package main

import (
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net"
"redis/src/golang_study/18-tcp/tcpdemo/common/message"
)

func readPkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 4096)

fmt.Println("读取客户端发送的数据...")
_, err = conn.Read(buf[:4])
if err != nil {
fmt.Println("conn.Read err=", err)
return
}

// 根据 buf[:4] 转成一个 uint32 类型
// var pkgLen uint32
pkgLen := binary.BigEndian.Uint32(buf[:4])
fmt.Println("读取到的buf=", buf[:4])

// 根据 pkgLen 读取消息内容
n, err := conn.Read(buf[:pkgLen])
if uint32(n) != pkgLen || err != nil {
fmt.Println("conn.Read fail err=", err)
return
}

// 将 pkgLen 反序列化为 message.Message
// 官方解释:
// func Unmarshal(data []byte, v interface{}) error
// Unmarshal函数解析json编码的数据并将结果存入v指向的值。
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.unmarshal fail err=", err)
return
}

return mes, err
}

// 处理客户端的通讯
func proc(conn net.Conn) {
//延时关闭conn
defer conn.Close()

//循环接收客户端发送的信息
for {
// 这里我们将读取数据包封装成一个函数 readPkg, 返回 Message, err
msg, err := readPkg(conn)
if err != nil {
if err == io.EOF {
fmt.Println("客户端已经退出, 服务端自动退出...")
return
} else {
fmt.Println("readPkg err=", err)
return
}
}

//输出客户端发送的信息
fmt.Println("msg=", msg)
}

}

func main() {
fmt.Println("服务器在 8889 端口监听中...")
listen, err := net.Listen("tcp", "0.0.0.0:8889")
if err != nil {
fmt.Println("net.Listen err=", err)
return
}
defer listen.Close()

//一旦监听成功, 就等待客户端来链接服务器
for {
fmt.Println("等待客户端来链接服务器...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("net.Accept err=", err)
}

//一旦链接成功, 就启动一个协程和客户端保持通讯
go proc(conn)
}

}

代码解释:

下图为 server/ming.go 中新增的函数

image-20220903145524274

新增的 readPkg 函数封装了以前函数 proc 中for循环中的内容,目前的内容如下图

image-20220903145629681

代码测试执行结果如下图

image-20220903150206050

步骤三
  • 能够完成登录,并提示相应信息

修改 common/message/message.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package message

const (
LoginMesType = "LoginMes"
LoginResMesType = "LoginResMes"
RegisterMesType = "RegisterMes"
)

type Message struct {
Type string `json:"type"` //消息类型
Data string `json:"data"` //消息的类型
}

type LoginMes struct {
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
}

type LoginResMes struct {
Code int `json:"code"` // 返回状态码 500 表示该用户未注册, 200 表示登录成功
Error string `json:"error"` //返回错误信息
}

//注册逻辑
type RegisterMes struct {
//...
}

修改server/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package main

import (
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net"
"redis/src/golang_study/18-tcp/tcpdemo/common/message"
)

func readPkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 4096)

fmt.Println("读取客户端发送的数据...")
_, err = conn.Read(buf[:4])
if err != nil {
fmt.Println("conn.Read err=", err)
return
}

// 根据 buf[:4] 转成一个 uint32 类型
// var pkgLen uint32
pkgLen := binary.BigEndian.Uint32(buf[:4])
fmt.Println("读取到的buf=", buf[:4])

// 根据 pkgLen 读取消息内容
n, err := conn.Read(buf[:pkgLen])
if uint32(n) != pkgLen || err != nil {
fmt.Println("conn.Read fail err=", err)
return
}

// 将 pkgLen 反序列化为 message.Message
// 官方解释:
// func Unmarshal(data []byte, v interface{}) error
// Unmarshal函数解析json编码的数据并将结果存入v指向的值。
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.unmarshal fail err=", err)
return
}

return mes, err
}

func writePkg(conn net.Conn, data []byte) (err error) {

//先发送一个长度给对方
pkgLen := uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[:4], pkgLen)

// 发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

//发送消息本身
n, err = conn.Write(data)
if n != int(pkgLen) || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

return
}



// 处理客户端的通讯
func proc(conn net.Conn) {
//延时关闭conn
defer conn.Close()

//循环接收客户端发送的信息
for {
// 这里我们将读取数据包封装成一个函数 readPkg, 返回 Message, err
mes, err := readPkg(conn)
if err != nil {
if err == io.EOF {
fmt.Println("客户端已经退出, 服务端自动退出...")
return
} else {
fmt.Println("readPkg err=", err)
return
}
}

//输出客户端发送的信息
// fmt.Println("msg=", msg)
err = ServerProcessMes(conn, &mes)
if err != nil {
return
}
}
}

//编写一个 ServerProcessLogin 函数, 专门处理登录请求
func ServerProcessLogin(conn net.Conn, mes *message.Message) (err error) {

//核心代码
//1. 先从mes中取出mes.Data, 并直接反序列化成LoginMes
var loginMes message.LoginMes
err = json.Unmarshal([]byte(mes.Data), &loginMes)
if err != nil {
fmt.Println("json.Unmarshal fail err=", err)
return
}
//2.声明一个 resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
//3. 声明一个 LoginResMes, 并完成赋值
var loginResMes message.LoginResMes
//4. 如果用户 id=100, 密码=123456, 认为合法, 否则不合法
if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
//合法
loginResMes.Code = 200
} else {
//不合法
loginResMes.Code = 500 // 500状态码, 表示该用户不存在
loginResMes.Error = "该用户不存在, 请注册再使用"
}
//5. 将 loginResMes 序列化
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("json.Marshal fail err=", err)
return
}
//6. 将 data 赋值给 resMes
resMes.Data = string(data)

//7. 对 resMes 进行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("json.Marshal fail err=", err)
return
}
//8. 发送data, 我们将其封装到writePkg函数
err = writePkg(conn, data)
return
}

//编写一个 ServerProcessMes 函数
//功能: 根据客户端发送消息种类不同, 来决定调用哪个函数来处理
func ServerProcessMes(conn net.Conn, mes *message.Message) (err error) {

switch mes.Type {
case message.LoginMesType:
//处理登录
err = ServerProcessLogin(conn, mes)
case message.RegisterMesType:
//处理注册
default:
fmt.Println("消息类型不存在, 无法处理...")
}

return
}

func main() {
fmt.Println("服务器在 8889 端口监听中...")
listen, err := net.Listen("tcp", "0.0.0.0:8889")
if err != nil {
fmt.Println("net.Listen err=", err)
return
}
defer listen.Close()

//一旦监听成功, 就等待客户端来链接服务器
for {
fmt.Println("等待客户端来链接服务器...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("net.Accept err=", err)
}

//一旦链接成功, 就启动一个协程和客户端保持通讯
go proc(conn)
}

}

下图为server/main.go代码中在之前基础上增加的内容

image-20220905115238000

image-20220905115344619

image-20220905115433930

在client下新建 utils.go 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"redis/src/golang_study/18-tcp/tcpdemo/common/message"
)

func readPkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 4096)

fmt.Println("读取客户端发送的数据...")
_, err = conn.Read(buf[:4])
if err != nil {
fmt.Println("conn.Read err=", err)
return
}

// 根据 buf[:4] 转成一个 uint32 类型
// var pkgLen uint32
pkgLen := binary.BigEndian.Uint32(buf[:4])
fmt.Println("读取到的buf=", buf[:4])

// 根据 pkgLen 读取消息内容
n, err := conn.Read(buf[:pkgLen])
if uint32(n) != pkgLen || err != nil {
fmt.Println("conn.Read fail err=", err)
return
}

// 将 pkgLen 反序列化为 message.Message
// 官方解释:
// func Unmarshal(data []byte, v interface{}) error
// Unmarshal函数解析json编码的数据并将结果存入v指向的值。
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.unmarshal fail err=", err)
return
}

return mes, err
}

func writePkg(conn net.Conn, data []byte) (err error) {

//先发送一个长度给对方
pkgLen := uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[:4], pkgLen)

// 发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

//发送消息本身
n, err = conn.Write(data)
if n != int(pkgLen) || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

return
}

修改 client/login.go 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package main

import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"redis/src/golang_study/18-tcp/tcpdemo/common/message"
)

func login(userId int, userPwd string) (err error) {
// fmt.Printf("userId=%v, userPwd=%v\n", userId, userPwd)
// return nil

// 1. 链接到服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()

// 2. 准备通过 conn 发送消息给服务
var mes message.Message
mes.Type = message.LoginMesType

// 3. 创建一个 LoginMes 结构体
var loginMes message.LoginMes
loginMes.UserId = userId
loginMes.UserPwd = userPwd

// 4. 将 loginMes 序列化
data, err := json.Marshal(loginMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}

// 5. 把data 赋值给 mes.Data 字段
mes.Data = string(data)

// 6. 将 mes 进行序列化操作
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}

// 7. 到这个时候, data 就是我们要发送的信息
// 先把 data 的长度发送给服务器
// 先获取到 data 的长度 => 转成一个表示长度的 byte 切片
pkgLen := uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[:4], pkgLen)

// 发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

//发送消息本身
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

// 休眠 20 秒
// time.Sleep(20 * time.Second)
// fmt.Println("休眠了 20 秒")

// fmt.Printf("客户端, 发送消息的长度=%d 内容=%s", len(data), string(data))

// 处理服务器端返回的信息
mes, err = readPkg(conn)
if err != nil {
fmt.Println("readPkg(conn) err=", err)
return
}

// 将 mes 的Data部分反序列化成 LoginResMes
var loginResMes message.LoginResMes
err = json.Unmarshal([]byte(mes.Data), &loginResMes)
if loginResMes.Code == 200 {
fmt.Println("登录成功")
} else if loginResMes.Code == 500 {
fmt.Println(loginResMes.Error)
}

return

}

下图为 client/login.go 中在原来的基础上所增加的内容

image-20220905115811096

执行测试结果:

client端

image-20220905115949656

server端

image-20220905120005234

步骤四
  • 程序代码改进

前面的程序虽然完成了功能,但是没有结构,系统的可读性、扩展性和维护性都不好,因此需要对程序的结构进行改进。

先改进服务端, 先画出程序的框架图[思路],再写代码

image-20220906100938734

  • 步骤
  1. 先把分析出来的文件,创建好,然后放到相应的文件夹[包]

image-20220906113628634

  1. 现在根据各个文件,完成的任务不同,将 main.go 的代码剥离到对应的文件中即可

  2. 修改server/utils/utils.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package utils

import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"golang_study/18-tcp/tcpdemo/common/message"
)

// 将这些方法关联到结构体中
type Transfer struct {
Conn net.Conn
Buf [4096]byte //这时传输时,使用缓存
}

func (t *Transfer) ReadPkg() (mes message.Message, err error) {
buf := make([]byte, 4096)

fmt.Println("读取客户端发送的数据...")
_, err = t.Conn.Read(buf[:4])
if err != nil {
fmt.Println("conn.Read err=", err)
return
}

// 根据 buf[:4] 转成一个 uint32 类型
// var pkgLen uint32
pkgLen := binary.BigEndian.Uint32(buf[:4])
fmt.Println("读取到的buf=", buf[:4])

// 根据 pkgLen 读取消息内容
n, err :=t.Conn.Read(buf[:pkgLen])
if uint32(n) != pkgLen || err != nil {
fmt.Println("conn.Read fail err=", err)
return
}

// 将 pkgLen 反序列化为 message.Message
// 官方解释:
// func Unmarshal(data []byte, v interface{}) error
// Unmarshal函数解析json编码的数据并将结果存入v指向的值。
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.unmarshal fail err=", err)
return
}

return
}

func (t *Transfer) WritePkg(data []byte) (err error) {

//先发送一个长度给对方
pkgLen := uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[:4], pkgLen)

// 发送长度
n, err := t.Conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

//发送消息本身
n, err = t.Conn.Write(data)
if n != int(pkgLen) || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

return
}
  1. 修改server/process/userProcess.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package process

import (
"encoding/json"
"fmt"
"golang_study/18-tcp/tcpdemo/common/message"
"golang_study/18-tcp/tcpdemo/server/utils"
"net"
)

type UserProcess struct {
Conn net.Conn
Buf [4096]byte
}

//编写一个 ServerProcessLogin 函数, 专门处理登录请求
func (u *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {

//核心代码
//1. 先从mes中取出mes.Data, 并直接反序列化成LoginMes
var loginMes message.LoginMes
err = json.Unmarshal([]byte(mes.Data), &loginMes)
if err != nil {
fmt.Println("json.Unmarshal fail err=", err)
return
}
//2.声明一个 resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
//3. 声明一个 LoginResMes, 并完成赋值
var loginResMes message.LoginResMes
//4. 如果用户 id=100, 密码=123456, 认为合法, 否则不合法
if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
//合法
loginResMes.Code = 200
} else {
//不合法
loginResMes.Code = 500 // 500状态码, 表示该用户不存在
loginResMes.Error = "该用户不存在, 请注册再使用"
}
//5. 将 loginResMes 序列化
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("json.Marshal fail err=", err)
return
}
//6. 将 data 赋值给 resMes
resMes.Data = string(data)

//7. 对 resMes 进行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("json.Marshal fail err=", err)
return
}
//8. 发送data, 我们将其封装到writePkg函数
//因为使用分层模式(mvc), 我们先创建一个 Transfer 实例, 然后读取
tf := &utils.Transfer{
Conn: u.Conn,
}
err = tf.WritePkg(data)
return
}
  1. 修改server/main/processor.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"golang_study/18-tcp/tcpdemo/common/message"
"golang_study/18-tcp/tcpdemo/server/process"
"golang_study/18-tcp/tcpdemo/server/utils"
"io"
"net"
)

type Processor struct {
Conn net.Conn
}

//编写一个 ServerProcessMes 函数
//功能: 根据客户端发送消息种类不同, 来决定调用哪个函数来处理
func (p *Processor) ServerProcessMes(mes *message.Message) (err error) {

switch mes.Type {
case message.LoginMesType:
//处理登录
//创建一个 UserProcess 实例
up := &process.UserProcess{
Conn: p.Conn,
}
err = up.ServerProcessLogin(mes)
case message.RegisterMesType:
//处理注册
default:
fmt.Println("消息类型不存在, 无法处理...")
}

return
}

func (p *Processor) Process_main() (err error) {
//循环的客户端发送的信息
for {
//这里我们将读取数据包, 直接封装成一个函数 readPkg(), 返回 Message, Err
//创建一个 Transfer 实例完成读包任务
tf := &utils.Transfer{
Conn: p.Conn,
}
mes, err := tf.ReadPkg()
if err != nil {
if err == io.EOF {
fmt.Println("客户端退出, 服务器端也退出...")
return err
} else {
fmt.Println("readPkg err=", err)
return err
}
}
err = p.ServerProcessMes(&mes)
if err != nil {
return err
}
}
}
  1. 修改server/main/main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"fmt"
"net"
)

// 处理客户端的通讯
func proc(conn net.Conn) {
//延时关闭conn
defer conn.Close()

//这里调用总控,创建一个
processor := &Processor{
Conn: conn,
}

err := processor.Process_main()
if err != nil {
fmt.Println("客户端和服务器通讯协程错误=err", err)
return
}
}

func main() {
fmt.Println("服务器在 8889 端口监听中...")
listen, err := net.Listen("tcp", "0.0.0.0:8889")
if err != nil {
fmt.Println("net.Listen err=", err)
return
}
defer listen.Close()

//一旦监听成功, 就等待客户端来链接服务器
for {
fmt.Println("等待客户端来链接服务器...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("net.Accept err=", err)
}
//一旦链接成功, 就启动一个协程和客户端保持通讯
go proc(conn)
}
}
步骤五

修改客户端代码

  1. 示意图如下:

image-20220907134930899

  1. 先把各个文件放到对应的文件夹[包]

image-20220907135958775

  1. 将 server/utils.go 拷贝到 client/utils/utils.go

  2. 创建了server/process/userProcess.go

说明: 该文件就是在原来的 login.go 做了一个改进,即封装到 UserProcess 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package process

import (
"encoding/binary"
"encoding/json"
"fmt"
"golang_study/18-tcp/tcpdemo/client/utils"
"golang_study/18-tcp/tcpdemo/common/message"
"net"
)

type UserProcess struct {
//暂时不需要字段
}

//给关联一个用户登录的方法
//写一个函数, 完成登录
func (u *UserProcess) Login(userId int, userPwd string) (err error) {

// 1. 链接到服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()

// 2. 准备通过 conn 发送消息给服务
var mes message.Message
mes.Type = message.LoginMesType

// 3. 创建一个 LoginMes 结构体
var loginMes message.LoginMes
loginMes.UserId = userId
loginMes.UserPwd = userPwd

// 4. 将 loginMes 序列化
data, err := json.Marshal(loginMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}

// 5. 把data 赋值给 mes.Data 字段
mes.Data = string(data)

// 6. 将 mes 进行序列化操作
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}

// 7. 到这个时候, data 就是我们要发送的信息
// 先把 data 的长度发送给服务器
// 先获取到 data 的长度 => 转成一个表示长度的 byte 切片
pkgLen := uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[:4], pkgLen)

// 发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

//发送消息本身
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}

// 休眠 20 秒
// time.Sleep(20 * time.Second)
// fmt.Println("休眠了 20 秒")

// fmt.Printf("客户端, 发送消息的长度=%d 内容=%s", len(data), string(data))

ft := utils.Transfer{
Conn: conn,
}

// 处理服务器端返回的信息
mes, err = ft.ReadPkg()
if err != nil {
fmt.Println("readPkg(conn) err=", err)
return
}

// 将 mes 的Data部分反序列化成 LoginResMes
var loginResMes message.LoginResMes
err = json.Unmarshal([]byte(mes.Data), &loginResMes)
if loginResMes.Code == 200 {
fmt.Println("登录成功")
//循环显示登陆成功后的菜单
for {
ShowMenu()
}
} else if loginResMes.Code == 500 {
fmt.Println(loginResMes.Error)
}

return

}

上面代码中变化的内容如下图

image-20220908150825455

  1. 创建了server/process/server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package process

import (
"fmt"
"golang_study/18-tcp/tcpdemo/server/utils"
"net"
"os"
)

//显示登录成功后的界面
func ShowMenu() {
fmt.Println("----------恭喜xxx登录成功----------")
fmt.Println("----------1. 显示在线用户列表----------")
fmt.Println("----------2. 发送消息----------")
fmt.Println("----------3. 信息列表----------")
fmt.Println("----------4. 退出系统----------")
fmt.Println("请选择(1-4):")
var key int
fmt.Scanf("%d\n", &key)
switch key {
case 1:
fmt.Println("显示在线用户列表")
case 2:
fmt.Println("发送消息")
case 3:
fmt.Println("信息列表")
case 4:
fmt.Println("退出系统")
os.Exit(0)
default:
fmt.Println("你输入的选项不正确!")
}
}

//和服务器保持通讯
func ServerProcessMes(conn net.Conn) {
//创建一个 transfer 实例, 不停的读取服务器发送的消息
tf := &utils.Transfer{
Conn: conn,
}
for {
fmt.Println("客户端正在等待读取服务器发送的消息")
mes, err := tf.ReadPkg()
if err != nil {
fmt.Println("tf.ReadPkg err=", err)
return
}
//如果读取到消息, 又事下一步处理逻辑
fmt.Printf("mes=%v\n", mes)
}
}

上面代码中增加的内容如下图

image-20220908151407158

  1. 修改 client/main/main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"fmt"
"golang_study/18-tcp/tcpdemo/client/process"
)

//用户ID
var userId int

//用户密码
var userPwd string

func main() {
//接收用户的选择
var key string
//判断是否继续显示菜单
var loop = true

for {
fmt.Println("--------------------欢迎登录多人聊天系统--------------------")
fmt.Println("\t\t\t1 登录聊天室")
fmt.Println("\t\t\t2 注册用户")
fmt.Println("\t\t\tq 退出系统")
fmt.Println("请选择进行的操作[1|2|q]:")
fmt.Scanf("%v\n", &key)
switch key {
case "1":
fmt.Println("登录聊天室")
fmt.Println("请输入用户的Id:")
fmt.Scanf("%v\n", &userId)
fmt.Println("请输入用户的密码:")
fmt.Scanf("%v\n", &userPwd)
//完成登录
//1. 创建一个 UserProcess 的实例
up := &process.UserProcess{}
up.Login(userId, userPwd)
case "2":
fmt.Println("注册用户")
case "q":
fmt.Println("退出系统")
loop = false
default:
fmt.Println("输入有误, 请重新输入!")
}
if !loop {
break
}
}
}

上面代码中变化的内容如下图

image-20220908151000190

Redis操作

Redis手动添加用户

示意图如下

image-20220909104859524

json数据格式如下

1
{"type":"LoginMes","data":"{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"poker\"}"}

具体redis操作如下

1
2
3
4
127.0.0.1:6379> hset users 100 "{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"poker\"}"
(integer) 1
127.0.0.1:6379> hget users 100
"{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"poker\"}"
代码实现Redis添加用户
  • 如输入的用户名密码在 Redis 中存在则登录,否则退出系统,并给出相应的提示信息:
  1. 用户不存在,你也可以重新注册,再登录
  2. 输入的密码不正确
  • 首先创建对应的文件

image-20220909112028149

Redis

Redis 是一个NoSql数据库,不是传统的关系型数据库

Redis 全程为 REmote Dictionary Server

Redis性能非常高,单机能够达到 15w qps, 通常用来做缓存,也可以持久化.

Redis是完全开源免费的, 高性能的分布式内存数据库,基于内存运行并支持持久化的NoSql数据库

官网:

操作原理图

image-20220831112133267

Redis的安装

我的笔记本为 MAC OS, 这里只演示 Mac 上安装 Redis 的过程

参考链接: MAC安装redis的简单方法

1
brew install redis@6.2

image-20220831113403174

启动redis

1
brew services start redis

image-20220831113513479

检查端口号6379

1
netstat -AaLlnW | grep 6379

image-20220831113547556

客户端访问

1
2
redis-cli
127.0.0.1:6379>

Redis的基本使用

使用手册参考:Redis命令参考

说明: Redis 安装好后,默认有 16 个数据库,初始默认使用 0 号库, 编号(index)是 0…15

  • 添加 key-val [set]
1
2
127.0.0.1:6379> set key1 hello
OK
  • 查看当前 redis 的 所有 key [keys *]
1
2
3
127.0.0.1:6379> keys *
1) "key2"
2) "key1"
  • 获取key对应的值 [getkey]
1
2
127.0.0.1:6379> get key1
"hello"
  • 切换 redis 数据库 [select index]
1
2
3
4
5
6
7
8
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> get key1
(nil)
127.0.0.1:6379[1]> select 0
OK
127.0.0.1:6379> get key1
"hello"
  • 如何查看当前数据库的key-val数量 [dbsize]
1
2
3
4
127.0.0.1:6379> set key2 imau
OK
127.0.0.1:6379> dbsize
(integer) 2
  • 清空当前数据库的key-val和清空所有数据库的 key-val [flushdb flushall]
    • flushdb => 清空当前index的数据库
    • flushall => 清空左右index的数据库

Redis 的 Crud 操作

Redis 的五大数据类型

Redis 的五大数据类型是: String(字符串) 、Hash (哈希)、List(列表)、Set(集合) 和 zset(sorted set:有序集合)

String(字符串)

string 是 redis 最基本的类型,一个 key 对应一个 value。

string 类型是二进制安全的。除普通的字符串外,也可以存放图片等数据。

redis 中字符串 value 最大是 512M

1
2
3
4
5
6
7
8
9
10
11
# 定义一个key和value
127.0.0.1:6379> set address beijing
OK
# 查询指定的key
127.0.0.1:6379> get address
"beijing"
# 删除指定的key
127.0.0.1:6379> del address
(integer) 1
127.0.0.1:6379> get address
(nil)
  • setex(set with expire)键秒值
1
2
3
4
5
6
7
8
9
10
# setex [key] [n秒] [value]
# 超过 n 秒之后,删除这个key
127.0.0.1:6379> setex mess01 10 helloworld
OK
127.0.0.1:6379> get mess01
"helloworld"
127.0.0.1:6379> get mess01
"helloworld"
127.0.0.1:6379> get mess01
(nil)
  • mset[同时设置一个或多个 key-value 对]

  • mget[同时获取多个 key-value]

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> mset m1 hello m2 world
OK
127.0.0.1:6379> get m1
"hello"
127.0.0.1:6379> get m2
"world"
127.0.0.1:6379> mget m1 m2
1) "hello"
2) "world"

Hash (哈希)

类似 golang 里的 Map

Redis hash 是一个键值对集合。var user1 map[string]string

Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
127.0.0.1:6379> hset user1 name poker
(integer) 1
127.0.0.1:6379> hset user1 age 25
(integer) 1
127.0.0.1:6379> hset user1 job coder
(integer) 1

127.0.0.1:6379> hget user1 name
"poker"
127.0.0.1:6379> hget user1 age
"25"
127.0.0.1:6379> hget user1 job
"coder"

# 查看key中的field个数
127.0.0.1:6379> hlen user2
(integer) 3

127.0.0.1:6379> hgetall user1
1) "name"
2) "poker"
3) "age"
4) "25"
5) "job"
6) "coder"
  • hmset(同时设定多个)
  • hmget(同时查看多个)
1
2
3
4
5
6
127.0.0.1:6379> hmset user2 name jenny age 25 job "good coder "
OK
127.0.0.1:6379> hmget user2 name age job
1) "jenny"
2) "25"
3) "good coder "
  • hexists(判定哈希表中key的field是否存在)
1
2
3
4
5
127.0.0.1:6379> hexists user2 name
(integer) 1
# 0就表示该 field 不存在
127.0.0.1:6379> hexists user2 name1
(integer) 0

List(列表)

列表是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部或者尾部

List 本质是个链表, List 的元素是有序的,元素的值可以重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 从左到右依次传入数据
127.0.0.1:6379> lpush city tinjian beijing shanghai
(integer) 3

127.0.0.1:6379> lrange city 0 0
1) "shanghai"
127.0.0.1:6379> lrange city 0 1
1) "shanghai"
2) "beijing"

# 查看所有field元素
127.0.0.1:6379> lrange city 0 -1
1) "shanghai"
2) "beijing"
3) "tinjian"
# 从右到左依次传入数据
127.0.0.1:6379> rpush city shenzhen nanjing
(integer) 5

127.0.0.1:6379> lrange city 0 -1
1) "shanghai"
2) "beijing"
3) "tinjian"
4) "shenzhen"
5) "nanjing"

127.0.0.1:6379> lpop city
"shanghai"
127.0.0.1:6379> lrange city 0 -1
1) "beijing"
2) "tinjian"
3) "shenzhen"
4) "nanjing"

127.0.0.1:6379> rpop city
"nanjing"
127.0.0.1:6379> lrange city 0 -1
1) "beijing"
2) "tinjian"
3) "shenzhen"

# 查看key的元素个数
127.0.0.1:6379> llen city
(integer) 3

特别说明

  • List 数据可以从左或右添加数据
  • 如果 List 的数据全部移除,那么对应的键也会删除

Set(集合)

  • sadd [添加值]

  • smembers [取出所有值]

  • sismember [判断值是否是成员]

  • srem [删除指定值]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
127.0.0.1:6379> sadd emails tom@sohu.com
(integer) 1

# 判断指定的值是否存在
127.0.0.1:6379> sismember emails tom@sohu.com
(integer) 1
127.0.0.1:6379> sismember emails tom1@sohu.com
(integer) 0


127.0.0.1:6379> sadd emails shang@sohu.com
(integer) 1
127.0.0.1:6379> sadd emails jack@sohu.com
(integer) 1

# 输出所有值
127.0.0.1:6379> smembers emails
1) "shang@sohu.com"
2) "jack@sohu.com"
3) "tom@sohu.com"

# 删除指定值
127.0.0.1:6379> srem emails tom@sohu.com
(integer) 1
127.0.0.1:6379> smembers emails
1) "shang@sohu.com"
2) "jack@sohu.com"

Golang 操作 Redis

安装第三方开源 Redis 库

使用第三方开源的 Redis 库: go-redis

安装方式:

1
2
3
4
5
# 1.切换到当前go文件目录中
# 2.初始化一个 mod 文件
go mod init demo01
# 3.安装 github.com/go-redis/redis
go get -u github.com/go-redis/redis

安装过程碰到了很多莫名其妙的问题,没有来得及记录,有问题及时百度~

image-20220901101256283

注意: 官方源码文档中给出的快速使用示例,代码跟引入的包有些出入,因为go-redis版本原因, 有些地方使用方式不同

注: 以下内容通过官方文档的定义进行的编写

Set/Get-String字符串操作

通过 Golang 添加和获取 key-value

在redis中添加两个键值

1
2
3
4
5
redis-cli
127.0.0.1:6379> set car1 baoma
OK
127.0.0.1:6379> set car2 aodi
OK

快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
"fmt"

"github.com/go-redis/redis"
)

func ExampleClient() {
//连接本地 redis 数据库
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})

defer rdb.Close()

// 添加一对redis的键值
// 最后一个参数是过期。0表示密钥没有过期时间
err := rdb.Set("test", "aaa", 0).Err()
if err != nil {
panic(err)
}
// 输出刚刚上面添加的键值
val, err := rdb.Get("test").Result()
if err != nil {
panic(err)
}
fmt.Println("key", val)

// 输出 car1
val2, err := rdb.Get("car1").Result()
if err == redis.Nil {
fmt.Println("car1 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("car1", val2)
}

//输出 car2
val3, err := rdb.Get("car2").Result()
if err == redis.Nil {
fmt.Println("car2 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("car2", val3)
}

// car3没有添加,所以不存在
val4, err := rdb.Get("car3").Result()
if err == redis.Nil {
fmt.Println("car3 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("car3", val4)
}
}

func main() {
ExampleClient()
}

image-20220901105815244

MSet/MGet操作-String字符串操作

MSet/MGet 数据操作数据

通过 Golang 对 Redis 操作,一次操作可以 Set / Get 多个 key-val 数据

MSet的官方定义如下图, 参数为一个接口类型的切片,我们可以传入一个 string 类型的切片或者使用for循环依次遍历map切片,传入一对键值字符串(我感觉后者更方便管理,代码也更加简洁,一会儿看下面的代码就可以了)

image-20220901134358082

MGet的官方定义如下图, 参数为一个string类型的切片,我们可以传入一个 string 类型的切片

image-20220901141745461

MGet方法的返回值 *SliceCmd是一个空的结构体(众所周知,空的结构体更加灵活,可以随意调用定义的各种函数),里面已经定义了多个函数,其中Result返回值, 返回的是一个接口切片error信息

image-20220901133527851

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
"fmt"

"github.com/go-redis/redis"
)

func ExampleClient() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
defer rdb.Close()
//方式一: 可以使用map切片
strArr := make(map[string]string)
strArr["animal1"] = "Monkey"
strArr["animal2"] = "Elephant"
strArr["animal3"] = "Tiger"
// 添加redis的键值
for i, v := range strArr {
err := rdb.MSet(i, v).Err()
if err != nil {
panic(err)
}
}

//方式二: 也可以使用string类型的切片
//strArr := []string{"animal1", "Monkey", "animal2", "elephant", "animal3", "tiger"}
// 添加redis的键值
// err := rdb.MSet(strArr).Err()
// if err != nil {
// panic(err)
// }

// strArr_key := []string{"animal1", "animal2", "animal3"}

for key := range strArr {
// 输出刚刚上面添加的键值
result, err := rdb.MGet(key).Result()
if err == redis.Nil {
fmt.Println("this key does not exist")
} else if err != nil {
panic(err)
} else {
for _, v := range result {
fmt.Printf("%v=%v\n", key, v)
}
}
}
}

func main() {
ExampleClient()
}

image-20220901141510533

HSet/HGet-Hash操作

通过 Golang 对 Redis 操作 Hash 数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"fmt"

"github.com/go-redis/redis"
)

func ExampleClient() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})

defer rdb.Close()

//1 HSet 添加hash键值
err := rdb.HSet("userinfo", "name", "tom").Err()
if err != nil {
panic(err)
}

err = rdb.HSet("userinfo", "age", "6").Err()
if err != nil {
panic(err)
}

//2 HGet 输出hash键值
res := ""
res, err = rdb.HGet("userinfo", "name").Result()
if err == redis.Nil {
fmt.Println("userinfo name does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("userinfo-name", res)
}

res, err = rdb.HGet("userinfo", "age").Result()
if err == redis.Nil {
fmt.Println("userinfo age does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("userinfo-age", res)
}
}

func main() {
ExampleClient()
}

image-20220901113555207

HMSet/HMGet-Hash操作

对 hash 数据结构,field-val 是批量放入和读取

根据官方文档, HMSet传入的是一个map切片,我们需要自己定义然后传参即可

image-20220901114824427

MHGet方法传参时,传入的是一个 []string数组

image-20220901122947344

HMGet方法的返回值 *SliceCmd是一个空的结构体,里面已经定义了多个函数,其中Result返回值, 返回的是一个接口切片error信息

image-20220901133527851

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
"fmt"

"github.com/go-redis/redis"
)

func ExampleClient() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})

defer rdb.Close()

m := make(map[string]interface{})
m["name"] = "jack"
m["age"] = "7"

//1 HMSet
err := rdb.HMSet("userinfo1", m).Err()
if err != nil {
panic(err)
}

//2 HMGet
// 定义一个string类型切片,指定field值
str := []string{"name", "age"}
// Result返回的是一个 string 类型的 interface 接口
result, err := rdb.HMGet("userinfo1", str...).Result()
if err == redis.Nil {
fmt.Println("userinfo1 does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("userinfo1=", result)
fmt.Printf("result的类型=%T\n", result)
}

// 循环遍历 result ,借助上面定义的 str 输出 userinfo1 的索引与值
fmt.Println("userinfo1的键值内容如下:")
for i, v := range result {
fmt.Printf("%v=%v\n", str[i], v)
}
}

func main() {
ExampleClient()
}

image-20220901133806602

列表操作

列表中比较常用的指令有: LPush/RPush/LInsert/RInsert/LRange遍历 等

  • LPush/RPush

从下图可以得知,values传入的值是一个 接口切片,可以传入任意类型数据,这里我们直接传入任意数量的字符串

image-20220901150522051

image-20220901150655299

可以使用LRange遍历列表的数据

image-20220901151310746

  • LInsert/RInsert

下面图中的定义可能会有人看不懂

  • key => 键值
  • op => after/before
  • pivot => 指定的数据位置
  • value => 需要添加的数组

image-20220901152535204

举个例子

1
2
rdb.LPush("hobby","篮球")
rdb.LInsert("hobby","before","篮球","乒乓球")

上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
"fmt"

"github.com/go-redis/redis"
)

func ExampleClient() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})

defer rdb.Close()

// Lpush向列表中添加数据
strArr1 := []string{"1.唱", "2.跳"}
err := rdb.LPush("蔡徐坤的hobby", strArr1).Err()
if err != nil {
panic(err)
}

// Rpush向列表中添加数据
strArr2 := []string{"3.rap", "4.篮球"}
err = rdb.RPush("蔡徐坤的hobby", strArr2).Err()
if err != nil {
panic(err)
}

// LRange 读取数据
result, err := rdb.LRange("蔡徐坤的hobby", 0, -1).Result()
if err == redis.Nil {
fmt.Println("this key does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("result=", result)
}

// LInsert 在列表中的指定位置添加数据
err = rdb.LInsert("蔡徐坤的hobby", "before", "3.rap", "鸡你太美").Err()
if err != nil {
panic(err)
}

//RInsert 这里不做演示了,原理同上

// LRange 读取数据
result, err = rdb.LRange("蔡徐坤的hobby", 0, -1).Result()
if err == redis.Nil {
fmt.Println("this key does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Println("result=", result)
}

// 删除该键值,方便重复进行调试
rdb.Del("蔡徐坤的hobby")

}

func main() {
ExampleClient()
}

image-20220901153450512

Set-集合操作

SAdd用来向集合中添加键值

image-20220901160841226

SMembers用来输出集合中的数据

image-20220901160942192

SIsMember 用于判断一个值在集合中是否存在,从下图可以看出,该方法返回一个*BoolCmd

image-20220901160501422

*BoolCmdResult方法会返回一个bool类型的值

image-20220901160726249

上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package main

import (
"fmt"

"github.com/go-redis/redis"
)

// 输出集合中的内容
func Smembers(rdb *redis.Client, urls string) []string {
result, err := rdb.SMembers(urls).Result()
if err == redis.Nil {
fmt.Println("this key does not exist")
} else if err != nil {
panic(err)
}
return result
}

//判断集合中某个值是否存在
func SisMember(rdb *redis.Client, urls string, url string) bool {
result, err := rdb.SIsMember(urls, url).Result()
if err == redis.Nil {
fmt.Println("this key does not exist")
} else if err != nil {
panic(err)
}
return result
}

//计算集合的大小
func Scard(rdb *redis.Client, urls string) {
result, err := rdb.SCard("urls").Result()
if err == redis.Nil {
fmt.Println("this key does not exist")
} else if err != nil {
panic(err)
} else {
fmt.Printf("%v 的集合大小=%d\n", urls, result)
}
}

func ExampleClient() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
fmt.Printf("rdb的类型为%T\n", rdb)

defer rdb.Close()

// SAdd 向无序集合中添加数据
err := rdb.SAdd("urls", "www.baidu.com", "www.google.com").Err()
if err != nil {
panic(err)
}

// 输出集合中的内容
res_urls := Smembers(rdb, "urls")
fmt.Println("urls=", res_urls)

//计算集合大小
Scard(rdb, "urls")

//判断集合中某个值是否存在
res_bool := SisMember(rdb, "urls", "www.baidu.com")
if res_bool { // 当 res_bool 为 true 时
fmt.Println("www.baidu.com 存在")
} else {
fmt.Println("www.baidu.com 不存在")
}

//删除指定值
err = rdb.SRem("urls", "www.baidu.com").Err()
if err != nil {
panic(err)
} else {
fmt.Println("www.baidu.com 已被删除")
}

//判断集合中某个值是否存在
res_bool = SisMember(rdb, "urls", "www.baidu.com")
if res_bool { // 当 res_bool 为 true 时
fmt.Println("www.baidu.com 存在")
} else {
fmt.Println("www.baidu.com 不存在")
}

// 输出集合中的内容
res_urls = Smembers(rdb, "urls")
fmt.Println("urls=", res_urls)
}

func main() {
ExampleClient()
}

image-20220901165008732

Redis链接池

说明: 通过 Golang 对 Redis 操作, 还可以通过 Redis 链接池, 流程如下:

1) 事先初始化一定数量的链接,放入到链接池
2) 当 Go 需要操作 Redis 时,直接从 Redis 链接池取出链接即可。

2) 这样可以节省临时获取 Redis 链接的时间,从而提高效率

image-20220902092338955

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import (
"fmt"
"net"
"time"

"github.com/go-redis/redis"
)

// 声明一个全局变量
var pool *redis.Client

func init() {
pool = redis.NewClient(
&redis.Options{
//连接池容量及闲置连接数量
PoolSize: 15, // 连接池最大socket连接数,默认为4倍CPU数, 4 * runtime.NumCPU
MinIdleConns: 10, //在启动阶段创建指定数量的Idle连接,并长期维持idle状态的连接数不少于指定数量

//超时
DialTimeout: 5 * time.Second, //连接建立超时时间,默认5秒。
ReadTimeout: 3 * time.Second, //读超时,默认3秒, -1表示取消读超时
WriteTimeout: 3 * time.Second, //写超时,默认等于读超时
PoolTimeout: 4 * time.Second, //当所有连接都处在繁忙状态时,客户端等待可用连接的最大等待时长,默认为读超时+1秒。

//闲置连接检查包括IdleTimeout,MaxConnAge
IdleCheckFrequency: 60 * time.Second, //闲置连接检查的周期,默认为1分钟,-1表示不做周期性检查,只在客户端获取连接时对闲置连接进行处理。
IdleTimeout: 5 * time.Minute, //闲置超时,默认5分钟,-1表示取消闲置超时检查
MaxConnAge: 0 * time.Second, //连接存活时长,从创建开始计时,超过指定时长则关闭连接,默认为0,即不关闭存活时长较长的连接

//命令执行失败时的重试策略
MaxRetries: 0, // 命令执行失败时,最多重试多少次,默认为0即不重试
MinRetryBackoff: 8 * time.Millisecond, //每次计算重试间隔时间的下限,默认8毫秒,-1表示取消间隔
MaxRetryBackoff: 512 * time.Millisecond, //每次计算重试间隔时间的上限,默认512毫秒,-1表示取消间隔

//可自定义连接函数
Dialer: func() (net.Conn, error) {
netDialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 5 * time.Minute,
}
return netDialer.Dial("tcp", "127.0.0.1:6379")
},

//钩子函数
//仅当客户端执行命令时需要从连接池获取连接时,如果连接池需要新建连接时则会调用此钩子函数
OnConnect: func(conn *redis.Conn) error {
fmt.Printf("conn=%v\n", conn)
return nil
},
},
)
}

func main() {
// 先从 pool 取出一个链接
err := pool.Set("user1", "poker", 0).Err()
if err != nil {
panic(err)
}

result, err := pool.Get("user1").Result()
if err != nil {
panic(err)
}
fmt.Println("result=", result)

}

image-20220902111256486

本文标题:Golang学习笔记

文章作者:尚先森

发布时间:2022年06月02日 - 13:59:21

最后更新:2023年02月20日 - 18:08:05

原始链接:https://imauu.gitee.io/2022/06/02/Golang学习笔记/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

尚先森 wechat
有任何问题可以扫描上方二维码私聊我哦😊
您的支持是我创作的最大动力^_^