Live2d Test Env

Golang核心编程

源码地址:

https://github.com/mikeygithub/GoCode

第1章 1Golang 的学习方向

Go 语言,我们可以简单的写成 Golang

 

1.2Golang 的应用领域

1.2.1区块链的应用开发

1.2.2后台的服务应用

1.2.3云计算/云服务后台应用

 

1.3学习方法的介绍

 

1) 努力做到通俗易懂
2) 注重 Go 语言体系,同时也兼顾技术细节
3) 在实际工作中,如何快速的掌握一个技术的分享,同时也是我们授课的思路(怎么讲解或者学习一个
技术)。(很多学员反馈非常受用

)

 


第2章 Golang 的概述

2.1什么是程序


程序:就是完成某个功能的指令的集合。画一个图理解:

 

 

2.2Go 语言的诞生小故事


2.2.1Go 语言的核心开发团队-三个大牛

2.2.2Google 创造 Golang 的原因

2.2.3Golang 的发展历程

2007 年,谷歌工程师 Rob Pike, Ken Thompson 和 Robert Griesemer 开始设计一门全新的语言,这是Go 语言的最初原型。
2009 年 11 月 10 日,Google 将 Go 语言以开放源代码的方式向全球发布。
2015 年 8 月 19 日,Go 1.5 版发布,本次更新中移除了”最后残余的 C 代码”
2017 年 2 月 17 日,Go 语言 Go 1.8 版发布。
2017 年 8 月 24 日,Go 语言 Go 1.9 版发布。 1.9.2 版本
2018 年 2 月 16 日,Go 语言 Go 1.10 版发布。

2.3Golang 的语言的特点

简介:
Go 语言保证了既能到达静态编译语言的安全和性能,又达到了动态语言开发维护的高效率
,使用一个表达式来形容 Go 语言:Go = C + Python , 说明 Go 语言既有 C 静态语言程
序的运行速度,又能达到 Python 动态语言的快速开发。
1) 从 C 语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型,调用参数传值,指针等
等,也保留了和 C 语言一样的编译执行方式及弱化的指针
举一个案例(体验):
//go 语言的指针的使用特点(体验)

func testPtr(num *int) {
*num = 20
}

2) 引入包的概念,用于组织程序结构,Go 语言的一个文件都要归属于一个包,而不能单独存在。

3) 垃圾回收机制,内存自动回收,不需开发人员管理
4) 天然并发 (重要特点)
(1) 从语言层面支持并发,实现简单
(2) goroutine,轻量级线程,可实现大并发处理,高效利用多核。
(3) 基于 CPS 并发模型(Communicating Sequential Processes )实现
5) 吸收了管道通信机制,形成 Go 语言特有的管道 channel 通过管道 channel , 可以实现不同的 goroute之间的相互通信。
6) 函数可以返回多个值。举例:

//写一个函数,实现同时返回 和,差
//go 函数支持返回多个值
func getSumAndSub(n1 int, n2 int) (int, int ) {
sum := n1 + n2 //go 语句后面不要带分号.
sub := n1 - n2
return sum , sub
}

7) 新的创新:比如切片 slice、延时执行 defer

2.4Golang 的开发工具的介绍

2.4.1工具介绍

2.4.2工具选择:

如何选择开发工具
我们先选择用 visual studio code 或者 vim 文本编辑器本,到大家对 Go 语言有一定了解后,我们再
使用 Eclipse 等 IDE 开发工具。
这是为什么呢?
1) 更深刻的理解 Go 语言技术,培养代码感。->写代码的感觉。
2) 有利于公司面试。-> 给你纸,写程序
2.4.3VSCode 的安装和使用
1) 先到下载地址去选择适合自己系统的 VSCode 安装软件

2) 演示如何在 windows 下安装 vscode 并使用
步骤 1: 把 vscode 安装文件准备好

 

步骤 2:双击安装文件,就可以一步一步安装,我个人的习惯是安装到 d:/programs 目录.
当看到如下界面时,就表示安装成功!

步骤 3: 简单的使用一下 vscode
在 d 盘创建了一个文件夹 gocode.

 


3) 演示如何在 Linux(ubuntu /centos)下安装 vscode 并使用
这里,我们介绍一下我的 linux 的环境:
步骤 1: 先下载 linux 版本的 vscode 安装软件.

步骤 2: 因为我这里使用的是虚拟机的 ubuntu, 因此我们需要先将 vscode 安装软件传输到 ubuntu
下,使用的 xftp5 软件上传。
步骤 3: 如果你是在 ubuntu 下做 go 开发,我们建议将 vscode 安装到 /opt 目录..
步骤 4:将安装软件拷贝到 /opt
步骤 5: cd
/opt 【切换到 /opt】
步骤 6: 将安装文件解决即可

步骤 7: 现在进入解压后的目录,即可运行我们的 vscode


4) 演示如何在 Mac 下安装 vscode 并使用
如果你使用的就是 mac 系统,也可以在该系统下进行 go 开发.
步骤 1:下载 mac 版本的 vscode 安装软件

步骤 2:把 vscode 安装软件,传输到 mac 系统
细节: 在,默认情况下 mac 没有启动 ssh 服务,所以需要我们启动一下,才能远程传输文件.
mac 本身安装了 ssh 服务,默认情况下不会开机自启

1.启动 sshd 服务:
sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist
2.停止 sshd 服务:
sudo launchctl unload -w /System/Library/LaunchDaemons/ssh.plist
3 查看是否启动:
sudo launchctl list | grep ssh

如果看到下面的输出表示成功启动了:

--------------- 0 com.openssh.sshd

步骤 3:将安装软件解压后即可使用.

进入到这个解压后的文件夹(图形界面),双击即可
步骤 4:编写解简单测试.
在用户所在的目录,创建了 gocode,然后将 test.go 写到这个文件夹下 ..

2.4.4小结
我们会讲解在 windows, linux , mac 如何安装 vscode 开发工具,并且还会讲解如何在三个系统下
安装 go 的 sdk 和如何开发 go 程序。
但是为了学习方便,我们前期选择 Windows 下开发 go。到我们开始讲项目和将区块链时,就会使
用 linux 系统。
在实际开发中,也可以在 windows 开发好程序,然后部署到 linux 下。
2.5
Windows 下搭建 Go 开发环境-安装和配置 SDK
2.5.1介绍了 SDK
1) SDK 的全称(Software Development Kit
软件开发工具包)
2) SDK 是提供给开发人员使用的,其中包含了对应开发语言的工具包
2.5.2下载 SDK 工具包
1) Go 语言的官网为:golang.org , 因为各种原因,可能无法访问。
2) SDK 下载地址:Golang 中国
https://www.golangtc.com/download
3) 如何选择对应的 sdk 版本

2.5.3windows 下安装 sdk
1) Windows 下 SDK 的各个版本说明:
Windows 下:根据自己系统是 32 位还是 64 位进行下载:
32 位系统:go1.9.2.windows-386.zip
64 位系统:go1.9.2.windows-amd64.zip
2) 请注意:安装路径不要有中文或者特殊符号如空格等
3) SDK 安装目录建议:windows 一般我安装在 d:/programs
4) 安装时,基本上是傻瓜式安装,解压就可以使用
5) 安装看老师的演示:
6) 解压后,我们会看到 d:/go 目录,这个是 sdk

如何测试我们的 go 的 sdk 安装成功。

2.5.4windows 下配置 Golang 环境变量:
为什么需要配置环境变量

配置环境变量介绍
根据 windows 系统在查找可执行程序的原理,可以将 Go 所在路径定义到环境变量中,让系统帮我
们去找运行执行的程序,这样在任何目录下都可以执行 go 指令。


在 Go 开发中,需要配置哪些环境变量

看老师如何配置
步骤 1:先打开环境变量配置的界面

步骤 2: 配置我们的环境变量

对上图的一个说明:
1) Path 这个环境变量不需要在创建,因为系统本身就有,你后面增加即可
2) 增加 Go 的 bin : ;%GOROOT%\bin

对上图的一个说明
1) GOPATH :就是你以后 go 项目存放的路径,即工作目录
2) GOPATH:是一个新建的环境变量
测试一下我们的环境变量是否配置 ok

注意:配置环境变量后,需要重新打开一次 dos 的终端,这样环境变量才会生效。


2.6Linux 下搭建 Go 开发环境-安装和配置 SDK
2.6.1Linux 下安装 SDK:
1) Linux 下 SDK 的各个版本说明:
Linux 下:根据系统是 32 位还是 64 位进行下载:
32 位系统:go1.9.2.linux-386.tar.gz
64 位系统:go1.9.2.linux-amd64.tar.gz
如何确认你的 linux 是多少位:

2) 请注意:安装路径不要有中文或者特殊符号如空格等
3) SDK 安装目录建议: linux 放在 /opt 目录下
4) 安装时,解压即可,我们使用的是 tar.gz
5) 看老师演示
步骤 1: 将 go1.9.2.linux-amd64.tar.gz 传输到 ubuntu
步骤 2: 将 go1.9.2.linux-amd64.tar.gz 拷贝到 /opt 下

步骤 3: cd /opt
步骤 4:tar -zxvf go1.9.2.linux-amd64.tar.gz [解压后,就可以看到一个 go 目录]
步骤 5: cd
go/bin
步骤 6:./go version

2.6.2Linux 下配置 Golang 环境变量
步骤 1:使用 root 的权限来编辑 vim /etc/profile 文件

步骤 2: 如果需要生效的话,需要注销一下(重新登录),再使用

2.7Mac 下搭建 Go 开发环境-安装和配置 SDK
2.7.1mac 下安装 Go 的 sdk
1) Mac 下 SDK 的各个版本说明:
Mac OS 下:只有 64 位的软件安装包
Mac OS 系统的安装包:go1.9.2.darwin-amd64.tar.gz
2) 请注意:安装路径不要有中文或者特殊符号如空格等
3) SDK 安装目录建议: Mac 一般放在用户目录下 go_dev/go 下
4) 安装时,解压即可
5) 看老师的演示步骤
步骤 1: 先将我们的安装文件 go1.9.2.darwin-amd64.tar.gz 上传到 mac

第 21页尚硅谷 Go 语言课程
步骤 2: 先在用户目录下,创建一个目录 go_dev , 将我们上传的文件 移动到 go_dev 目录
步骤 3: 解压 tar -zxvf go1.9.2.darwin-amd64.tar.gz
步骤 4: 解压后,我们会得到一个目录 go, 进入到 go/bin 就是可以使用

这里还是有一个问题,就是如果我们不做 bin 目录下,就使用不了 go 程序。因此我们仍然需要配置 go
的环境变量。
2.7.2Mac 下配置 Golang 环境变量:
步骤 1:使用 root 用户,修改 /etc/profile 增加环境变量的配置

 

步骤 2: 配置完后,需要重新注销用户,配置才会生效.

2.8Go 语言快速开发入门
2.8.1需求
要求开发一个 hello.go 程序,可以输出
"hello,world”
2.8.2开发的步骤
1) 开发这个程序/项目时,go 的目录结构怎么处理.

2) 代码如下:

对上图的说明
(1) go 文件的后缀是 .go
(2) package main
表示该 hello.go 文件所在的包是 main, 在 go 中,每个文件都必须归属于一个包。
(3) import “fmt”
表示:引入一个包,包名 fmt, 引入该包后,就可以使用 fmt 包的函数,比如:fmt.Println

(4) func main() {
}

func 是一个关键字,表示一个函数。
main 是函数名,是一个主函数,即我们程序的入口。
(5) fmt.Println(“hello”)
表示调用 fmt 包的函数 Println 输出 “hello,world”
3) 通过 go build 命令对该 go 文件进行编译,生成 .exe 文件.

4) 运行 hello.exe 文件即可

5) 注意:通过 go run 命令可以直接运行 hello.go 程序 [类似执行一个脚本文件的形式]

2.8.3linux 下如何开发 Go 程序
说明:linux 下开发 go 和 windows 开发基本是一样的。只是在运行可执行的程序时,
是以 ./文件名方式
演示: 在 linux 下开发 Go 程序。

编译和运行 hello.go

也可以直接使用 go run hello.go 方式运行

2.8.4Mac 下如何开发 Go 程序
说明:在 mac 下开发 go 程序和 windows 基本一样。
演示一下:如何在 mac 下开发一个 hello.go 程序
源代码的编写:hello.go

编译再运行,直接 go run 来运行

直接 go run 来运行

2.8.5go 语言的快速入门的课堂练习

2.8.6Golang 执行流程分析
如果是对源码编译后,再执行,Go 的执行流程如下图

如果我们是对源码直接 执行 go run 源码,Go 的执行流程如下图

两种执行流程的方式区别
1) 如果我们先编译生成了可执行文件,那么我们可以将该可执行文件拷贝到没有 go 开发环境的机器上,仍然可以运行
2) 如果我们是直接 go run    go 源代码,那么如果要在另外一个机器上这么运行,也需要 go 开发环境,否则无法执行。
3) 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件变大了很多。

2.8.7编译和运行说明

1) 有了 go 源文件,通过编译器将其编译成机器可以识别的二进制码文件。
2) 在该源文件目录下,通过 go build 对 hello.go 文件进行编译。可以指定生成的可执行文件名,在
windows 下 必须是 .exe 后缀。

3) 如果程序没有错误,没有任何提示,会在当前目录下会出现一个可执行文件(windows 下是.exe
Linux 下是一个可执行文件),该文件是二进制码文件,也是可以执行的程序。
4) 如果程序有错误,编译时,会在错误的那行报错。有助于程序员调试错误.

5) 运行有两种形式

2.8.8Go 程序开发的注意事项

1) Go 源文件以 "go" 为扩展名。
2) Go 应用程序的执行入口是 main()函数。 这个是和其它编程语言(比如 java/c)
3) Go 语言严格区分大小写。
4) Go 方法由一条条语句构成,每个语句后不需要分号(Go 语言会在每行后自动加分号),这也体现出 Golang 的简洁性。
5) Go 编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否则报错

6) go 语言定义的变量或者 import 的包如果没有使用到,代码不能编译通过。

7) 大括号都是成对出现的,缺一不可。

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

说明:常用的转义字符有如下:
1) \t : 表示一个制表符,通常使用它可以排版。

2) \n :换行符
3) \\ :一个\
4) \" :一个"
5) \r :一个回车  fmt.Println("天龙八部雪山飞狐\r 张飞");
6) 案例截图

课堂练习

5min 作业评讲:

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

2.10 Golang 开发常见问题和解决方法

2.10.1 文件名或者路径错误

2.10.2 小结和提示

学习编程最容易犯的错是语法错误 。Go 要求你必须按照语法规则编写代码。如果你的程序违反了
语法规则,例如:忘记了大括号、引号,或者拼错了单词,Go 编译器都会报语法错误, 要求:尝试
着去看懂编译器会报告的错误信息 。

2.11 注释(comment)

2.11.1 介绍注释
用于注解说明解释程序的文字就是注释,注释提高了代码的阅读性;
注释是一个程序员必须要具有的良好编程习惯。将自己的思想通过注释先整理出来,再用代码去
体现。
2.11.2 在 Golang 中注释有两种形式
1) 行注释
基本语法
// 注释内容
举例

2) 块注释(多行注释)
 基本语法
/*
注释内容
*/
举例说明

使用细节
1) 对于行注释和块注释,被注释的文字,不会被 Go 编译器执行。
2) 块注释里面不允许有块注释嵌套 [注意一下]
2.12 规范的代码风格
2.12.1 正确的注释和注释风格:
1) Go 官方推荐使用行注释来注释整个方法和语句。
2) 带看 Go 源码
2.12.2 正确的缩进和空白
1) 使用一次 tab 操作,实现缩进,默认整体向右边移动,时候用 shift+tab 整体向左移
看老师的演示:
2) 或者使用 gofmt 来进行格式化 [演示]

3) 运算符两边习惯性各加一个空格。比如:2 + 4 * 5。

4) Go 语言的代码风格.

package main
import "fmt"
func main() {
fmt.Println("hello,world!")
}

上面的写法是正确的.

package main
import "fmt"
func main()
{
fmt.Println("hello,world!")
}

上面的写法不是正确,Go 语言不允许这样编写。 【Go 语言不允许这样写,是错误的!】
Go 设计者思想: 一个问题尽量只有一个解决方法
5) 一行最长不超过 80 个字符,超过的请使用换行展示,尽量保持格式优雅
举例说明

2.13 Golang 官方编程指南
说明: Golang 官方网站
https://golang.org

 

点击上图的 tour -> 选择 简体中文就可以进入中文版的 Go 编程指南 。
Golang 官方标准库 API 文档, https://golang.org/pkg可以查看 Golang 所有包下的函数和使用

解释术语:API
api : application program interface :应用程序编程接口。
就是我们 Go 的各个包的各个函数。

2.14 Golang 标准库 API 文档

1) API (Application Programming Interface,应用程序编程接口)是 Golang 提供的基本编程接口。
2) Go 语言提供了大量的标准库,因此 google 公司 也为这些标准库提供了相应的 API 文档,用于告诉开发者如何使用这些标准库,以及标准库包含的方法。
3) Golang 中文网 在线标准库文档: https://studygolang.com/pkgdoc
4) Golang 的包和源文件和函数的关系简图

5) 有一个离线版的 Golang_Manual_By_AstaXie_20120522.chm
2.15 Dos 的常用指令(了解)
2.15.1 dos 的基本介绍
Dos: Disk Operating System 磁盘操作系统, 简单说一下 windows 的目录结构
2.15.2 dos 的基本操作原理

2.15.3 目录操作指令
查看当前目录是什么

切换到其他盘下:盘符号 F 盘
案例演示:

切换到当前盘的其他目录下 (使用相对路径和绝对路径演示)
案例演示:

切换到上一级:
案例演示:

切换到根目录:
案例演示:

新建目录 md (make directory)
新建一个目录:

新建多个目录:

删除目录
删除空目录

删除目录以及下面的子目录和文件,不带询问

删除目录以及下面的子目录和文件,带询问

2.15.4 文件的操作

新建或追加内容到文件
案例演示:

复制或移动文件
复制

移动

删除文件
删除指定文件

删除所有文件

2.15.5 其它指令
清屏
cls [苍老师]
退出 dos
exit
2.15.6 综合案例
2.16 课后练习题的评讲
1) 独立编写 Hello,Golang!程序[评讲]

2) 将个人的基本信息(姓名、性别、籍贯、住址)打印到终端上输出。各条信息分别占一行

3) 在终端打印出如下图所示的效果

2.17 本章的知识回顾
Go 语言的 SDK 是什么?
SDK 就是软件开发工具包。我们做 Go 开发,首先需要先安装并配置好 sdk.
Golang 环境变量配置及其作用。
GOROOT: 指定 go sdk 安装目录。
Path: 指令 sdk\bin 目录:go.exe
godoc.exe
gofmt.exe
GOPATH: 就是 golang 工作目录:我们的所有项目的源码都这个目录下。
Golang 程序的编写、编译、运行步骤是什么? 能否一步执行?
编写:就是写源码
编译:go build 源码 =》 生成一个二进制的可执行文件
运行:1. 对可执行文件运行 xx.exe ./可执行文件
2. go run 源码
Golang 程序编写的规则。
1) go 文件的后缀 .go

2) go 程序区分大小写
3) go 的语句后,不需要带分号
4) go 定义的变量,或者 import 包,必须使用,如果没有使用就会报错
5) go 中,不要把多条语句放在同一行。否则报错
6) go 中的大括号成对出现,而且风格
func main() {
//语句
}
简述:在配置环境、编译、运行各个步骤中常见的错误
对初学者而言,最容易错的地方拼写错误。比如文件名,路径错误。拼写错误
本章小结

第 3 章 Golang 变量

3.1为什么需要变量
3.1.1一个程序就是一个世界

3.1.2变量是程序的基本组成单位不论是使用哪种高级程序语言编写程序,变量都是其程序的基本组成单位,比如一个示意图:

比如上图的 sum,sub 都是变量。

3.2变量的介绍

3.2.1变量的概念
变量相当于内存中一个数据存储空间的表示,你可以把变量看做是一个房间的门牌号,通过门牌号我们可以找到房间,同样的道理,通过变量名可以访问到变量(值)。
3.2.2变量的使用步骤
1) 声明变量(也叫:定义变量)
2) 非变量赋值
3) 使用变量

3.3变量快速入门案例

看一个案例:

输出:

3.4变量使用注意事项
1) 变量表示内存中的一个存储区域

2) 该区域有自己的名称(变量名)和类型(数据类型)

示意图:

3) Golang 变量使用的三种方式

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

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

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

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

如何一次性声明多个全局变量【在 go 中函数外部定义变量就是全局变量】:
5) 该区域的数据值可以在同一类型范围内不断变化(重点)

6) 变量在同一个作用域(在一个函数或者在代码块)内不能重名

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

3.5变量的声明,初始化和赋值

3.6程序中 +号的使用

1) 当左右两边都是数值型时,则做加法运算
2) 当左右两边都是字符串,则做字符串拼接

3.7 数据类型的基本介绍

3.8 整数类型

3.8.1基本介绍
简单的说,就是用于存放整数值的,比如 0, -1, 2345 等等。

3.8.2案例演示
3.8.3整数的各个类型
int 的无符号的类型:

int 的其它类型的说明:

3.8.4整型的使用细节
1) Golang 各整数类型分:有符号和无符号,int uint 的大小和系统有关。
2) Golang 的整型默认声明为 int 型

3) 如何在程序查看某个变量的字节大小和数据类型 (使用较多)

4) Golang 程序中整型变量在使用时,遵守保小不保大的原则,即:在保证程序正确运行下,尽量
使用占用空间小的数据类型。【如:年龄】

5) bit: 计算机中的最小存储单位。byte:计算机中基本存储单元。[二进制再详细说] 1byte = 8 bit

3.9小数类型/浮点型

3.9.1基本介绍
小数类型就是用于存放小数的,比如 1.2 0.23 -1.911
3.9.2案例演示

3.9.3小数类型分类

对上图的说明:
1) 关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数位+尾数位
说明:浮点数都是有符号的.

2) 尾数部分可能丢失,造成精度损失。 -123.0000901

说明:float64 的精度比 float32 的要准确.
说明:如果我们要保存一个精度高的数,则应该选用 float64
3) 浮点型的存储分为三部分:符号位+指数位+尾数位 在存储过程中,精度会有丢失
3.9.4浮点型使用细节
1) Golang 浮点类型有固定的范围和字段长度,不受具体 OS(操作系统)的影响。
2) Golang 的浮点型默认声明为 float64 类型。

3) 浮点型常量有两种表示形式
十进制数形式:如:5.12 .512
(必须有小数点)
科学计数法形式:如:5.1234e2 = 5.12 * 10 的 2 次方
5.12E-2
= 5.12/10 的 2 次方

4) 通常情况下,应该使用 float64 ,因为它比 float32 更精确。[开发中,推荐使用 float64]

3.10 字符类型

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

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

3.10.4 字符类型本质探讨
1) 字符型 存储到 计算机中,需要将字符对应的码值(整数)找出来
存储:字符--->对应码值---->二进制-->存储
读取:二进制----> 码值 ----> 字符 --> 读取
2) 字符和码值的对应关系是通过字符编码表决定的(是规定好)
3) Go 语言的编码都统一成了 utf-8。非常的方便,很统一,再也没有编码乱码的困扰了

3.11 布尔类型

3.11.1 基本介绍
1) 布尔类型也叫 bool 类型,bool 类型数据只允许取值 true 和 false
2) bool 类型占 1 个字节。
3) bool 类型适于逻辑运算,一般用于程序流程控制[注:这个后面会详细介绍]:
if 条件控制语句;
for 循环控制语句
3.11.2 案例演示

3.12 string 类型

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

3.12.3 string 使用注意事项和细节
1) Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本,这样 Golang 统一使用 UTF-8 编码,中文
乱码问题不会再困扰程序员。
2) 字符串一旦赋值了,字符串就不能修改了:在 Go 中字符串是不可变的。

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

4) 字符串拼接方式

5) 当一行字符串太长时,需要使用到多行字符串,可以如下处理

3.13 基本数据类型的默认值
3.13.1 基本介绍
在 go 中,数据类型都有一个默认值,当程序员没有赋值时,就会保留默认值,在 go 中,默认值
又叫零值。
3.13.2 基本数据类型的默认值如下

案例:

3.14 基本数据类型的相互转换
3.14.1 基本介绍
Golang 和 java / c 不同,Go 在不同类型的变量之间赋值时需要显式转换。也就是说 Golang 中数据类型不能自动转换。
3.14.2 基本语法

表达式 T(v) 将值 v 转换为类型 T

T: 就是数据类型,比如 int32,int64,float32 等等
v: 就是需要转换的变量

3.14.3 案例演示

3.14.4 基本数据类型相互转换的注意事项
1) Go 中,数据类型的转换可以是从 表示范围小-->表示范围大,也可以 范围大--->范围小
2) 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化!

3) 在转换中,比如将 int64
转成 int8 【-128---127】 ,编译时不会报错,只是转换的结果是按
溢出处理,和我们希望的结果不一样。 因此在转换时,需要考虑范围.

3.14.5 课堂练习
练习 1

如何修改上面的代码,就可以正确.

练习 2

3.15 基本数据类型和 string 的转换

3.15.1 基本介绍
在程序开发中,我们经常将基本数据类型转成 string,或者将 string 转成基本数据类型。
3.15.2 基本类型转 string 类型
方式 1:fmt.Sprintf("%参数", 表达式)
【个人习惯这个,灵活】
函数的介绍:

参数需要和表达式的数据类型相匹配
fmt.Sprintf().. 会返回转换后的字符串
案例演示

方式 2:使用 strconv 包的函数
案例说明

3.15.3 string 类型转基本数据类型
使用时 strconv 包的函数

案例演示

说明一下

3.15.4 string 转基本数据类型的注意事项
在将 String 类型转成 基本数据类型时,要确保 String 类型能够转成有效的数据,比如 我们可以把 "123" , 转成一个整数,但是不能把 "hello" 转成一个整数,如果这样做,Golang 直接将其转成 0 ,其它类型也是一样的道理. float => 0 bool => false
案例说明:

3.16 指针

3.16.1 基本介绍
1) 基本数据类型,变量存的就是值,也叫值类型
2) 获取变量的地址,用&,比如: var num int, 获取 num 的地址:&num分析一下基本数据类型在内存的布局.

3) 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值比如:var ptr *int = &num
举例说明:指针在内存的布局.

4) 获取指针类型所指向的值,使用:*,比如:var ptr *int, 使用*ptr 获取 ptr 指向的值

5) 一个案例再说明

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

3.16.3 指针的课堂练习

 

3.16.4 指针的使用细节
1) 值类型,都有对应的指针类型, 形式为
对应的指针类型就是
*数据类型,比如 int 的对应的指针就是 *int, float32  *float32, 依次类推。
2) 值类型包括:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
3.17 值类型和引用类型
3.17.1 值类型和引用类型的说明
1) 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
2) 引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型
3.17.2 值类型和引用类型的使用特点
1) 值类型:变量直接存储值,内存通常在栈中分配
示意图:

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

3) 内存的栈区和堆区示意图

3.18 标识符的命名规范

3.18.1 标识符概念
1) Golang 对各种变量、方法、函数等命名时使用的字符序列称为标识符
2) 凡是自己可以起名字的地方都叫标识符
3.18.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 等等...
3.18.3

hello
标识符的案例
// ok
hello12 //ok
1hello // error ,不能以数字开头
h-b // error ,不能使用 -
xh
// error, 不能含有空格
h_4 // ok
_ab // ok
int // ok , 我们要求大家不要这样使用
float32 // ok , 我们要求大家不要这样使用
_
// error
Abc
// ok

3.18.4 标识符命名注意事项
1) 包名:保持 package 的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和标准库不要冲突 fmt

2) 变量名、函数名、常量名:采用驼峰法
举例:

var stuName string = “tom”
形式: xxxYyyyyZzzz ...
var goodPrice float32 = 1234.5

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

3.19 系统保留关键字

3.20 系统的预定义标识符

第 4 章 运算符

4.1运算符的基本介绍

运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等
运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等
1) 算术运算符
2) 赋值运算符
3) 比较运算符/关系运算符
4) 逻辑运算符
5) 位运算符
6) 其它运算符

4.2算术运算符

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

4.2.2案例演示

案例演示算术运算符的使用。
+, - , * , / , %, ++, --
, 重点讲解 /、%
自增:++
自减:--
演示 / 的使用的特点 

演示 % 的使用特点
// 演示
% 的使用
// 看一个公式 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

++ 和 --的使用

4.2.3算术运算符使用的注意事项
1) 对于除号 "/",它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃小数部分。 例如: x := 19/5 ,结果是3
2) 当对一个数取模时,可以等价 a%b=a-a/b*b , 这样我们可以看到 取模的一个本质运算。
3) Golang 的自增自减只能当做一个独立语言使用时,不能这样使用

4) Golang 的++ 和 -- 只能写在变量的后面,不能写在变量的前面,即:只有 a++ a-- 没有 ++a    --a

5) Golang 的设计者去掉 c / java 中的 自增自减的容易混淆的写法,让 Golang 更加简洁,统一。 (强制性的)
4.2.4课堂练习 1

4.2.5课堂练习 2
1) 假如还有 97 天放假,问:xx 个星期零 xx 天
2) 定义一个变量保存华氏温度,华氏温度转换摄氏温度的公式为: 5/9*(华氏温度-100),请求出华氏温度对应的摄氏温度。

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

4.3.1基本介绍
1) 关系运算符的结果都是 bool 型,也就是要么是 true,要么是 false
2) 关系表达式 经常用在 if 结构的条件中或循环结构的条件中
4.3.2关系运算符一览图

4.3.3案例演示

4.3.4关系运算符的细节说明
细节说明
1) 关系运算符的结果都是 bool 型,也就是要么是 true,要么是 false。

2) 关系运算符组成的表达式,我们称为关系表达式: a > b
3) 比较运算符"=="不能误写成 "=" !!
4.4逻辑运算符
4.4.1基本介绍
用于连接多个条件(一般来讲就是关系表达式),最终的结果也是一个 bool 值
4.4.2逻辑运算的说明
4.4.3案例演示
   

4.4.4注意事项和细节说明
1) &&也叫短路与:如果第一个条件为 false,则第二个条件不会判断,最终结果为 false
2) ||也叫短路或:如果第一个条件为 true,则第二个条件不会判断,最终结果为 true
3) 案例演示

4.5赋值运算符
4.5.1基本的介绍
赋值运算符就是将某个运算后的值,赋给指定的变量。

4.5.2赋值运算符的分类

4.5.3赋值运算的案例演示
案例演示赋值运算符的基本使用。
1) 赋值基本案例
2) 有两个变量,a 和 b,要求将其进行交换,最终打印结果
3) += 的使用案例
4) 案例

4.5.4赋值运算符的特点
1) 运算顺序从右往左

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

3) 复合赋值运算符等价于下面的效果
比如:a += 3 等价于 a = a + 3
4.5.5面试题
有两个变量,a 和 b,要求将其进行交换,但是不允许使用中间变量,最终打印结果

4.6 位运算符

4.7 其它运算符说明

举例说明:

4.7.1课堂案例
案例 1:求两个数的最大值

案例 2:求三个数的最大值

4.8特别说明

举例说明,如果在 golang 中实现三元运算的效果。

4.9运算符的优先级
4.9.1运算符的优先级的一览表

4.9.2对上图的说明
1) 运算符有不同的优先级,所谓优先级就是表达式运算中的运算顺序。如右表,上一行运算符总优先于下一行。
2) 只有单目运算符、赋值运算符是从右向左运算的。
3) 梳理了一个大概的优先级

1:括号,++, --
2: 单目运算
3:算术运算符
4:移位运算
5:关系运算符
6:位运算符
7:逻辑运算符
8:赋值运算符
9:逗号

4.10 键盘输入语句

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

1) 导入 fmt 包
2) 调用 fmt 包的 fmt.Scanln() 或者 fmt.Scanf()

4.10.3 案例演示:
要求:可以从控制台接收用户信息,【姓名,年龄,薪水, 是否通过考试 】。
1) 使用 fmt.Scanln() 获取

2) 使用 fmt.Scanf() 获取

4.11 进制

 对于整数,有四种表示方式:
1) 二进制:0,1 ,满 2 进 1。
在 golang 中,不能直接使用二进制来表示一个整数,它沿用了 c 的特点。
2) 十进制:0-9 ,满 10 进 1。
3) 八进制:0-7 ,满 8 进 1. 以数字 0 开头表示。
4) 十六进制:0-9 及 A-F,满 16 进 1. 以 0x 或 0X 开头表示。
此处的 A-F 不区分大小写。
4.11.1 进制的图示

4.11.2 进制转换的介绍

4.11.3 其它进制转十进制

 4.11.4 二进制如何转十进制

4.11.5 八进制转换成十进制示例 

4.11.6 16 进制转成 10 进制

4.11.7 其它进制转 10 进制的课堂练习
课堂练习:请将
二进制: 110001100 转成 十进制
八进制: 02456 转成十进制
十六进制: 0xA45 转成十进制
4.11.8 十进制如何转成其它进制

4.11.9 十进制如何转二进制

4.11.10 十进制转成八进制

4.11.11 十进制转十六进制

4.11.12 课堂练习
课堂练习:请将
123 转成 二进制
678 转成八进制
8912 转成十六进制
4.11.13 二进制转换成八进制、十六进制

4.11.14 二进制转换成八进制

4.11.15 二进制转成十六进制

课堂练习
课堂练习:请将
二进制:11100101 转成 八进制
二进制:1110010110 转成 十六进制
4.11.16 八进制、十六进制转成二进制

4.11.17 八进制转换成二进制
4.11.18 十六进制转成二进制
4.12 位运算
4.12.1 位运算的思考题
1) 请看下面的代码段,回答 a,b,c,d 结果是多少?

func main() {
var a int = 1 >> 2
var b int = -1 >> 2
var c int = 1 << 2
var d int = -1 << 2
//a,b,c,d 结果是多少
fmt.Println("a=", a)
fmt.Println("b=", b)
fmt.Println("c=", c)
fmt.Println("d=", d)
}

2) 请回答在 Golang 中,下面的表达式运算的结果是:

func main() {
fmt.Println(2&3)
fmt.Println(2|3)
fmt.Println(13&7)
fmt.Println(5|4) //?
fmt.Println(-3^3) //?
}

4.12.2 二进制在运算中的说明
二进制是逢 2 进位的进位制,0、1 是基本算符。
现代的电子计算机技术全部采用的是二进制,因为它只使用 0、1 两个数字符号,非常简单方便,易于用电子方式实现。计算机内部处理的信息,都是采用二进制数来表示的。二进制(Binary)数用 0和 1 两个数字及其组合来表示任何数。进位规则是“逢 2 进 1”,数字 1 在不同的位上代表不同的值,按从右至左的次序,这个值以二倍递增。
在计算机的内部,运行各种运算时,都是以二进制的方式来运行。

4.12.3 原码、反码、补码

4.12.4 位运算符和移位运算符

Golang 中有 3 个位运算


分别是”按位与&、按位或|、按位异或^,它们的运算规则是:

按位与& : 两位全为1,结果为 1,否则为 0
按位或| : 两位有一个为 1,结果为 1,否则为 0
按位异或 ^ : 两位一个为 0,一个为 1,结果为 1,否则为 0

  案例练习
比如:2&3=? 2|3=?
2^3=?

Golang 中有 2 个移位运算符:

>>、<< 右移和左移,运算规则:
右移运算符 >>:低位溢出,符号位不变,并用符号位补溢出的高位
左移运算符 <<:
符号位不变,低位补 0
案例演示
a := 1 >> 2
// 0000 0001 =>0000 0000 = 0
c := 1 << 2
// 0000 0001 ==> 0000 0100 => 4

第 5 章 程序流程控制

5.1程序流程控制介绍

在程序中,程序运行的流程控制决定程序是如何执行的,是我们必须掌握的,主要有三大流程控
制语句。
1) 顺序控制
2) 分支控制
3) 循环控制

5.2顺序控制

程序从上到下逐行地执行,中间没有任何判断和跳转。
一个案例说明,必须下面的代码中,没有判断,也没有跳转.因此程序按照默认的流程执行,即顺序控制。

 5.2.1顺序控制的一个流程图

5.2.2顺序控制举例和注意事项
Golang 中定义变量时采用合法的前向引用。如:

func main() {
var num1 int = 10 //声明了 num1
var num2 int = num1 + 20 //使用 num1
fmt.Println(num2)
}
错误形式:
func main() {
var num2 int = num1 + 20 //使用 num1
var num1 int = 10 //声明 num1 (×)
fmt.Println(num2)
}

5.3分支控制

5.3.1分支控制的基本介绍
分支控制就是让程序有选择执行。有下面三种形式
1) 单分支
2) 双分支
3) 多分支
5.3.2单分支控制

基本语法

应用案例
请大家看个案例[ifDemo.go]:
编写一个程序,可以输入人的年龄,如果该同志的年龄大于 18 岁,则输出 "你年龄大
于 18,要对自己的行为负责!"
需求---[分析]---->代码
代码:

输出的结果:

单分支的流程图
流程图可以用图形方式来更加清晰的描述程序执行的流程。

单分支的细节说明

5.3.3双分支控制
基本语法

应用案例
请大家看个案例[IfDemo2.go]:
编写一个程序,可以输入人的年龄,如果该同志的年龄大于 18 岁,则输出 “你年龄大于 18,要对
自己的行为负责!”。否则 ,输出”你的年龄不大这次放过你了.”

双分支的流程图的分析

对双分支的总结

1. 从上图看 条件表达式就是 age >18
2. 执行代码块 1 ===> fmt.Println("你的年龄大于 18") ..
3. 执行代码块 2 ===> fmt.Println("你的年龄不大....") .
4. 强调一下 双分支只会执行其中的一个分支。

5.3.4单分支和双分支的案例

5) 编写程序,声明 2 个 int32 型变量并赋值。判断两数之和,如果大于等于 50,打印“hello world!

6) 编写程序,声明 2 个 float64 型变量并赋值。判断第一个数大于 10.0,且第 2 个数小于 20.0,打印两数之和。

7) 【选作】定义两个变量 int32,判断二者的和,是否能被 3 又能被 5 整除,打印提示信息

8) 判断一个年份是否是闰年,闰年的条件是符合下面二者之一: (1)年份能被 4 整除,但不能被 100整除;(2)能被 400 整除

5.3.5多分支控制

基本语法

对上面基本语法的说明
1) 多分支的判断流程如下:
(1) 先判断条件表达式 1 是否成立,如果为真,就执行代码块 1
(2) 如果条件表达式 1 如果为假,就去判断条件表达式 2 是否成立, 如果条件表达式 2 为真,
就执行代码块 2
(3) 依次类推.
(4) 如果所有的条件表达式不成立,则执行 else 的语句块。
2) else 不是必须的。
3) 多分支只能有一个执行入口。
看一个多分支的流程图(更加清晰)

多分支的快速入门案例
岳小鹏参加 Golang 考试,他和父亲岳不群达成承诺:
如果:
成绩为 100 分时,奖励一辆 BMW;
成绩为(80,99]时,奖励一台 iphone7plus;
当成绩为[60,80]时,奖励一个 iPad;
其它时,什么奖励也没有。
请从键盘输入岳小鹏的期末成绩,并加以判断
代码如下:

对初学者而言,有一个使用陷阱.

多分支的课堂练习

案例 3:

代码:

5.3.6嵌套分支

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

应用案例 1
参加百米运动会,如果用时 8 秒以内进入决赛,否则提示淘汰。并且根据性别提示进入男子组或女

子组。【可以让学员先练习下】, 输入成绩和性别。
代码:

应用案例 2

出票系统:根据淡旺季的月份和年龄,打印票价 [考虑学生先做]
4_10 旺季:
成人(18-60):60
儿童(<18):半价
老人(>60):1/3
淡季:
成人:40
其他:20

代码:

5.4switch 分支控制

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

5.4.3switch 的流程图

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

请编写一个程序,该程序可以接收一个字符,比如: a,b,c,d,e,f,g,a 表示星期一, b 表示星期二 ...根据用户的输入显示相依的信息.要求使用 switch 语句完成
代码

5.4.5switch 的使用的注意事项和细节
1) case/switch 后是一个表达式( 即:常量值、变量、一个有返回值的函数等都可以)

2) case 后的各个表达式的值的数据类型,必须和 switch 的表达式数据类型一致

3) case 后面可以带多个表达式,使用逗号间隔。比如 case 表达式 1, 表达式 2 ...

4) case 后面的表达式如果是常量值(字面量),则要求不能重复

5) case 后面不需要带 break , 程序匹配到一个 case 后就会执行对应的代码块,然后退出 switch,如
果一个都匹配不到,则执行 default
6) default 语句不是必须的.
7) switch 后也可以不带表达式,类似 if --else 分支来使用。【案例演示】

8) switch 后也可以直接声明/定义一个变量,分号结束,不推荐。 【案例演示】

9) switch 穿透-fallthrough ,如果在 case 语句块后增加 fallthrough ,则会继续执行下一个 case,也
叫 switch 穿透

10) Type Switch:switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际指向的变量类型 【还没有学 interface, 先体验一把】

5.4.6switch 的课堂练习
1) 使用 switch 把小写类型的 char 型转为大写(键盘输入)。只转换 a, b, c, d, e. 其它的输出“other”。

2) 对学生成绩大于 60 分的,输出“合格”。低于 60 分的,输出“不合格”。(注:输入的成绩不能大于 100)

3) 根据用户指定月份,打印该月份所属的季节。3,4,5 春季 6,7,8 夏季9,10,11 秋季 12, 1, 2 冬季

5.4.7switch 和 if 的比较

总结了什么情况下使用 switch ,什么情况下使用 if
1) 如果判断的具体数值不多,而且符合整数、浮点数、字符、字符串这几种类型。建议使用 swtich
语句,简洁高效。
2) 其他情况:对区间判断和结果为 bool 类型的判断,使用 if,if 的使用范围更广。

5.5for 循环控制

5.5.1基本介绍
听其名而知其意。就是让我们的一段代码循环的执行。
5.5.2一个实际的需求
请大家看个案例
[forTest.go]:
编写一个程序, 可以打印 10 句
"你好,尚硅谷!"。请大家想想怎么做?
使用传统的方式实现

for 循环的快速入门

5.5.3for 循环的基本语法
语法格式
for 循环变量初始化; 循环条件; 循环变量迭代 {
循环操作(语句)
}
对上面的语法格式说明

1) 对 for 循环来说,有四个要素:
2) 循环变量初始化
3) 循环条件
4) 循环操作(语句) ,有人也叫循环体。
5) 循环变量迭代

for 循环执行的顺序说明:

1) 执行循环变量初始化,比如 i := 1
2) 执行循环条件, 比如 i <= 10
3) 如果循环条件为真,就执行循环操作 :比如 fmt.Println(“....”)
4) 执行循环变量迭代 , 比如 i++
5) 反复执行 2, 3, 4 步骤,直到 循环条件为 False ,就退出 for 循环。

5.5.4  for 循环执行流程分析
for 循环的流程图

对照代码分析 for 循环的执行过程

5.5.5for 循环的使用注意事项和细节讨论
1) 循环条件是返回一个布尔值的表达式
2) for 循环的第二种使用方式

for 循环判断条件 {
//循环执行语句
}

将变量初始化和变量迭代写到其它位置
案例演示:
3) for 循环的第三种使用方式

for {
//循环执行语句
}

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

4) Golang 提供 for-range 的方式,可以方便遍历字符串和数组(注: 数组的遍历,我们放到讲数组的时候再讲解) ,案例说明如何遍历字符串。
字符串遍历方式 1-传统方式

字符串遍历方式 2-for - range

上面代码的细节讨论
如果我们的字符串含有中文,那么传统的遍历字符串方式,就是错误,会出现乱码。原因是传统的对字符串的遍历是按照字节来遍历,而一个汉字在 utf8 编码是对应 3 个字节。如何解决 需要要将str 转成 []rune 切片.=> 体验一把

对应 for-range 遍历方式而言,是按照字符方式遍历。因此如果有字符串有中文,也是 ok

5.5.6for 循环的课堂练习
1) 打印 1~100 之间所有是 9 的倍数的整数的个数及总和

2) 完成下面的表达式输出 ,6 是可变的。

5.6while 和 do..while 的实现

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

说明上图
1) for 循环是一个无限循环
2) break 语句就是跳出 for 循环
使用上面的 while 实现完成输出 10 句”hello,wrold”

5.6.2do..while 的实现

对上图的说明
1) 上面的循环是先执行,在判断,因此至少执行一次。
2) 当循环条件成立后,就会执行 break, break 就是跳出 for 循环,结束循环.
案例演示
使用上面的 do...while 实现完成输出 10 句”hello,ok”

5.7多重循环控制(重点,难点)

5.7.1基本介绍
1) 将一个循环放在另一个循环体内,就形成了嵌套循环。在外边的 for 称为外层循环在里面的 for
循环称为内层循环。【建议一般使用两层,最多不要超过 3 层】
2) 实质上,嵌套循环就是把内层循环当成外层循环的循环体。当只有内层循环的循环条件为 false
时,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次的循环。
3) 外层循环次数为 m 次,内层为 n 次,则内层循环体实际上需要执行 m*n 次
5.7.2应用案例
1) 统计 3 个班成绩情况,每个班有 5 名同学,求出各个班的平均分和所有班级的平均分[学生的成
绩从键盘输入]
编程时两大绝招
(1) 先易后难, 即将一个复杂的问题分解成简单的问题。
(2) 先死后活
代码:

 

2) 统计三个班及格人数,每个班有 5 名同学
对上面的代码进行了一点修改.

3) 打印金字塔经典案例
使用 for 循环完成下面的案例请编写一个程序,可以接收一个整数,表示层数,打印出金字分析编程思路
走代码

 

4) 打印出九九乘法表

代码:

5.8跳转控制语句-break
5.8.1看一个具体需求,引出 break

随机生成 1-100 的一个数,直到生成了 99 这个数,看看你一共用了几次?
分析:编写一个无限循环的控制,然后不停的随机生成数,当生成了 99 时,就退出这个无限循环==》break 提示使用这里我们给大家说一下,如下随机生成 1-100 整数.
5.8.2break 的快速入门案例

5.8.3基本介绍:
break 语句用于终止某个语句块的执行,用于中断当前 for 循环或跳出 switch 语句。
5.8.4基本语法:

{
......
break
......
}

5.8.5以 for 循环使用 break 为例,画出示意图

5.8.6break 的注意事项和使用细节
1) break 语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块
2) 看一个案例

3) 对上面案例的说明
(1) break 默认会跳出最近的 for 循环

(2) break 后面可以指定标签,跳出标签对应的 for 循环

第 139页尚硅谷 Go 语言课程
5.8.7课堂练习
1) 100 以内的数求和,求出 当和 第一次大于 20 的当前数
2) 实现登录验证,有三次机会,如果用户名为”张无忌” ,密码”888”提示登录成功,否则提示
还有几次机会.

5.9跳转控制语句-continue
5.9.1基本介绍:
continue 语句用于结束本次循环,继续执行下一次循环。
continue 语句出现在多层嵌套的循环语句体中时,可以通过标签指明要跳过的是哪一层循环 , 这
个和前面的 break 标签的使用的规则一样.
5.9.2基本语法:

{
......
continue
......
}

5.9.3continue 流程图

 


5.9.4案例分析 continue 的使用

5.9.5continu 的课堂练习   练习 1

continue 实现 打印 1——100 之内的奇数[要求使用 for 循环+continue]   代码:

从键盘读入个数不确定的整数,并判断读入的正数和负数的个数,输入为 0 时结束程序

课后练习题(同学们课后自己完成):

某人有 100,000 元,每经过一次路口,需要交费,规则如下:
当现金>50000 时,每次交 5%
当现金<=50000 时,每次交 1000
编程计算该人可以经过多少次路口,使用 for break 方式完成

5.10 跳转控制语句-goto

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

5.10.4 快速入门案例

5.11 跳转控制语句-return
5.11.1 介绍:
return 使用在方法或者函数中,表示跳出所在的方法或函数,在讲解函数的时候,会详细的介绍。

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

第 6 章 函数、包和错误处理

6.1为什么需要函数
6.1.1请大家完成这样一个需求:输入两个数,再输入一个运算符(+,-,*,/),得到结果.。
6.1.2使用传统的方法解决     走代码

分析一下上面代码问题
1) 上面的写法是可以完成功能, 但是代码冗余
2) 同时不利于代码维护
3) 函数可以解决这个问题

6.2函数的基本概念

为完成某一功能的程序指令(语句)的集合,称为函数。
在 Go 中,函数分为: 自定义函数、系统函数(查看 Go 编程手册)
6.3 函数的基本语法
6.4 快速入门案例   使用函数解决前面的计算问题。走代码:

6.5 包的引出 

1) 在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如 main.go 中,去使用 utils.go 文件中的函数,如何实现? -》包
2) 现在有两个程序员共同开发一个 Go 项目,程序员 xiaoming 希望定义函数 Cal ,程序员 xiaoqiang  也想定义函数也叫 Cal。两个程序员为此还吵了起来,怎么办? -》包

6.6  包的原理图  

包的本质实际上就是创建不同的文件夹,来存放程序文件。画图说明一下包的原理

 

6.7包的基本概念

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

6.8包的三大作用

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

6.9包的相关说明

打包基本语法
package 包名
引入包的基本语法
import "包的路径"

6.10 包使用的快速入门

包快速入门-Go 相互调用函数,我们将 func Cal 定义到文件 utils.go , 将 utils.go 放到一个包中,当
其它文件需要使用到 utils.go 的方法时,可以 import 该包,就可以使用了. 【为演示:新建项目目录结构】
代码演示:

utils.go 文件

main.go 文件

6.11 包使用的注意事项和细节讨论
1) 在给一个文件打包时,该包对应一个文件夹,比如这里的 utils 文件夹对应的包名就是 utils,
文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
2) 当一个文件要使用其它包函数或变量时,需要先引入对应的包
引入方式 1:import "包名"
引入方式 2:

import (
"包名"
"包名"
)

package 指令在 文件第一行,然后是 import 指令。
在 import 包时,路径从 $GOPATH 的
src 下开始,不用带 src , 编译器会自动从 src 下开始引入
3) 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言
的 public ,这样才能跨包访问。比如 utils.go 的

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

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

说明: 如果给包取了别名,则需要使用别名来访问该包的函数和变量。
6) 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义
7) 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main .这个就是一个语法规范,如果你是写一个库 ,包名可以自定义

6.12 函数的调用机制

6.12.1 通俗易懂的方式的理解

6.12.2 函数-调用过程
介绍:为了让大家更好的理解函数调用过程, 看两个案例,并画出示意图,这个很重要
1) 传入一个数+1

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


2) 计算两个数,并返回

6.12.3 return 语句
基本语法和说明
案例演示 1
请编写要给函数,可以计算两个数的和和差,并返回结果。

案例演示 2
一个细节说明: 希望忽略某个返回值,则使用 _ 符号表示占位忽略

6.13 函数的递归调用

6.13.1 基本介绍
一个函数在函数体内又调用了本身,我们称为递归调用
6.13.2 递归调用快速入门
代码 1

上面代码的分析图:

代码 2

对上面代码分析的示意图:

6.13.3 递归调用的总结

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

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

6.13.4 递归课堂练习题
题 1:斐波那契数
请使用递归的方式,求出斐波那契数 1,1,2,3,5,8,13...
给你一个整数 n,求出它的斐波那契数是多少?
思路:
1) 当 n == 1 || n ==2 , 返回 1
2) 当 n >= 2, 返回 前面两个数的和 f(n-1) + f(n-2)
代码:

题 2:求函数值
已知 f(1)=3; f(n) = 2*f(n-1)+1;
请使用递归的思想编程,求出 f(n)的值?
思路:
直接使用给出的表达式即可完成
代码:

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

6.14 函数使用的注意事项和细节讨论
1) 函数的形参列表可以是多个,返回值列表也可以是多个。
2) 形参列表和返回值列表的数据类型可以是值类型和引用类型。
3) 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其
它包文件使用,类似 public , 首字母小写,只能被本包文件使用,其它包文件不能使用,类似 privat
4) 函数中的变量是局部的,函数外不生效【案例说明】

5) 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。

6) 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传
入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。

7) Go 函数不支持函数重载
8) 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量
了。通过该变量可以对函数调用

9) 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用

10) 为了简化数据类型定义,Go 支持自定义数据类型

基本语法:type 自定义数据类型名
数据类型
// 理解: 相当于一个别名
案例:type myInt int // 这时 myInt 就等价 int 来使用了.
案例:type mySum
func (int, int) int
// 这时 mySum 就等价 一个 函数类型 func (int, int)
int

举例说明自定义数据类型的使用:

----------------------------------------------------------------------------------------------

11) 支持对函数返回值命名

12) 使用 _ 标识符,忽略返回值

13) Go 支持可变参数

(3) 如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后。
代码演示:

6.15 函数的课堂练习
题 1

题 2

题 3:请编写一个函数 swap(n1 *int, n2 *int) 可以交换 n1 和 n2 的值

6.16 init 函数

6.16.1 基本介绍
每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也
就是说 init 会在 main 函数前被调用。
6.16.2 案例说明:

输出的结果是:

6.16.3 inti 函数的注意事项和细节

1) 如果一个文件同时包含全局变量定义, init 函数和 main 函数,则执行的流程全局变量定义->init
函数->main 函数

2) init 函数最主要的作用,就是完成一些初始化的工作,比如下面的案例

3) 细节说明: 面试题:案例如果 main.go 和 utils.go 都含有 变量定义,init 函数时,执行的流程
又是怎么样的呢?

6.17 匿名函数

6.17.1 介绍
Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考
虑使用匿名函数,匿名函数也可以实现多次调用。
6.17.2 匿名函数使用方式 1
在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。 【案例演示】

6.17.3 匿名函数使用方式 2
将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数 【案例演示】

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

6.18 闭包

6.18.1 介绍
基本介绍:闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
6.18.2 案例演示:

对上面代码的说明和总结
1) AddUpper 是一个函数,返回的数据类型是 fun (int) int
2) 闭包的说明

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

6.18.3 闭包的最佳实践
请编写一个程序,具体要求如下
1) 编写一个函数 makeSuffix(suffix string)可以接收一个文件后缀名(比如.jpg),并返回一个闭包
2) 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。
3) 要求使用闭包的方式完成
4) strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。
代码:

 

上面代码的总结和说明:
1) 返回的匿名函数和 makeSuffix (suffix string) 的 suffix 变量 组合成一个闭包,因为 返回的函数引用到 suffix 这个变量
2) 我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入 后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用。大家可以仔细的体会一把!

6.19 函数的 defer

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

执行后,输出的结果:

6.19.3 defer 的注意事项和细节
1) 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中[我为了讲课方便,暂时称该栈为 defer 栈], 然后继续执行函数下一个语句。
2) 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制),所以同学们看到前面案例输出的顺序。
3) 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。请看一段代码:

上面代码输出的结果如下:

6.19.4 defer 的最佳实践

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

说明

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

6.20 函数参数传递方式

6.20.1 基本介绍
我们在讲解函数注意事项和使用细节时,已经讲过值类型和引用类型了,这里我们再系统总结一下,因为这是重难点,值类型参数默认就是值传递,而引用类型参数默认就是引用传递。
6.20.2 两种传递方式
1) 值传递
2) 引用传递
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。
6.20.3 值类型和引用类型

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

6.20.4 值传递和引用传递使用特点

3) 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。这个案例在前面详解函数使用注意事项的

6.21 变量作用域
1) 函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部

 

2) 函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效

3) 如果变量是在一个代码块,比如 for / if 中,那么这个变量的的作用域就在该代码块

6.21.1 变量作用域的课堂练习

输出的结果是: tom tom jack tom

6.22 函数课堂练习(综合)
1) 函数可以没有返回值案例,编写一个函数,从终端输入一个整数打印出对应的金子塔
分析思路:就是将原来写的打印金字塔的案例,使用函数的方式封装,在需要打印时,直接调用
即可。

2) 编写一个函数,从终端输入一个整数(1—9),打印出对应的乘法表
分析思路:就是将原来写的调用九九乘法表的案例,使用函数的方式封装,在需要打印时,直接调
用即可
代码:

3) 编写函数,对给定的一个二维数组(3×3)转置,这个题讲数组的时候再完成

 


6.23 字符串常用的系统函数


说明:字符串在我们程序开发中,使用的是非常多的,常用的函数需要同学们掌握[带看手册或者
官方编程指南]:
1) 统计字符串的长度,按字节 len(str)

2) 字符串遍历,同时处理有中文的问题 r := []rune(str)

3) 字符串转整数 : n, err := strconv.Atoi("12")

4) 整数转字符串  str = strconv.Itoa(12345)

5) 字符串 转 []byte: var bytes = []byte("hello go")

6) []byte 转 字符串: str = string([]byte{97, 98, 99})

7) 10 进制转 2, 8, 16 进制: str = strconv.FormatInt(123, 2) // 2-> 8 , 16

8) 查找子串是否在指定的字符串中: strings.Contains("seafood", "foo") //true

9) 统计一个字符串有几个指定的子串 : strings.Count("ceheese", "e") //4

10) 不 区 分 大 小 写 的 字 符 串 比 较 (== 是 区 分 字 母 大 小 写 的 ): fmt.Println(strings.EqualFold("abc",
"Abc")) // true

11) 返回子串在字符串第一次出现的 index 值,如果没有返回-1 : strings.Index("NLT_abc", "abc") // 4

12) 返回子串在字符串最后一次出现的 index,如没有返回-1 : strings.LastIndex("go golang", "go")

 


13) 将指定的子串替换成 另外一个子串: strings.Replace("go go hello", "go", "go 语言", n) n 可以指
定你希望替换几个,如果 n=-1 表示全部替换

14) 按 照 指 定 的 某 个 字 符 , 为 分 割 标 识 , 将 一 个 字 符 串 拆 分 成 字 符 串 数 组 :
strings.Split("hello,wrold,ok", ",")

15) 将字符串的字母进行大小写的转换: strings.ToLower("Go") // go strings.ToUpper("Go") // GO

16) 将字符串左右两边的空格去掉: strings.TrimSpace(" tn a lone gopher ntrn")

17) 将字符串左右两边指定的字符去掉 : strings.Trim("! hello! ", " !")// ["hello"] //将左右两边 !和 " "去掉

18) 将字符串左边指定的字符去掉 : strings.TrimLeft("! hello! ", " !") // ["hello"] //将左边 ! 和 "="去掉
19) 将字符串右边指定的字符去掉 :strings.TrimRight("! hello! ", " !")// ["hello"] //将右边 ! 和 ""去掉
20) 判断字符串是否以指定的字符串开头: strings.HasPrefix("ftp://192.168.10.1", "ftp") // true

21) 判断字符串是否以指定的字符串结束: strings.HasSuffix("NLT_abc.jpg", "abc") //false
6.24 时间和日期相关函数
6.24.1 基本的介绍
说明:在编程中,程序员会经常使用到日期相关的函数,比如:统计某段代码执行花费的时间等等。
1) 时间和日期相关函数,需要导入 time 包

2) time.Time 类型,用于表示时间

3) 如何获取到其它的日期信息

4) 格式化日期时间
方式 1: 就是使用 Printf 或者 SPrintf

方式二: 使用 time.Format() 方法完成:

对上面代码的说明:
"2006/01/02 15:04:05" 这个字符串的各个数字是固定的,必须是这样写。
"2006/01/02 15:04:05"
这个字符串各个数字可以自由的组合,这样可以按程序需求来返回时间和日期
5) 时间的常量

const (
Nanosecond
Microsecond
Millisecond
Duration = 1 //纳秒
= 1000 * Nanosecond
//微秒
= 1000 * Microsecond //毫秒
Second = 1000 * Millisecond //
Minute = 60 * Second //分钟
Hour = 60 * Minute //小时
)
View Code

常量的作用:在程序中可用于获取指定时间单位的时间,比如想得到 100 毫秒
100 * time. Millisecond
6) 结合 Sleep 来使用一下时间常量

7) time 的 Unix 和 UnixNano 的方法

 

得到的结果是:

6.24.2 时间和日期的课堂练习
编写一段代码来统计 函数 test03 执行的时间

6.25 内置函数

6.25.1 说明:
Golang 设计者为了编程方便,提供了一些函数,这些函数可以直接使用,我们称为 Go 的内置函
数。文档:https://studygolang.com/pkgdoc -> builtin
1) len:用来求长度,比如 string、array、slice、map、channel
2) new:用来分配内存,主要用来分配值类型,比如 int、float32,struct...返回的是指针
举例说明 new 的使用:

上面代码对应的内存分析图:

3) make:用来分配内存,主要用来分配引用类型,比如 channel、map、slice。这个我们后面讲解。
6.26 错误处理
6.26.1 看一段代码,因此错误处理

对上面代码的总结

1) 在默认情况下,当发生错误后(panic) ,程序就会退出(崩溃.)
2) 如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
3) 这里引出我们要将的错误处理机制

6.26.2 基本说明

1) Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try...catch...finally 这种处理。
2) Go 中引入的处理方式为:defer, panic, recover
3) 这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理

6.26.3 使用 defer+recover 来处理错误

6.26.4 错误处理的好处进行错误处理后,程序不会轻易挂掉,如果加入预警代码,就可以让程序更加的健壮。看一个
案例演示:

6.26.5 自定义错误

6.26.6 自定义错误的介绍
Go 程序中,也支持自定义错误, 使用 errors.New 和 panic 内置函数。
1) errors.New("错误说明") , 会返回一个 error 类型的值,表示一个错误
2) panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序.
6.26.7 案例说明

第 7 章 数组与切片

7.1为什么需要数组
看一个问题
一个养鸡场有 6 只鸡,它们的体重分别是 3kg,5kg,1kg,3.4kg,2kg,50kg 。请问这六只鸡的总体重是多少?平均体重是多少? 请你编一个程序。=》数组
使用传统的方法来解决

对上面代码的说明
1) 使用传统的方法不利于数据的管理和维护.
2) 传统的方法不够灵活,因此我们引出需要学习的新的数据类型=>数组.

7.2数组介绍

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

7.3数组的快速入门

我们使用数组的方法来解决养鸡场的问题.

对上面代码的总结
1) 使用数组来解决问题,程序的可维护性增加.
2) 而且方法代码更加清晰,也容易扩展。

7.4数组定义和内存布局

数组的定义

var 数组名 [数组大小]数据类型
var a [5]int
赋初值 a[0] = 1 a[1] = 30 ....
数组在内存布局(重要)

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

int32->4...
数组的使用
访问数组元素
数组名[下标] 比如:你要使用 a 数组的第三个元素 a[2]
快速入门案例
从终端循环输入 5 个成绩,保存到 float64 数组,并输出.

四种初始化数组的方式

7.6数组的遍历

7.6.1方式 1-常规遍历:
前面已经讲过了,不再赘述。
7.6.2方式 2-for-range 结构遍历
这是 Go 语言一种独有的结构,可以用来遍历访问数组的元素。
for--range 的基本语法

for-range 的案例

7.7数组使用的注意事项和细节
1) 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化

2) var arr []int 这时 arr 就是一个 slice 切片,切片后面专门讲解,不急哈.
3) 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。
4) 数组创建后,如果没有赋值,有默认值(零值)

数值类型数组:默认值为 0
字符串数组:默认值为 ""
bool 数组: 默认值为 false 

5) 使用数组的步骤 1. 声明数组并开辟空间 2 给数组各个元素赋值(默认零值) 3 使用数组
6) 数组的下标是从 0 开始的

7) 数组下标必须在指定范围内使用,否则报 panic:数组越界,比如 var arr [5]int 则有效下标为 0-4
8) Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响

9) 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)

10) 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度,看下面案例

7.8数组的应用案例

1) 创建一个 byte 类型的 26 个元素的数组,分别 放置'A'-'Z‘。使用 for 循环访问所有元素并打印
出来。提示:字符数据运算 'A'+1 -> 'B'

2) 请求出一个数组的最大值,并得到对应的下标。

3) 请求出一个数组的和和平均值。for-range

4) 要求:随机生成五个数,并将其反转打印 , 复杂应用.

7.9为什么需要切片先看一个需求:我们需要一个数组用于保存学生的成绩,但是学生的个数是不确定的,请问怎么办?解决方案:-》使用切片。
7.10 切片的基本介绍

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

7.11 快速入门
演示一个切片的基本使用:

运行结果是:

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

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

对上面的分析图总结
1. slice 的确是一个引用类型
2. slice 从底层来说,其实就是一个数据结构(struct 结构体)

type slice struct {
    ptr *[2]int
    len int
    cap int
}
View Code

7.13 切片的使用

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

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

参数说明: type: 就是数据类型
len : 大小
cap :指定切片容量,可选, 如果你分配了 cap,则要
求 cap>=len.
案例演示:

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

案例演示:

方式 1 和方式 2 的区别(面试)

7.14 切片的遍历

切片的遍历和数组一样,也有两种方式
for 循环常规方式遍历
for-range 结构遍历切片

7.15 切片的使用的注意事项和细节讨论
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[:]

3) cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素。
4) 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一
个空间供切片来使用
5) 切片可以继续切片[案例演示]

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

对上面代码的小结

切片 append 操作的底层原理分析:
切片 append 操作的本质就是对数组扩容
go 底层会创建一下新的数组 newArr(安装扩容后大小)
将 slice 原来包含的元素拷贝到新的数组 newArr
slice 重新引用到 newArr
注意 newArr 是在底层来维护的,程序员不可见.
7) 切片的拷贝操作
切片使用 copy 内置函数完成拷贝,举例说明

对上面代码的说明:
(1) copy(para1, para2) 参数的数据类型是切片
(2) 按照上面的代码来看, slice4 和 slice5 的数据空间是独立,相互不影响,也就是说 slice4[0]= 999,
slice5[0] 仍然是 1

8) 关于拷贝的注意事项

说明: 上面的代码没有问题,可以运行, 最后输出的是 [1]
9) 切片是引用类型,所以在传递时,遵守引用传递机制。看两段代码,并分析底层原理

 

7.16 string 和 slice


1) string 底层是一个 byte 数组,因此 string 也可以进行切片处理 案例演示:

2) string 和切片在内存的形式,以 "abcd" 画出内存示意图

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

4) 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string

7.17 切片的课堂练习题
说明:编写一个函数 fbn(n int) ,要求完成
1) 可以接收一个 n int
2) 能够将斐波那契的数列放到切片中
3) 提示, 斐波那契的数列形式:
arr[0] = 1; arr[1] = 1; arr[2]=2; arr[3] = 3; arr[4]=5; arr[5]=8
代码+思路:

第 8 章 排序和查找

8.1 排序的基本介绍

8.2 冒泡排序的思路分析

8.3冒泡排序实现

8.4课后练习
要求同学们能够,不看老师的代码,可以默写冒泡排序法(笔试题)

8.5查找

介绍:
在 Golang 中,我们常用的查找有两种:
1) 顺序查找
2) 二分查找(该数组是有序)
案例演示:
1) 有一个数列:白眉鹰王、金毛狮王、紫衫龙王、青翼蝠王
猜数游戏:从键盘中任意输入一个名称,判断数列中是否包含此名称【顺序查找】
代码:

2) 请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。【会使用到递归】
二分查找的思路分析:

二分查找的代码实现:

package main
import (
"fmt"
)
//二分查找的函数
/*
二分查找的思路: 比如我们要查找的数是 findVal
1. arr 是一个有序数组,并且是从小到大排序
2.
先找到 中间的下标 middle = (leftIndex + rightIndex) / 2, 然后让 中间下标的值和 findVal 进行
比较
2.1 如果 arr[middle] > findVal , 就应该向 leftIndex ---- (middle - 1)
2.2 如果 arr[middle] < findVal , 就应该向 middel+1---- rightIndex
2.3 如果 arr[middle] == findVal , 就找到
2.4 上面的 2.1 2.2 2.3 的逻辑会递归执行
3. 想一下,怎么样的情况下,就说明找不到[分析出退出递归的条件!!]
if leftIndex > rightIndex {
// 找不到..
return ..
}
*/
func BinaryFind(arr *[6]int, leftIndex int, rightIndex int, findVal int) {
//判断 leftIndex 是否大于 rightIndex
if leftIndex > rightIndex {

第 218页尚硅谷 Go 语言课程
fmt.Println("找不到")
return
}
//先找到 中间的下标
middle := (leftIndex + rightIndex) / 2
if (*arr)[middle] > findVal {
//说明我们要查找的数,应该在 leftIndex --- middel-1
BinaryFind(arr, leftIndex, middle - 1, findVal)
} else if (*arr)[middle] < findVal {
//说明我们要查找的数,应该在 middel+1 --- rightIndex
BinaryFind(arr, middle + 1, rightIndex, findVal)
} else {
//找到了
fmt.Printf("找到了,下标为%v \n", middle)
}
}
func main() {
arr := [6]int{1,8, 10, 89, 1000, 1234}
//测试一把
BinaryFind(&arr, 0, len(arr) - 1, -6)

}
View Code

8.6二维数组的介绍
多维数组我们只介绍二维数组
8.7二维数组的应用场景
比如我们开发一个五子棋游戏,棋盘就是需要二维数组来表示。如图

8.8二维数组快速入门
快速入门案例:
请用二维数组输出如下图形

000000
001000
020300
000000

代码演示

8.9使用方式 1: 先声明/定义,再赋值

语法: var 数组名 [大小][大小]类型
比如: var arr [2][3]int , 再赋值。

使用演示
二维数组在内存的存在形式(重点)

8.10 使用方式 2: 直接初始化

声明:var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}
赋值(有默认值,比如 int类型的就是 0)

使用演示

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

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

8.11 二维数组的遍历

双层 for 循环完成遍历
for-range 方式完成遍历
案例演示:

8.12 二维数组的应用案例
要求如下:
定义二维数组,用于保存三个班,每个班五名同学成绩,
并求出每个班级平均分、以及所有班级平均分
代码

第 9 章 map

 

9.1map 的基本介绍


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

9.2map 的声明


9.2.1基本语法
var map 变量名 map[keytype]valuetype
key 可以是什么类型
golang 中的 map,的 key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只
包含前面几个类型的 接口, 结构体, 数组
通常 key 为 int 、string
注意: slice, map 还有 function 不可以,因为这几个没法用 == 来判断
valuetype 可以是什么类型
valuetype 的类型和 key 基本一样,这里我就不再赘述了
通常为: 数字(整数,浮点数),string,map,struct
9.2.2map 声明的举例
map 声明的举例:

var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string

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

对上面代码的说明

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

9.3map 的使用

方式 1

方式 2

方式 3

map 使用的课堂案例
课堂练习:演示一个 key-value 的 value 是 map 的案例
比如:我们要存放 3 个学生信息, 每个学生有 name 和 sex 信息
思路:
map[string]map[string]string
代码:

9.4 map 的增删改查操作

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

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

案例演示:

细节说明
如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以遍历一下 key, 逐个删除
或者 map = make(...),make 一个新的,让原来的成为垃圾,被 gc 回收

map 查找:
案例演示:

对上面代码的说明:
说明:如果 heroes 这个 map 中存在 "no1" , 那么 findRes 就会返回 true,否则返回 false

9.5map 遍历:

案例演示相对复杂的 map 遍历:该 map 的 value 又是一个 map
说明:map 的遍历使用 for-range 的结构遍历
案例演示:

map 的长度:

9.6map 切片

9.6.1基本介绍
切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化了。
9.6.2案例演示
要求:使用一个 map 来记录 monster 的信息 name 和 age, 也就是说一个 monster 对应一个 map,并且妖怪的个数可以动态的增加=>map 切片
代码:

9.7map 排序

 9.7.1基本介绍
1) golang 中没有一个专门的方法针对 map 的 key 进行排序
2) golang 中的 map 默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出
可能不一样. 【案例演示 1】
3) golang 中 map 的排序,是先将 key 进行排序,然后根据 key 值遍历输出即可
9.7.2案例演示

9.8map 使用细节

1) map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map 【案例演示】

2) map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长 键值对(key-value)
3) map 的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面 value 是一个 map 更好),比如 value 为 Student 结构体 【案例演示,因为还没有学结构体,体验一下即可】

9.9map 的课堂练习题
课堂练习:
1) 使用 map[string]map[string]sting 的 map 类型
2) key: 表示用户名,是唯一的,不可以重复
3) 如果某个用户名存在,就将其密码修改"888888",如果不存在就增加这个用户信息,(包括昵称nickname 和 密码 pwd)。
4) 编写一个函数 modifyUser(users map[string]map[string]sting, name string) 完成上述功能
代码实现

package main
import (
"fmt"
)
/*
1)使用 map[string]map[string]sting 的 map 类型
2)key: 表示用户名,是唯一的,不可以重复
3)如果某个用户名存在,就将其密码修改"888888",如果不存在就增加这个用户信息,
(包括昵称 nickname 和 密码 pwd)。
4)编写一个函数 modifyUser(users map[string]map[string]sting, name string) 完成上述功能
*/
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)
}
View Code

第 10 章面向对象编程(上)

10.1 结构体


10.1.1 看一个问题

10.1.2 使用现有技术解决
1) 单独的定义变量解决
代码演示:

2) 使用数组解决
代码演示:

10.1.3 现有技术解决的缺点分析
1) 使用变量或者数组来解决养猫的问题,不利于数据的管理和维护。因为名字,年龄,颜色都是
属于一只猫,但是这里是分开保存。
2) 如果我们希望对一只猫的属性(名字、年龄,颜色)进行操作(绑定方法), 也不好处理。
3) 引出我们要讲解的技术-》结构体。
10.1.4 一个程序就是一个世界,有很多对象(变量)

10.1.5 Golang 语言面向对象编程说明

1) Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
2) Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
3) Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
4) Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
5) Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。

10.1.6 结构体与结构体变量(实例/对象)的关系示意图

对上图的说明
1) 将一类事物的特性提取出来(比如猫类), 形成一个新的数据类型, 就是一个结构体。
2) 通过这个结构体,我们可以创建多个变量(实例/对象)
3) 事物可以猫类,也可以是 Person , Fish 或是某个工具类。。。

10.1.7 快速入门-面向对象的方式(struct)解决养猫问题
代码演示

10.1.8 结构体和结构体变量(实例)的区别和联系
通过上面的案例和讲解我们可以看出:
1) 结构体是自定义的数据类型,代表一类事物.
2) 结构体变量(实例)是具体的,实际的,代表一个具体变量
10.1.9 结构体变量(实例)在内存的布局(重要!)

10.1.10 如何声明结构体
基本语法

type 结构体名称 struct {
field1 type
field2 type
}

举例:

type Student struct {
Name string //字段
Age int //字段
Score float32
}

10.1.11 字段/属性

基本介绍
1) 从概念或叫法上看: 结构体字段 = 属性 = field (即授课中,统一叫字段)
2) 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体 的 Name string 就是属性
注意事项和细节说明
1) 字段声明语法同变量,示例:字段名 字段类型
2) 字段的类型可以为:基本类型、数组或引用类型
3) 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:布尔类型是 false ,数值是 0 ,字符串是 ""。数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]指针,slice,和 map 的零值都是 nil ,即还没有分配空间。
案例演示:

4) 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。
案例:

画出上面代码的内存示意图:

10.1.12 创建结构体变量和访问结构体字段
方式 1-直接声明
案例演示: var person Person
前面我们已经说了。
方式 2-{}
案例演示: var person Person = Person{}

方式 3-&
案例: var person *Person = new (Person)

方式 4-{}
案例: var person *Person = &Person{}

说明:
1) 第 3 种和第 4 种方式返回的是 结构体指针。
2) 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 ,比如 (*person).Name = "tom"
3) 但 go 做了一个简化,也支持 结构体指针.字段名,
比如 person.Name = "tom"。更加符合程序员
使用的习惯,go 编译器底层 对 person.Name 做了转化 (*person).Name。
10.1.13 struct 类型的内存分配机制

看一个思考题

输出的结果是: p2.Name = tom p1.Name = 小明
基本说明

结构体在内存中示意图

看下面代码,并分析原因

输出的结果是:

上面代码对应的内存图的分析:

看下面代码,并分析原因

10.1.14 结构体使用注意事项和细节
1) 结构体的所有字段在内存中是连续的

对应的分析图:

 


2) 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类
型)

 


3) 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转

 


4) struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序
列化和反序列化。
序列化的使用场景:

 


举例:

10.2 方法
10.2.1 基本介绍
在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名..),Person 结构体还有一些行为比如:可以说话、跑步..,通过学习,还可以做算术题。这时就要用方法才能完成。
Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。
10.2.2 方法的声明和调用

type A struct {
  Num int
}
func (a A) test() {
  fmt.Println(a.Num)
}

对上面的语法的说明
1) func (a A) test() {}
表示 A 结构体有一方法,方法名为 test
2) (a A) 体现 test 方法是和 A 类型绑定的
举例说明

对上面的总结
1) test 方法和 Person 类型绑定
2) test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用

3) func (p Person) test() {}... p 表示哪个 Person 变量调用,这个 p 就是它的副本, 这点和函数传参非常相似。
4) p 这个名字,有程序员指定,不是固定, 比如修改成 person 也是可以

10.2.3 方法快速入门
1) 给 Person 结构体添加 speak 方法,输出xxx 是一个好人

2) 给 Person 结构体添加 jisuan 方法,可以计算从 1+..+1000 的结果, 说明方法体内可以函数一样,进行各种运算

3) 给 Person 结构体 jisuan2 方法,该方法可以接收一个数 n,计算从 1+..+n 的结果

4) 给 Person 结构体添加 getSum 方法,可以计算两个数的和,并返回结果

5) 方法的调用

10.2.4 方法的调用和传参机制原理:(重要!)
说明:
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做
实参也传递给方法。下面我们举例说明。
案例 1:
画出前面 getSum 方法的执行过程+说明

说明:
1) 在通过一个变量去调用方法时,其调用机制和函数一样
2) 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地质拷贝)
案例 2
请编写一个程序,要求如下:
1) 声明一个结构体 Circle, 字段为 radius
2) 声明一个方法 area 和 Circle 绑定,可以返回面积。
3) 提示:画出 area 执行过程+说明

10.2.5 方法的声明(定义)

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

1) 参数列表:表示方法输入
2) recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
3) receiver type : type 可以是结构体,也可以其它的自定义类型
4) receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
5) 返回值列表:表示返回的值,可以多个
6) 方法主体:表示为了实现某一功能代码块
7) return 语句不是必须的。
10.2.6 方法的注意事项和细节
1) 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
2) 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理

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

4) 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。[讲解]
5) 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出

10.2.7 方法的课堂练习题
1) 编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个 10*8 的矩形,
在 main 方法中调用该方法。

2) 编写一个方法,提供 m 和 n 两个参数,方法中打印一个 m*n 的矩形

3) 编写一个方法算该矩形的面积(可以接收长 len,和宽 width), 将其作为方法返回值。在 main
方法中调用该方法,接收返回的面积值并打印。

4) 编写方法:判断一个数是奇数还是偶数

5) 根据行、列、字符打印 对应行数和列数的字符,比如:行:3,列:2,字符*,则打印相应的效果

6) 定义小小计算器结构体(Calcuator),实现加减乘除四个功能
实现形式 1:分四个方法完成:
实现形式 2:用一个方法搞定

10.2.8 方法的课后练习题

强调: 一定自己要做,否则学习效果不好!!
10.2.9 方法和函数区别
1) 调用方式不一样

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

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

3) 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反
过来同样也可以

总结:
1) 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
2) 如果是和值类型,比如
(p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则
是地址拷贝。
10.3 面向对象编程应用实例
10.3.1 步骤
1) 声明(定义)结构体,确定结构体名
2) 编写结构体的字段
3) 编写结构体的方法
10.3.2 学生案例:
1) 编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、
int、float64 类型。
2) 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
3) 在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出。
4) 走代码

package main
import (
"fmt"
)
/*
学生案例:
编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、int、
float64 类型。
结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出。
*/
type Student struct {
name string
gender string
age int
id int
score float64
}
func (student *Student) say()
string {
infoStr := fmt.Sprintf("student 的信息 name=[%v] gender=[%v], age=[%v] id=[%v] score=[%v]",
student.name, student.gender, student.age, student.id, student.score)
return infoStr
}
func main() {
//测试
//创建一个 Student 实例变量
var stu = Student{
name : "tom",
gender : "male",
age : 18,
id : 1000,
score : 99.98,
}
fmt.Println(stu.say())
}
View Code

10.3.3 小狗案例 [学员课后练习]
1) 编写一个 Dog 结构体,包含 name、age、weight 字段
2) 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
3) 在 main 方法中,创建 Dog 结构体实例(变量),并访问 say 方法,将调用结果打印输出。
10.3.4 盒子案例
1) 编程创建一个 Box 结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终端获取
2) 声明一个方法获取立方体的体积。
3) 创建一个 Box 结构体变量,打印给定尺寸的立方体的体积
4) 走代码

10.3.5 景区门票案例
1) 一个景区根据游人的年龄收取不同价格的门票,比如年龄大于 18,收费 20 元,其它情况门票免费.
2) 请编写 Visitor 结构体,根据年龄段决定能够购买的门票价格并输出
3) 代码:

10.4 创建结构体变量时指定字段值
说明
Golang 在创建结构体实例(变量)时,可以直接指定字段的值
方式 1

方式 2

10.5 工厂模式
10.5.1 说明
Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
10.5.2 看一个需求
一个结构体的声明是这样的:
package model
type Student struct {
Name string...
}
因为这里的 Student 的首字母 S 是大写的,如果我们想在其它包创建 Student 的实例(比如 main 包),引入 model 包后,就可以直接创建 Student 结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如 是 type student struct {....} 就不不行了,怎么办---> 工厂模式来解决.
10.5.3 工厂模式来解决问题
使用工厂模式实现跨包创建结构体实例(变量)的案例:
如果 model 包的 结构体变量首字母大写,引入后,直接使用, 没有问题

如果 model 包的 结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决, 看老师演
示, 代码:
student.go

main.go

10.5.4 思考题
同学们思考一下,如果 model 包的 student 的结构体的字段 Score 改成 score,我们还能正常访问
吗?又应该如何解决这个问题呢?[老师给出思路,学员自己完成]
解决方法如下:

第 11 章 面向对象编程(下)

11.1 VSCode 的使用

11.1.1 VSCode 使用技巧和经验
设置字体
文件->首选项->设置

快捷键的使用

自定义快捷配置:文件->首选项->键盘快捷方式

介绍几个常用的快捷键

11.2 面向对象编程思想-抽象
11.2.1 抽象的介绍
我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取
出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。

11.2.2 代码实现

package main
import (
"fmt"
)
//定义一个结构体 Account
type Account struct {
AccountNo string
Pwd string
Balance float64
}
//方法
//1. 存款
func (account *Account) Deposite(money float64, pwd string)
{
//看下输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
//看看存款金额是否正确
if money <= 0 {
fmt.Println("你输入的金额不正确")
return
}

第 277页尚硅谷 Go 语言课程
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) Query(pwd string)
{
//看下输入的密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
fmt.Printf("你的账号为=%v 余额=%v \n", account.AccountNo, account.Balance)
}
func main() {
//测试一把
account := Account{
AccountNo : "gs1111111",
Pwd : "666666",
Balance : 100.0,
}
//这里可以做的更加灵活,就是让用户通过控制台来输入命令...
//菜单....
account.Query("666666")
account.Deposite(200.0, "666666")
account.Query("666666")
account.WithDraw(150.0, "666666")
account.Query("666666")
}
View Code

对上面代码的要求
1) 同学们自己可以独立完成
2) 增加一个控制台的菜单,可以让用户动态的输入命令和选项

11.3 面向对象编程三大特性-封装

11.3.1 基本介绍
Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,下面我们一一为同学们进行详细的讲解 Golang 的三大特性是如何实现的。
11.3.2 封装介绍
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作

11.3.3 封装的理解和好处
1) 隐藏实现细节
2) 提可以对数据进行验证,保证安全合理(Age)
11.3.4 如何体现封装
1) 对结构体中的属性进行封装
2) 通过方法,包 实现封装
11.3.5 封装的实现步骤
1) 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
2) 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
3) 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值

func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
//加入数据验证的业务逻辑
var.字段 = 参数
}

4) 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值

func (var 结构体类型名) GetXxx() {
return var.age;
}

特别说明:在 Golang 开发中并没有特别强调封装,这点并不像 Java. 所以提醒学过 java 的朋友,
不用总是用 java 的语法特性来看待 Golang, Golang 本身对面向对象的特性做了简化的.

11.3.6 快速入门案例


看一个案例
请大家看一个程序(person.go),不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验
证。设计: model 包(person.go) main 包(main.go 调用 Person 结构体)
代码实现
model/person.go

main/main.go

11.3.7 课堂练习(学员先做)
要求
1) 创建程序,在 model 包中定义 Account 结构体:在 main 函数中体会 Golang 的封装性。
2) Account 结构体要求具有字段:账号(长度在 6-10 之间)、余额(必须>20)、密码(必须是六
3) 通过 SetXxx 的方法给 Account 的字段赋值。(同学们自己完成
4) 在 main 函数中测试
代码实现
model/account.go

package model
import (
"fmt"
)
//定义一个结构体 account
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 nil
}
return &account{
accountNo : accountNo,
pwd : pwd,
balance : balance,
}
}
//方法
//1. 存款
func (account *account) Deposite(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) Query(pwd string)
{
//看下输入的密码是否正确
if pwd != account.pwd {
fmt.Println("你输入的密码不正确")
return
}
fmt.Printf("你的账号为=%v 余额=%v \n", account.accountNo, account.balance)
}
View Code

main/main.go

package main
import (
"fmt"
"go_code/chapter11/encapexercise/model"
)
func main() {
//创建一个 account 变量
account := model.NewAccount("jzh11111", "000", 40)
if account != nil {
fmt.Println("创建成功=", account)
} else {
fmt.Println("创建失败")
}
}
View Code

说明:在老师的代码基础上增加如下功能:
通过 SetXxx 的方法给 Account 的字段赋值 通过 GetXxx 方法获取字段的值。(同学们自己完成)
在 main 函数中测试
11.4 面向对象编程三大特性-继承
11.4.1 看一个问题,引出继承的必要性
一个小问题,看个学生考试系统的程序 extends01.go,提出代码复用的问题

走一下代码

package main
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("大学生正在考试中.....")
}
//代码冗余.. 高中生....
func main() {
//测试
var pupil = &Pupil{
Name :"tom",
Age : 10,
}
pupil.testing()
pupil.SetScore(90)
pupil.ShowInfo()
//测试
var graduate = &Graduate{
Name :"mary",
Age : 20,
}
graduate.testing()
graduate.SetScore(90)
graduate.ShowInfo()
}
View Code

对上面代码的小结
1) Pupil 和 Graduate 两个结构体的字段和方法几乎,但是我们却写了相同的代码, 代码复用性不强
2) 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展。
3) 解决方法-通过继承方式来解决
11.4.2 继承基本介绍和示意图
继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的
Student),在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 Student 匿名结构体即可。 [画出示意图]

也就是说:在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访
问匿名结构体的字段和方法,从而实现了继承特性。
11.4.3 嵌套匿名结构体的基本语法

type Goods struct {
    Name string
    Price int
}
type Book struct {
    Goods
    //这里就是嵌套匿名结构体 Goods
    Writer string
}

11.4.4 快速入门案例
案例
我们对 extends01.go 改进,使用嵌套匿名结构体的方式来实现继承特性,请大家注意体会这样编程的好处
代码实现

package main
import (
"fmt"
)
//编写一个学生考试系统
type Student struct {
Name string
Age int
Score int
}
//将 Pupil 和 Graduate 共有的方法也绑定到 *Student
func (stu *Student) ShowInfo() {
fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n", stu.Name, stu.Age, stu.Score)
}
func (stu *Student) SetScore(score int) {
//业务判断
stu.Score = score
}
//小学生
type Pupil struct {
Student //嵌入了 Student 匿名结构体
}
//显示他的成绩
//这时 Pupil 结构体特有的方法,保留
func (p *Pupil) testing() {
fmt.Println("小学生正在考试中.....")
}
//大学生, 研究生。。
//大学生
type Graduate struct {
Student //嵌入了 Student 匿名结构体
}
//显示他的成绩
//这时 Graduate 结构体特有的方法,保留
func (p *Graduate) testing() {
fmt.Println("大学生正在考试中.....")
}
//代码冗余.. 高中生....
func main() {
//当我们对结构体嵌入了匿名结构体使用方法会发生变化
pupil := &Pupil{}
pupil.Student.Name = "tom~"
pupil.Student.Age = 8
pupil.testing()
pupil.Student.SetScore(70)
pupil.Student.ShowInfo()
graduate := &Graduate{}
graduate.Student.Name = "mary~"
graduate.Student.Age = 28
graduate.testing()
graduate.Student.SetScore(90)
graduate.Student.ShowInfo()
}
View Code

11.4.5 继承给编程带来的便利
1) 代码的复用性提高了
2) 代码的扩展性和维护性提高了
11.4.6 继承的深入讨论
1) 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。【举例说明】

2) 匿名结构体字段访问可以简化,如图

对上面的代码小结
(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找..如果都找不到就报错.
3) 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分【举例说明】

4) 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。【举例说明】

5) 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

6) 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值

11.4.7 课堂练习
结构体的匿名字段是基本数据类型,如何访问, 下面代码输出什么

说明
1) 如果一个结构体有 int 类型的匿名字段,就不能第二个。
2) 如果需要有多个 int 的字段,则必须给 int 字段指定名字
11.4.8 面向对象编程-多重继承
多重继承说明
如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
案例演示
通过一个案例来说明多重继承使用

多重继承细节说明
1) 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。【案例演示】

2) 为了保证代码的简洁性,建议大家尽量不使用多重继承
11.5 接口(interface)
11.5.1 基本介绍
按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在 Golang 中 多态
特性主要是通过接口来体现的。
11.5.2 为什么有接口

11.5.3 接口快速入门
这样的设计需求在 Golang 编程中也是会大量存在的,我曾经说过,一个程序就是一个世界,在现实世
界存在的情况,在程序中也会出现。我们用程序来模拟一下前面的应用场景。
代码实现

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 {
}
//编写一个方法 Working 方法,接收一个 Usb 接口类型变量
//只要是实现了 Usb 接口 (所谓实现 Usb 接口,就是指实现了 Usb 接口声明所有方法)
func (c Computer) Working(usb Usb) { //usb 变量会根据传入的实参,来判断到底是 Phone,还是 Camera
//通过 usb 接口变量来调用 Start 和 Stop 方法
usb.Start()
usb.Stop()
}
func main() {
//测试
//先创建结构体变量
computer := Computer{}
phone := Phone{}
camera := Camera{}
//关键点
computer.Working(phone)
computer.Working(camera) //
}
View Code

说明: 上面的代码就是一个接口编程的快速入门案例。
11.5.4 接口概念的再说明
interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个
自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
11.5.5 基本语法

小结说明:
1) 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的
多态和高内聚低偶合的思想。
2) Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个
变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字
11.5.6 接口使用的应用场景

11.5.7 注意事项和细节
1) 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)

2) 接口中所有的方法都没有方法体,即都是没有实现的方法。
3) 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
4) 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
5) 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。

6) 一个自定义类型可以实现多个接口

7) Golang 接口中不能有任何变量

 8) 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必
须将 B,C 接口的方法也全部实现。

 9) interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil

10) 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口。

11.5.8 课堂练习

11.5.9 接口编程的最佳实践

实现对 Hero 结构体切片的排序: sort.Sort(data Interface)

package main
import (
"fmt"
"sort"
"math/rand"
)
//1.声明 Hero 结构体
type
Hero struct{
Name string
Age int
}
//2.声明一个 Hero 结构体切片类型
type HeroSlice []Hero
//3.实现 Interface 接口
func (hs HeroSlice) Len() int {
return len(hs)
}
//Less 方法就是决定你使用什么标准进行排序
//1. 按 Hero 的年龄从小到大排序!!
func (hs HeroSlice) Less(i, j int) bool {
return hs[i].Age < hs[j].Age
//修改成对 Name 排序
//return hs[i].Name < hs[j].Name
}
func (hs HeroSlice) Swap(i, j int) {
//交换
// temp := hs[i]
// hs[i] = hs[j]
// hs[j] = temp
//下面的一句话等价于三句话
hs[i], hs[j] = hs[j], hs[i]
}
//1.声明 Student 结构体
type
Student struct{
Name string
Age int
Score float64
}
//将 Student 的切片,安 Score 从大到小排序!!
func main() {
//先定义一个数组/切片
var intSlice = []int{0, -1, 10, 7, 90}
//要求对 intSlice 切片进行排序
//1. 冒泡排序...
//2. 也可以使用系统提供的方法
sort.Ints(intSlice)
fmt.Println(intSlice)
//请大家对结构体切片进行排序
//1. 冒泡排序...
//2. 也可以使用系统提供的方法
//测试看看我们是否可以对结构体切片进行排序
var heroes HeroSlice
for i := 0; i < 10 ; i++ {
hero := Hero{
Name : fmt.Sprintf("英雄|%d", rand.Intn(100)),
Age : rand.Intn(100),
}
//将 hero append 到 heroes 切片
heroes = append(heroes, hero)
}
//看看排序前的顺序
for _ , v := range heroes {
fmt.Println(v)
}
//调用 sort.Sort
sort.Sort(heroes)
fmt.Println("-----------排序后------------")
//看看排序后的顺序
for _ , v := range heroes {
fmt.Println(v)
}
i := 10
j := 20
i, j = j, i
fmt.Println("i=", i, "j=", j) // i=20 j = 10
}
View Code

接口编程的课后练习

//1.声明 Student 结构体
type Student struct{
Name string
Age int
Score float64
}
//将 Student 的切片,安 Score 从大到小排序!!

11.5.10 实现接口 vs 继承
大家听到现在,可能会对实现接口和继承比较迷茫了, 这个问题,那么他们究竟有什么区别呢

 代码说明:

对上面代码的小结
1) 当 A 结构体继承了 B 结构体,那么 A 结构就自动的继承了 B 结构体的字段和方法,并且可以直
接使用
2) 当 A 结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我
们可以认为:实现接口是对继承机制的补充.
实现接口可以看作是对 继承的一种补充

 


接口和继承解决的解决的问题不同
继承的价值主要在于:解决代码的复用性和可维护性。
接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。
接口比继承更加灵活
Person
Student
BirdAble LittleMonkey
接口比继承更加灵活,继承是满足 is - a 的关系,而接口只需满足 like - a 的关系。
接口在一定程度上实现代码解耦
11.6 面向对象编程-多态
11.6.1 基本介绍
变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
11.6.2 快速入门
在前面的 Usb 接口案例,Usb usb ,既可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口 多态特性。[点明]

11.6.3 接口体现多态的两种形式
多态参数
在前面的 Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接
口 多态。
多态数组
演示一个案例:给 Usb 数组中,存放 Phone 结构体 和
Camera 结构体变量
案例说明:

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("相机停止工作。。。")
}
func main() {
//定义一个 Usb 接口数组,可以存放 Phone 和 Camera 的结构体变量
//这里就体现出多态数组
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
fmt.Println(usbArr)
}
View Code

11.7 类型断言
11.7.1 由一个具体的需要,引出了类型断言.

11.7.2 基本介绍
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,
具体的如下:

对上面代码的说明:
在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口
指向的就是断言的类型.
如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic

11.7.3 类型断言的最佳实践 1
在前面的 Usb 接口案例做改进:
给 Phone 结构体增加一个特有的方法 call(), 当 Usb 接口接收的是 Phone 变量时,还需要调用 call
方法, 走代码:

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("手机停止工作。。。")
}
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 (computer 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 usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
//遍历 usbArr
//Phone 还有一个特有的方法 call(),请遍历 Usb 数组,如果是 Phone 变量,
//除了调用 Usb 接口声明的方法外,还需要调用 Phone 特有方法 call. =》类型断言
var computer Computer
for _, v := range usbArr{
computer.Working(v)
fmt.Println()
}
//fmt.Println(usbArr)
}
View Code

11.7.4 类型断言的最佳实践 2
写一函数,循环判断传入参数的类型:

11.7.5 类型断言的最佳实践 3 【学员自己完成】
在前面代码的基础上,增加判断 Student 类型和 *Student 类型

 

第 12 章项目 1-家庭收支记账软件项目


12.1 项目开发流程说明

12.2 项目需求说明


1) 模拟实现基于文本界面的《家庭记账软件》
2) 该软件能够记录家庭的收入、支出,并能够打印收支明细表
12.3 项目的界面

 


其它的界面,我们就直接参考 项目效果图.txt

12.4 项目代码实现

12.4.1 实现基本功能(先使用面向过程,后面改成面向对象)
功能 1:先完成可以显示主菜单,并且可以退出
思路分析:
更加给出的界面完成,主菜单的显示, 当用户输入 4 时,就退出该程序
走代码:

功能 2:完成可以显示明细和登记收入的功能
思路分析:
1) 因为需要显示明细,我们定义一个变量 details string 来记录
2) 还需要定义变量来记录余额(balance)、每次收支的金额(money), 每次收支的说明(note)
走代码:

功能 3:完成了登记支出的功能
思路分析:
登记支出的功能和登录收入的功能类似,做些修改即可
走代码:

12.4.2 项目代码实现改进
1) 用户输入 4 退出时,给出提示"你确定要退出吗? y/n",必须输入正确的 y/n ,否则循环输入指令,直到输入 y 或者 n

2) 当没有任何收支明细时,提示 "当前没有收支明细... 来一笔吧!"

3) 在支出时,判断余额是否够,并给出相应的提示

4) 将 面 向 过 程 的 代 码 修 改 成 面 向 对 象 的 方 法 , 编 写 myFamilyAccount.go , 并 使 用
testMyFamilyAccount.go 去完成测试
思路分析:
把记账软件的功能,封装到一个结构体中,然后调用该结构体的方法,来实现记账,显示明细。结
构体的名字 FamilyAccount .
在通过在 main 方法中,创建一个结构体 FamilyAccount 实例,实现记账即可.
代码实现:
代码不需要重写,只需要重写组织一下.
familyaccount/main/main.go

familyaccount/utils/familyAccount.go

package utils
import (
"fmt"
)
type FamilyAccount struct {
//声明必须的字段.
//声明一个字段,保存接收用户输入的选项
key
string
//声明一个字段,控制是否退出 for
loop bool
//定义账户的余额 []
balance float64
//每次收支的金额
money float64
//每次收支的说明
note string
//定义个字段,记录是否有收支的行为
flag bool
//收支的详情使用字符串来记录
//当有收支时,只需要对 details 进行拼接处理即可
details string
}
//编写要给工厂模式的构造方法,返回一个*FamilyAccount 实例
func NewFamilyAccount() *FamilyAccount {
return &FamilyAccount{
key : "",
loop : true,
balance : 10000.0,
money : 0.0,
note : "",
flag : false,
details : "收支\t 账户金额\t 收支金额\t 说",
}
}
//将显示明细写成一个方法
func (this *FamilyAccount) showDetails() {
fmt.Println("-----------------当前收支明细记录-----------------")
if this.flag {
fmt.Println(this.details)
} else {
fmt.Println("当前没有收支明细... 来一笔吧!")
}
}
//将登记收入写成一个方法,和*FamilyAccount 绑定
func (this *FamilyAccount) income() {
fmt.Println("本次收入金额:")
fmt.Scanln(&this.money)
this.balance += this.money // 修改账户余额
fmt.Println("本次收入说明:")
fmt.Scanln(&this.note)
//将这个收入情况,拼接到 details 变量
//收入
11000
1000
有人发红包
this.details += fmt.Sprintf("\n 收入\t%v\t%v\t%v", this.balance, this.money, this.note)
this.flag = true
}
//将登记支出写成一个方法,和*FamilyAccount 绑定
func (this *FamilyAccount) pay() {
fmt.Println("本次支出金额:")
fmt.Scanln(&this.money)
//这里需要做一个必要的判断
if this.money > this.balance {
fmt.Println("余额的金额不足")
//break
}
this.balance -= this.money
fmt.Println("本次支出说明:")
fmt.Scanln(&this.note)
this.details += fmt.Sprintf("\n 支出\t%v\t%v\t%v", this.balance, this.money, this.note)
this.flag = true
}
//将退出系统写成一个方法,和*FamilyAccount 绑定
func (this *FamilyAccount) exit() {
fmt.Println("你确定要退出吗? y/n")
choice := ""
for {
fmt.Scanln(&choice)
if choice == "y" || choice == "n" {
break
}
fmt.Println("你的输入有误,请重新输入 y/n")
}
if choice == "y" {
this.loop = false
}
}
//给该结构体绑定相应的方法
//显示主菜单
func (this *FamilyAccount) MainMenu() {
for {
fmt.Println("\n-----------------家庭收支记账软件-----------------")
fmt.Println(" 1 收支明细")
fmt.Println(" 2 登记收入")
fmt.Println(" 3 登记支出")
fmt.Println(" 4 退出软件")
fmt.Print("请选择(1-4):")
fmt.Scanln(&this.key)
switch this.key {
case "1":
this.showDetails()
case "2":
this.income()
case "3":
this.pay()
case "4":
this.exit()
default :
fmt.Println("请输入正确的选项..")
}
if !this.loop {
break
}
}
}
View Code

12.4.3 对项目的扩展功能的练习
1) 对上面的项目完成一个转账功能
2) 在使用该软件前,有一个登录功能,只有输入正确的用户名和密码才能操作.

第 13 章项目 2-客户信息关系系统

13.1 项目需求分析
1) 模拟实现基于文本界面的《客户信息管理软件》。
2) 该软件能够实现对客户对象的插入、修改和删除(用切片实现),并能够打印客户明细表
13.2 项目的界面设计
主菜单界面

 


添加客户界面

 


修改客户界面

 


删除客户界面

 


客户列表界面

13.3 客户关系管理系统的程序框架图

13.4 项目功能实现-显示主菜单和完成退出软件功能
功能的说明
当用户运行程序时,可以看到主菜单,当输入 5 时,可以退出该软件.
思路分析
编写 customerView.go ,另外可以把 customer.go 和 customerService.go 写上.
代码实现
customerManage/model/customer.go

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,
}
}
View Code

customerManage/service/customerService.go

package service
import (
"go_code/customerManage/model"
)
//该 CustomerService, 完成对 Customer 的操作,包括
//增删改查
type CustomerService struct {
customers []model.Customer
//声明一个字段,表示当前切片含有多少个客户
//该字段后面,还可以作为新客户的 id+1
customerNum int
}
View Code

customerManage/view/customerView.go

package main
import (
"fmt"
)
type customerView struct {
//定义必要字段
key string //接收用户输入...
loop bool
//表示是否循环的显示主菜单
}
//显示主菜单
func (this *customerView) mainMenu() {
for {
fmt.Println("-----------------客户信息管理软件-----------------")
fmt.Println(" 1 添 加 客 户")
fmt.Println(" 2 修 改 客 户")
fmt.Println(" 3 删 除 客 户")
fmt.Println(" 4 客 户 列 表")
fmt.Println(" 5 退")
fmt.Print("请选择(1-5):")
fmt.Scanln(&this.key)
switch this.key {
case "1" :
fmt.Println("添 加 客 户")
case "2" :
fmt.Println("修 改 客 户")
case "3" :
fmt.Println("删 除 客 户")
case "4" :
fmt.Println("客 户 列 表")
case "5" :
this.loop = false
default :
fmt.Println("你的输入有误,请重新输入...")
}
if !this.loop {
break
}
}
fmt.Println("你退出了客户关系管理系统...")
}
func main() {
//在 main 函数中,创建一个 customerView,并运行显示主菜单..
customerView := customerView{
key : "",
loop : true,
}
//显示主菜单..
customerView.mainMenu()
}
View Code

13.5 项目功能实现-完成显示客户列表的功能
功能说明

 


思路分析

 


代码实现
customerManage/model/customer.go

customerManage/service/customerService.go[增加了两个方法]

customerManage/view/customerView.go

package main
import (
"fmt"
"go_code/customerManage/service"
)
type customerView struct {
//定义必要字段
key string //接收用户输入...
loop bool
//表示是否循环的显示主菜单
//增加一个字段 customerService
customerService *service.CustomerService
}
//显示所有的客户信息
func (this *customerView) list() {
//首先,获取到当前所有的客户信息(在切片中)
customers := this.customerService.List()
//显示
fmt.Println("---------------------------客户列表---------------------------")
fmt.Println("编号\t 姓名\t 性别\t 年龄\t 电话\t 邮箱")
for i := 0; i < len(customers); i++ {
//fmt.Println(customers[i].Id,"\t", customers[i].Name...)
fmt.Println(customers[i].GetInfo())
}
fmt.Printf("\n-------------------------客户列表完成-------------------------\n\n")
}
//显示主菜单
func (this *customerView) mainMenu() {
for {
fmt.Println("-----------------客户信息管理软件-----------------")
fmt.Println(" 1 添 加 客 户")
fmt.Println(" 2 修 改 客 户")
fmt.Println(" 3 删 除 客 户")
fmt.Println(" 4 客 户 列 表")
fmt.Println("5 退出")
fmt.Print("请选择(1-5):")
fmt.Scanln(&this.key)
switch this.key {
case "1" :
fmt.Println("添 加 客 户")
case "2" :
fmt.Println("修 改 客 户")
case "3" :
fmt.Println("删 除 客 户")
case "4" :
this.list()
case "5" :
this.loop = false
default :
fmt.Println("你的输入有误,请重新输入...")
}
if !this.loop {
break
}
}
fmt.Println("你退出了客户关系管理系统...")
}
func main() {
//在 main 函数中,创建一个 customerView,并运行显示主菜单..
customerView := customerView{
key : "",
loop : true,
}
//这里完成对 customerView 结构体的 customerService 字段的初始化
customerView.customerService = service.NewCustomerService()
//显示主菜单..
customerView.mainMenu()
}
View Code

13.6 项目功能实现-添加客户的功能
功能说明

思路分析

代码实现
customerManage/model/customer.go

customerManage/service/customerService.go

customerManage/service/customerView.go

//得到用户的输入,信息构建新的客户,并完成添加
func (this *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 this.customerService.Add(customer) {
fmt.Println("---------------------添加完成---------------------")
} else {
fmt.Println("---------------------添加失败---------------------")
}
}
View Code

13.7 项目功能实现-完成删除客户的功能
功能说明

思路分析

 


代码实现
customerManage/model/customer.go [没有变化]
customerManage/service/customerService.go

customerManage/view/customerView.go

13.8 项目功能实现-完善退出确认功能(课后作业)
功能说明:
要求用户在退出时提示 " 确认是否退出(Y/N):",用户必须输入 y/n, 否则循环提示。
思路分析:
需要编写 customerView.go
代码实现:

13.9 客户关系管理系统-课后练习

第 14 章文件操作

14.1 文件的基本介绍

文件的概念
文件,对我们并不陌生,文件是数据源(保存数据的地方)的一种,比如大家经常使用的 word 文档,txt 文
件,excel 文件...都是文件。文件最主要的作用就是保存数据,它既可以保存一张图片,也可以保持视频,声
音...
输入流和输出流

os.File 封装所有文件相关操作,File 是一个结构体

 


总结:后面我们操作文件,会经常使用到 os.File 结构体.
14.2 打开文件和关闭文件
使用的函数和方法

案例演示

14.3 读文件操作应用实例


1) 读取文件的内容并显示在终端(带缓冲区的方式),使用 os.Open, file.Close, bufio.NewReader(),
reader.ReadString 函数和方法.
代码实现:

package main
import (
"fmt"
"os"
"bufio"
"io"
)
func main() {
//打开文件
//概念说明: file 的叫法
//1. file 叫 file 对象
//2. file 叫 file 指针
//3. file 叫 file 文件句柄
file , err := os.Open("d:/test.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("文件读取结束...")
}
View Code

2) 读取文件的内容并显示在终端(使用 ioutil 一次将整个文件读入到内存中),这种方式适用于文件
不大的情况。相关方法和函数(ioutil.ReadFile)
代码演示:

14.4 写文件操作应用实例


14.4.1 基本介绍-os.OpenFile 函数

14.4.2 基本应用实例-方式一
1) 创建一个新文件,写入内容 5 句 "hello, Gardon"
代码实现:

 

2) 打开一个存在的文件中,将原来的内容覆盖成新的内容 10 句 "你好,尚硅谷!"

package main
import (
"fmt"
"bufio"
"os"
)
func main() {
//打开一个存在的文件中,将原来的内容覆盖成新的内容 10 句 "你好,尚硅谷!"
//创建一个新文件,写入内容 5 句 "hello, Gardon"
//1 .打开文件已经存在文件, d:/abc.txt
filePath := "d:/abc.txt"
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()
//准备写入 5 句 "你好,尚硅谷!"
str := "你好,尚硅谷!\r\n" // \r\n 表示换行
//写入时,使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString(str)
}
//因为 writer 是带缓存,因此在调用 WriterString 方法时,其实
//内容是先写入到缓存的,所以需要调用 Flush 方法,将缓冲的数据
//真正写入到文件中, 否则文件中会没有数据!!!
writer.Flush()
}
View Code

3) 打开一个存在的文件,在原来的内容追加内容 'ABC! ENGLISH!'
代码实现:

package main
import (
"fmt"
"bufio"
"os"
)
func main() {
//打开一个存在的文件,在原来的内容追加内容 'ABC! ENGLISH!'
//1 .打开文件已经存在文件, d:/abc.txt
filePath := "d:/abc.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()
//准备写入 5 句 "你好,尚硅谷!"
str := "ABC,ENGLISH!\r\n" // \r\n 表示换行
//写入时,使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString(str)
}
//因为 writer 是带缓存,因此在调用 WriterString 方法时,其实
//内容是先写入到缓存的,所以需要调用 Flush 方法,将缓冲的数据
//真正写入到文件中, 否则文件中会没有数据!!!
writer.Flush()
}
View Code

4) 打开一个存在的文件,将原来的内容读出显示在终端,并且追加 5 句"hello,北京!"
代码实现:

package main
import (
"fmt"
"bufio"
"os"
"io"
)
func main() {
//打开一个存在的文件,将原来的内容读出显示在终端,并且追加 5 句"hello,北京!"
//1 .打开文件已经存在文件, d:/abc.txt
filePath := "d:/abc.txt"
file, err := os.OpenFile(filePath, os.O_RDWR | 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')
if err == io.EOF { //如果读取到文件的末尾
break
}
//显示到终端
fmt.Print(str)
}
//准备写入 5 句 "你好,尚硅谷!"
str := "hello,北京!\r\n" // \r\n 表示换行
//写入时,使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
//因为 writer 是带缓存,因此在调用 WriterString 方法时,其实
//内容是先写入到缓存的,所以需要调用 Flush 方法,将缓冲的数据
//真正写入到文件中, 否则文件中会没有数据!!!
writer.Flush()
}
View Code

14.4.3 基本应用实例-方式二
编程一个程序,将一个文件的内容,写入到另外一个文件。注:这两个文件已经存在了.
说明:使用 ioutil.ReadFile / ioutil.WriteFile 完成写文件的任务.
代码实现:

14.4.4 判断文件是否存在

14.5 文件编程应用实例
14.5.1 拷贝文件
说明:将一张图片/电影/mp3 拷贝到另外一个文件 e:/abc.jpg
io 包
func Copy(dst Writer, src Reader) (written int64, err error)
注意; Copy 函数是 io 包提供的.
代码实现:

package main
import (
"fmt"
"os"
"io"
"bufio"
)
//自己编写一个函数,接收两个文件路径 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() {
//将 d:/flower.jpg 文件拷贝到 e:/abc.jpg
//调用 CopyFile 完成文件拷贝
srcFile := "d:/flower.jpg"
dstFile := "e:/abc.jpg"
_, err := CopyFile(dstFile, srcFile)
if err == nil {
fmt.Printf("拷贝完成\n")
} else {
fmt.Printf("拷贝错误 err=%v\n", err)
}
}
View Code

14.5.2 统计英文、数字、空格和其他字符数量
说明:统计一个文件中含有的英文、数字、空格及其它字符数量
代码实现:

package main
import (
"fmt"
"os"
"io"
"bufio"
)
//定义一个结构体,用于保存统计结果
type CharCount struct {
ChCount int // 记录英文个数
NumCount int // 记录数字的个数
SpaceCount int // 记录空格的个数
OtherCount int // 记录其它字符的个数
}
func main() {
//思路: 打开一个文件, 创一个 Reader
//每读取一行,就去统计该行有多少个 英文、数字、空格和其他字符
//然后将结果保存到一个结构体
fileName := "e:/abc.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
str = []run(str)
//遍历 str ,进行统计
for _, v := range str {
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",
count.ChCount, count.NumCount, count.SpaceCount, count.OtherCount)
}
View Code

14.6 命令行参数
14.6.1 看一个需求
我们希望能够获取到命令行输入的各种参数,该如何处理? 如图:=> 命令行参数

14.6.2 基本介绍
os.Args 是一个 string 的切片,用来存储所有的命令行参数
14.6.3 举例说明
请编写一段代码,可以获取命令行各个参数

代码实现:

14.6.4 flag 包用来解析命令行参数
说明: 前面的方式是比较原生的方式,对解析参数不是特别的方便,特别是带有指定参数形式的命
令行。
比如:cmd>main.exe
-f c:/aaa.txt -p 200 -u root 这样的形式命令行,go 设计者给我们提供了 flag
包,可以方便的解析命令行参数,而且参数顺序可以随意
请编写一段代码,可以获取命令行各个参数.

代码实现:

14.7 json 基本介绍


概述

应用场景(示意图)

 


14.8 json 数据格式说明

14.9 json 数据在线解析
https://www.json.cn/ 网站可以验证一个 json 格式的数据是否正确。尤其是在我们编写比较复杂的
json 格式数据时,很有用。

14.10json 的序列化


介绍
json 序列化是指,将有 key-value 结构的数据类型(比如结构体、map、切片)序列化成 json 字符串
的操作。
应用案例
这里我们介绍一下结构体、map 和切片的序列化,其它数据类型的序列化类似。
代码演示

package main
import (
"fmt"
"encoding/json"
)
//定义一个结构体
type Monster struct {
Name string
Age int
Birthday string
Sal float64
Skill string
}
func testStruct() {
//演示
monster := Monster{
Name :"牛魔王",
Age : 500,
Birthday : "2011-11-11",
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()//演示对基本数据类型的序列化
}
View Code

注意事项
对于结构体的序列化,如果我们希望序列化后的 key 的名字,又我们自己重新制定,那么可以给 struct
指定一个 tag 标签.

序列化后:

{"monster_name":"牛魔王","monster_age":500,"Birthday":"2011-11-11","Sal":8000,"Skill":"牛魔拳"}

14.11json 的反序列化


基本介绍

第 386页尚硅谷 Go 语言课程
json 反序列化是指,将 json 字符串反序列化成对应的数据类型(比如结构体、map、切片)的操作
应用案例
这里我们介绍一下将 json 字符串反序列化成结构体、map 和切片
代码演示:

package main
import (
"fmt"
"encoding/json"
)
//定义一个结构体
type Monster struct {
Name string
Age int
Birthday string //....
Sal float64
Skill string
}
//演示将 json 字符串,反序列化成 struct
func unmarshalStruct() {
//说明 str 在项目开发中,是通过网络传输获取到.. 或者是读取文件获取到
str := "{\"Name\":\"牛魔王\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"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()
}
View Code

对上面代码的小结说明
1) 在反序列化一个 json 字符串时,要确保反序列化后的数据类型和原来序列化前的数据类型一致。
2) 如果 json 字符串是通过程序获取到的,则不需要再对 “ 转义处理。

第 15 章单元测试

15.1 先看一个需求
在我们工作中,我们会遇到这样的情况,就是去确认一个函数,或者一个模块的结果是否正确,
如:

15.2 传统的方法
15.2.1 传统的方式来进行测试
在 main 函数中,调用 addUpper 函数,看看实际输出的结果是否和预期的结果一致,如果一致,
则说明函数正确,否则函数有错误,然后修改错误
代码实现:

15.2.2 传统方法的缺点分析


1) 不方便, 我们需要在 main 函数中去调用,这样就需要去修改 main 函数,如果现在项目正在运行,就可能去停止项目。
2) 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在 main 函数,不利于我们管理和清晰我们思路
3) 引出单元测试。-> testing 测试框架 可以很好解决问题。
15.3 单元测试-基本介绍
Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试,testing 框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题:
1) 确保每个函数是可运行,并且运行结果是正确的
2) 确保写出来的代码性能是好的,
3) 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定
15.4 单元测试-快速入门
使用 Go 的单元测试,对 addUpper 和 sub 函数进行测试。
特别说明: 测试时,可能需要暂时退出 360。(因为 360 可能会认为生成的测试用例程序是木马)
演示如何进行单元测试:

单元测试的运行原理示意图:

15.4.1 单元测试快速入门总结

1) 测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go , cal 不是固定的。
2) 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper
3) TestAddUpper(t *tesing.T)
的形参类型必须是 *testing.T 【看一下手册】
4) 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub
5) 运行测试用例指令
(1) cmd>go test
(2) cmd>go test -v
[如果运行正确,无日志,错误时,会输出日志]
[运行正确或是错误,都输出日志]
6) 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序
7) t.Logf 方法可以输出相应的日志
8) 测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处[原理图].
9) PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败
10) 测试单个文件,一定要带上被测试的原文件
go test -v cal_test.go cal.go
11) 测试单个方法
go test -v -test.run TestAddUpper

15.5 单元测试-综合案例

代码实现:
monster/monster.go

package monster
import (
"encoding/json"
"io/ioutil"
"fmt"
)
type Monster struct {
Name string
Age int
Skill string
}
//给 Monster 绑定方法 Store, 可以将一个 Monster 变量(对象),序列化后保存到文件中
func (this *Monster) Store() bool {
//先序列化
data, err := json.Marshal(this)
if err != nil {
fmt.Println("marshal err =", err)
return false
}
//保存到文件
filePath := "d:/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 (this *Monster) ReStore() bool {
//1. 先从文件中,读取序列化的字符串
filePath := "d:/monster.ser"
data, err := ioutil.ReadFile(filePath)
if err != nil {
fmt.Println("ReadFile err =", err)
return false
}
//2.使用读取到 data []byte ,对反序列化
err = json.Unmarshal(data, this)
if err != nil {
fmt.Println("UnMarshal err =", err)
return false
}
return true
}
View Code

monster/monster_test.go

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() 测试成功!")
}
View Code 

第 16 章goroutine 和 channel

16.1 goroutine-看一个需求
需求:要求统计 1-9000000000 的数字中,哪些是素数?
分析思路:
1) 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
2) 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到
goroutine.【速度提高 4 倍】
16.2 goroutine-基本介绍
16.2.1 进程和线程介绍

16.2.2 程序、进程和线程的关系示意图

16.2.3 并发和并行

并发和并行
1) 多线程程序在单核上运行,就是并发
2) 多线程程序在多核上运行,就是并行
3) 示意图:

小结
16.2.4 Go 协程和 Go 主线程
Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以
这样理解,协程是轻量级的线程[编译器做优化]。
Go 协程的特点

1) 有独立的栈空间
2) 共享程序堆空间
3) 调度由用户控制
4) 协程是轻量级的线程

一个示意图

16.3 goroutine-快速入门

16.3.1 案例说明
请编写一个程序,完成如下功能:
1) 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 "hello,world"
2) 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
3) 要求主线程和 goroutine 同时执行.
4) 画出主线程和协程执行流程图

代码实现
输出的效果说明, main 这个主线程和 test 协程同时执行.

主线程和协程执行流程图

16.3.2 快速入门小结

1) 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
2) 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
3) Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一
般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了
16.4 goroutine 的调度模型
16.4.1 MPG 模式基本介绍

16.4.2 MPG 模式运行的状态 1

16.4.3 MPG 模式运行的状态 2

16.5 设置 Golang 运行的 cpu 数
介绍:为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目

16.6 channel(管道)-看个需求

需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。
要求使用 goroutine 完成
分析思路:
1) 使用 goroutine 来完成,效率高,但是会出现并发/并行安全问题.
2) 这里就提出了不同 goroutine 如何通信的问题
代码实现
1) 使用 goroutine 来完成(看看使用 gorotine 并发完成会出现什么问题? 然后我们会去解决)
2) 在运行某个程序时,如何知道是否存在资源竞争问题。 方法很简单,在编译该程序时,增加一
个参数
-race 即可 [示意图]
3) 代码实现:

package main
import (
"fmt"
"time"
)
// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。
// 最后显示出来。要求使用 goroutine 完成
// 思路
// 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 //concurrent map writes?
}
func main() {
// 我们这里开启多个协程完成这个任务[200 个]
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠 10 秒钟【第二个问题 】
time.Sleep(time.Second * 10)
//这里我们输出结果,变量这个结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
View Code

4) 示意图:

16.6.1 不同 goroutine 之间如何通讯
1) 全局变量的互斥锁
2) 使用管道 channel 来解决
16.6.2 使用全局变量加锁同步改进程序
因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map
writes
解决方案:加入互斥锁
我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)
代码改进

16.6.3 为什么需要 channel

1) 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
2) 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
3) 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁
4) 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
5) 上面种种分析都在呼唤一个新的通讯机制-channel
16.6.4 channel 的基本介绍
1) channle 本质就是一个数据结构-队列【示意图】
2) 数据是先进先出【FIFO : first in first out】
3) 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
4) channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
5) 示意图:

16.6.5 定义/声明 channel

var 变量名 chan 数据类型

举例:

var intChan chan
var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
var perChan chan  Person
var perChan2 chan *Person
int (intChan 用于存放 int 数据)

说明
channel 是引用类型
channel 必须初始化才能写入数据, 即 make 后才能使用
管道是有类型的,intChan 只能写入 整数 int
16.6.6 管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项

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)) // 3, 3
//5. 从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
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)
}
View Code

16.6.7 channel 使用的注意事项
1) channel 中只能存放指定的数据类型
2) channle 的数据放满后,就不能再放入了
3) 如果从 channel 取出数据后,可以继续放入
4) 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock

16.6.8 读写 channel 案例演示

16.7 管道的课后练习题

16.8 channel 的遍历和关闭
16.8.1 channel 的关闭
使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据
案例演示:

16.8.2 channel 的遍历
channel 支持 for--range 的方式进行遍历,请注意两个细节
1) 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
2) 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
16.8.3 channel 遍历和关闭的案例演示
看代码演示:

16.8.4 应用实例 1
思路分析:

代码的实现:

package main
import (
"fmt"
_ "time"
)
//write Data
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
//放入数据
intChan<- i
fmt.Println("writeData ", i)
//time.Sleep(time.Second)
}
close(intChan) //关闭
}
//read data
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
//time.Sleep(time.Second)
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)
//time.Sleep(time.Second * 10)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
View Code

16.8.5 应用实例 2-阻塞

16.8.6 应用实例 3

需求:
要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有 goroutine和 channel 的知识后,就可以完成了 [测试数据: 80000]
分析思路:
传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。
使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine 去完成,完成任务时间短。
画出分析思路

代码实现

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) {
//使用 for 循环
// var num int
var flag bool //
for {
time.Sleep(time.Millisecond * 10)
num, ok := <-intChan
if !ok { //intChan 取不到..
break
}
flag = true //假设是素数
//判断 num 是不是素数
for i := 2; i < num; i++ {
if num % i == 0 {//说明该 num 不是素数
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
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//这里我们主线程,进行处理
//直接
go func(){
for i := 0; i < 4; i++ {
<-exitChan
}
//当我们从 exitChan 取出了 4 个结果,就可以放心的关闭 prprimeChan
close(primeChan)
}()
//遍历我们的 primeChan ,把结果取出
for {
res, ok := <-primeChan
if !ok{
break
}
//将结果输出
fmt.Printf("素数=%d\n", res)
}
fmt.Println("main 线程退出")
}
View Code

结论:使用 go 协程后,执行的速度,比普通方法提高至少 4 倍
16.9 channel 使用细节和注意事项
1) channel 可以声明为只读,或者只写性质 【案例演示】


2) channel 只读和只写的最佳实践案例

3) 使用 select 可以解决从管道取数据的阻塞问题

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 方式可以解决
//label:
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
//break label
}
}
}
4) goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
代码实现:
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[0] = "golang" //error
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}
View Code

第 17 章反射

17.1 先看一个问题,反射的使用场景

17.2 使用反射机制,编写函数的适配器, 桥连接


17.3 反射的基本介绍

17.3.1 基本介绍
1) 反射可以在运行时动态获取变量的各种信息, 比如变量的类型(type),类别(kind)
2) 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
3) 通过反射,可以修改变量的值,可以调用关联的方法。
4) 使用反射,需要 import (“reflect”)
5) 示意图

17.3.2 反射的应用场景

17.3.3 反射重要的函数和概念

3) 变量、interface{} 和 reflect.Value 是可以相互转换的,这点在实际开发中,会经常使用到。画出示意图

17.4 反射的快速入门

17.4.1 快速入门说明
请编写一个案例,演示对(基本数据类型、interface{}、reflect.Value)进行反射的基本操作
代码演示,见下面的表格:
请编写一个案例,演示对(结构体类型、interface{}、reflect.Value)进行反射的基本操作
代码演示:

package main
import (
"reflect"
"fmt"
)
//专门演示反射
func reflectTest01(b interface{}) {
//通过反射获取的传入的变量的 type , kind, 值
//1. 先获取到 reflect.Type
rTyp := reflect.TypeOf(b)
fmt.Println("rType=", rTyp)
//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
rTyp := reflect.TypeOf(b)
fmt.Println("rType=", rTyp)
//2. 获取到 reflect.Value
rVal := reflect.ValueOf(b)
//下面我们将 rVal 转成 interface{}
iV := rVal.Interface()
fmt.Printf("iv=%v iv type=%T \n", iV, iV)
//将 interface{} 通过断言转成需要的类型
//这里,我们就简单使用了一带检测的类型断言.
//同学们可以使用 swtich 的断言形式来做的更加的灵活
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 = 100
// reflectTest01(num)
//2. 定义一个 Student 的实例
stu := Student{
Name : "tom",
Age : 20,
}
reflectTest02(stu)
}
View Code

17.5 反射的注意事项和细节

1) reflect.Value.Kind,获取变量的类别,返回的是一个常量


2) Type 和 Kind 的区别
Type 是类型, Kind 是类别, Type 和 Kind 可能是相同的,也可能是不同的.
比如: var num int = 10  num 的 Type 是 int , Kind 也是 int
比如: var stu Student stu 的 Type 是 pkg1.Student , Kind 是 struct

5) 通过反射的来修改变量, 注意当使用 SetXxx 方法来设置需要通过对应的指针类型来完成, 这样才能改变传入的变量的值, 同时需要使用到 reflect.Value.Elem()方法

6) reflect.Value.Elem() 应该如何理解?

17.6 反射课堂练习

1) 给你一个变量
var v float64 = 1.2 , 请使用反射来得到它的 reflect.Value, 然后获取对应的 Type,
Kind 和值,并将 reflect.Value 转换成 interface{}
, 再将 interface{} 转换成 float64. [不说:]
2) 看段代码,判断是否正确,为什么

package main
import (
  "fmt"
  "reflect"
)
func main() {
  var str string = "tom"
  //ok
  fs := reflect.ValueOf(str) //ok fs -> string
  fs.SetString("jack") //error
  fmt.Printf("%v\n", str)
}

修改如下:

17.7 反射最佳实践

1) 使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值

package main
import (
"fmt"
"reflect"
)
//定义了一个 Monster 结构体
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32 `json:"成绩"`
Sex
string
}
//方法,返回两个数的和
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 methods\n", numOfMethod)
//var params []reflect.Value
//方法的排序默认是按照 函数名的排序(ASCII 码)
val.Method(1).Call(nil) //获取到第二个方法。调用它
//调用结构体的第 1 个方法 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)
}
View Code

2) 使用反射的方式来获取结构体的 tag 标签, 遍历字段的值,修改字段值,调用结构体方法(要求:
通过传递地址的方式完成, 在前面案例上修改即可)
3) 定义了两个函数 test1 和 test2,定义一个适配器函数用作统一处理接口【了解】
4) 使用反射操作任意结构体类型:【了解】
5) 使用反射创建并操作结构体
17.8 课后作业

第 18 章tcp 编程

18.1 看两个实际应用
QQ,迅雷,百度网盘客户端.
新浪网站,京东商城,淘宝..

18.2 网络编程基本介绍

Golang 的主要设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端 程序必不可少也是至关重要的一部分。
网络编程有两种:
1) TCP socket 编程,是网络编程的主流。之所以叫 Tcp socket 编程,是因为底层是基于 Tcp/ip 协议的. 比如: QQ 聊天 [示意图]
2) b/s 结构的 http 编程,我们使用浏览器去访问服务器时,使用的就是 http 协议,而 http 底层依旧是用 tcp socket 实现的。[示意图] 比如: 京东商城 【这属于 go web 开发范畴 】
18.2.1 网线,网卡,无线网卡
计算机间要相互通讯,必须要求网线,网卡,或者是无线网卡.

18.2.2 协议(tcp/ip)
TCP/IP(Transmission Control Protocol/Internet Protocol)的简写,中文译名为传输控制协议/因特网互联协议,又叫网络通讯协议,这个协议是 Internet 最基本的协议、Internet 国际互联网络的基础,简单地说,就是由网络层的 IP 协议和传输层的 TCP 协议组成的。

18.2.3 OSI 与 Tcp/ip 参考模型 (推荐 tcp/ip 协议 3 卷)

18.2.4 ip 地址

概述:每个 internet 上的主机和路由器都有一个 ip 地址,它包括网络号和主机号, ip 地址有 ipv4(32位)或者 ipv6(128 位). 可以通过 ipconfig 来查看

18.2.5 端口(port)-介绍

我们这里所指的端口不是指物理意义上的端口,而是特指 TCP/IP 协议中的端口,是逻辑意义上的端口。如果把 IP 地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个门,但是一个 IP 地址的端口 可以有 65536(即:256×256)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从 0 到 65535(256×256-1)

18.2.6 端口(port)-分类

0 号是保留端口.
1-1024 是固定端口(程序员不要使用)
又叫有名端口,即被某些程序固定使用,一般程序员不使用.
22: SSH 远程登录协议 23: telnet 使用
25: smtp 服务使用
21: ftp 使用
80: iis 使用 7: echo 服务
1025-65535 是动态端口
这些端口,程序员可以使用.
18.2.7 端口(port)-使用注意
1) 在计算机(尤其是做服务器)要尽可能的少开端口
2) 一个端口只能被一个程序监听
3) 如果使用 netstat –an 可以查看本机有哪些端口在监听
4) 可以使用 netstat –anb 来查看监听端口的 pid,在结合任务管理器关闭不安全的端口
18.3 tcp socket 编程的客户端和服务器端为了授课方法,我们将 tcp socket 编程,简称 socket 编程.下图为 Golang socket 编程中客户端和服务器的网络分布

18.4 tcp socket 编程的快速入门
18.4.1 服务端的处理流程
1) 监听端口 8888
2) 接收客户端的 tcp 链接,建立客户端和服务器端的链接.
3) 创建 goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
18.4.2 客户端的处理流程
1) 建立与服务端的链接
2) 发送请求数据[终端],接收服务器端返回的结果数据
3) 关闭链接

18.4.3 简单的程序示意图

18.4.4 代码的实现

程序框架图示意图
服务器端功能:
编写一个服务器端程序,在 8888 端口监听
可以和多个客户端创建链接
链接成功后,客户端可以发送数据,服务器端接受数据,并显示在终端上.
先使用 telnet 来测试,然后编写客户端程序来测试
服务端的代码:

package main
import (
"fmt"
"net" //做网络 socket 开发时,net 包含有我们需要所有的方法和函数
_"io"
)
func process(conn net.Conn) {
//这里我们循环的接收客户端发送的数据
defer conn.Close() //关闭 conn
for {
//创建一个新的切片
buf := make([]byte, 1024)
//conn.Read(buf)
//1. 等待客户端通过 conn 发送信息
//2. 如果客户端没有 wrtie[发送],那么协程就阻塞在这里
fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
n , err := conn.Read(buf) //从 conn 读取
if err != nil {
fmt.Printf("客户端退出 err=%v", err)
return //!!!
}
//3. 显示客户端发送的内容到服务器的终端
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服务器开始监听....")
//net.Listen("tcp", "0.0.0.0:8888")
//1. tcp 表示使用网络协议是 tcp
//2. 0.0.0.0:8888 表示在本地监听 8888 端口
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", 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)
}
server.go

客户端功能:
1. 编写一个客户端端程序,能链接到 服务器端的 8888 端口
2. 客户端可以发送单行数据,然后就退出
3. 能通过终端输入数据(输入一行发送一行), 并发送给服务器端 []
4. 在终端输入 exit,表示退出程序.
5. 代码:

package main
import (
"fmt"
"net"
"bufio"
"os"
)
func main() {
conn, err := net.Dial("tcp", "192.168.20.253:8888")
if err != nil {
fmt.Println("client dial err=", err)
return
}
//功能一:客户端可以发送单行数据,然后就退出
reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]
//从终端读取一行用户输入,并准备发送给服务器
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
//再将 line 发送给 服务器
n, err := conn.Write([]byte(line))
if err != nil {
fmt.Println("conn.Write err=", err)
}
fmt.Printf("客户端发送了 %d 字节的数据,并退出", n)
}
client.go

对 client.go 做了改进:

18.5 经典项目-海量用户即时通讯系统
18.5.1 项目开发流程
需求分析--> 设计阶段---> 编码实现 --> 测试阶段-->实施
18.5.2 需求分析
1) 用户注册
2) 用户登录
3) 显示在线用户列表
4) 群聊(广播)
5) 点对点聊天
6) 离线留言
18.5.3 界面设计

18.5.4 项目开发前技术准备
项目要保存用户信息和消息数据,因此我们需要学习数据库(Redis 或者 Mysql) , 这里我们选择
Redis , 所以先给同学们讲解如何在 Golang 中使用 Redis.

18.5.5 实现功能-显示客户端登录菜单
功能:能够正确的显示客户端的菜单。
界面:

思路分析:这个非常简单,直接写.
代码实现:
client/main.go

client/login.go

18.5.6 实现功能-完成用户登录
要求:先完成指定用户的验证,用户 id=100, 密码 pwd=123456 可以登录,其它用户不能登录
这里需要先说明一个 Message 的组成(示意图),并发送一个 Message 的流程

1.完成客户端可以发送消息长度,服务器端可以正常收到该长度值
分析思路
(1) 先确定消息 Message 的格式和结构
(2) 然后根据上图的分析完成代码
(3) 示意图

代码实现:
server/main.go

common/message/message.go

client/main.go
和前面的代码一样,没有修改
client/login.go

2.完成 客户端可以发送消息本身,服 务器端可以正 常接收到消息,并根据客户 端发送的消 息(LoginMes), 判断用户的合法性,并返回相应的 LoginResMes
思路分析:
(1) 让客户端发送消息本身
(2) 服务器端接受到消息, 然后反序列化成对应的消息结构体.
(3) 服务器端根据反序列化成对应的消息, 判断是否登录用户是合法, 返回 LoginResMes
(4) 客户端解析返回的 LoginResMes,显示对应界面
(5) 这里我们需要做函数的封装
代码实现:
client/login.go 做了修改

server/main.go 修改

 
将读取包的任务封装到了一个函数中.readPkg()

能够完成登录,并提示相应信息
server/main.go 修改

client/utils.go 新增

client/login.go 增加代码

程序结构的改进, 前面的程序虽然完成了功能,但是没有结构,系统的可读性、扩展性和维护性
都不好,因此需要对程序的结构进行改进。
1) 先改进服务端, 先画出程序的框架图[思路],再写代码.

2) 步骤
[1] . 先把分析出来的文件,创建好,然后放到相应的文件夹[包]

[2] 现在根据各个文件,完成的任务不同,将 main.go 的代码剥离到对应的文件中即可。
[3] 先修改了 utils/utils.go

 

[4] 修改了 process2/userProcess.go

[5] 修改了 main/processor.go

[6] 修改 main/main.go

修改客户端, 先画出程序的框架图[思路],再写代码
[1] 步骤 1-画出示意图

[2] 先把各个文件放到对应的文件夹[包]

[3] 将 server/utils.go 拷贝到 client/utils/utils.go
[4] 创建了 server/process/userProcess.go

说明:该文件就是在原来的 login.go 做了一个改进,即封装到 UserProcess 结构体
[5] 创建了 server/process/server.go

[6] server/main/main.go 修改

在 Redis 手动添加测试用户,并画图+说明注意. (后面通过程序注册用户)

手动直接在 redis 增加一个用户信息:

如输入的用户名密码在 Redis 中存在则登录,否则退出系统,并给出相应的
提示信息:
1. 用户不存在,你也可以重新注册,再登录
2. 你密码不正确。。
代码实现:
[1] 编写 model/user.go

[2] 编写 model/error.go

[3] 编写 model/userDao.go

package model
import (
"fmt"
"github.com/garyburd/redigo/redis"
"encoding/json"
)
//我们在服务器启动后,就初始化一个 userDao 实例,
//把它做成全局的变量,在需要和 redis 操作时,就直接使用即可
var (
MyUserDao *UserDao
)
//定义一个 UserDao 结构体体
//完成对 User 结构体的各种操作.
type UserDao struct {
pool *redis.Pool
}
//使用工厂模式,创建一个 UserDao 实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao) {
userDao = &UserDao{
pool: pool,
}
return
}
//思考一下在 UserDao 应该提供哪些方法给我们
//1. 根据用户 id 返回 一个 User 实例+err
func (this *UserDao) getUserById(conn redis.Conn, id int) (user *User, err error) {
//通过给定 id 去 redis 查询这个用户
res, err := redis.String(conn.Do("HGet", "users", id))
if err != nil {
//错误!
if err == redis.ErrNil { //表示在 users 哈希中,没有找到对应 id
err = ERROR_USER_NOTEXISTS
}
return
}
user = &User{}
//这里我们需要把 res 反序列化成 User 实例
err = json.Unmarshal([]byte(res), user)
if err != nil {
fmt.Println("json.Unmarshal err=", err)
return
}
return
}
//完成登录的校验 Login
//1. Login 完成对用户的验证
//2. 如果用户的 id 和 pwd 都正确,则返回一个 user 实例
//3. 如果用户的 id 或 pwd 有错误,则返回对应的错误信息
func (this *UserDao) Login(userId int, userPwd string) (user *User, err error) {
//先从 UserDao 的连接池中取出一根连接
conn := this.pool.Get()
defer conn.Close()
user, err = this.getUserById(conn, userId)
if err != nil {
return
}
//这时证明这个用户是获取到.
if user.UserPwd != userPwd {
err = ERROR_USER_PWD
return
}
return
}
View Code

[4] main/redis.go

[5] main/main.go

[6] 在 process/userProcess.go 使用到 redis 验证的功能

18.5.7 实现功能-完成注册用户
1) 完成注册功能,将用户信息录入到 Redis 中
2) 思路分析,并完成代码
3) 思路分析的示意图
实现功能-完成注册用户
[1] common/message/user.go

[2] common/message/message.go

[3] client/process/userProcess.go

func (this *UserProcess) Register(userId int,
userPwd string, userName 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.RegisterMesType
//3. 创建一个 LoginMes 结构体
var registerMes message.RegisterMes
registerMes.User.UserId = userId
registerMes.User.UserPwd = userPwd
registerMes.User.UserName = userName
//4.将 registerMes 序列化
data, err := json.Marshal(registerMes)
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
}
//创建一个 Transfer 实例
tf := &utils.Transfer{
Conn : conn,
}
//发送 data 给服务器端
err = tf.WritePkg(data)
if err != nil {
fmt.Println("注册发送信息错误 err=", err)
}
mes, err = tf.ReadPkg() // mes 就是 RegisterResMes
if err != nil {
fmt.Println("readPkg(conn) err=", err)
return
}
//将 mes 的 Data 部分反序列化成 RegisterResMes
var registerResMes message.RegisterResMes
err = json.Unmarshal([]byte(mes.Data), &registerResMes)
if registerResMes.Code == 200 {
fmt.Println("注册成功, 你重新登录一把")
os.Exit(0)
} else {
fmt.Println(registerResMes.Error)
os.Exit(0)
}
return
}
userProcess.go

[4] 在 client/main/main.go 增加了代码

[5] 在 server/model/userDao.go 增加方法

[6] 在 server/process/userProcess.go 增加了方法,处理注册

func (this *UserProcess) ServerProcessRegister(mes *message.Message) (err error) {
//1.先从 mes 中取出 mes.Data ,并直接反序列化成 RegisterMes
var registerMes message.RegisterMes
err = json.Unmarshal([]byte(mes.Data), &registerMes)
if err != nil {
fmt.Println("json.Unmarshal fail err=", err)
return
}
//1 先声明一个 resMes
var resMes message.Message
resMes.Type = message.RegisterResMesType
var registerResMes message.RegisterResMes
//我们需要到 redis 数据库去完成注册.
//1.使用 model.MyUserDao 到 redis 去验证
err = model.MyUserDao.Register(&registerMes.User)
if err != nil {
if err == model.ERROR_USER_EXISTS {
registerResMes.Code = 505
registerResMes.Error = model.ERROR_USER_EXISTS.Error()
} else {
registerResMes.Code = 506
registerResMes.Error = "注册发生未知错误..."
}
} else {
registerResMes.Code = 200
}
data, err := json.Marshal(registerResMes)
if err != nil {
fmt.Println("json.Marshal fail", err)
return
}
//4. 将 data 赋值给 resMes
resMes.Data = string(data)
//5. 对 resMes 进行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("json.Marshal fail", err)
return
}
//6. 发送 data, 我们将其封装到 writePkg 函数
//因为使用分层模式(mvc), 我们先创建一个 Transfer 实例,然后读取
tf := &utils.Transfer{
Conn : this.Conn,
}
err = tf.WritePkg(data)
return
}
View Code

[7] server/main/processor.go 调用了

18.5.8 实现功能-完成登录时能返回当前在线用户
用户登录后,可以得到当前在线用户列表思路分析、示意图、代码实现
思路分析:

代码实现:
[1] 编写了 server/process/userMgr.go

[2] server/process/userProcess.go

[3] common/message/message.go

[4] client/process/userProcess.go

当一个新的用户上线后,其它已经登录的用户也能获取最新在线用户列表,思路分析、示意图、代码实现
[1] server/process/userProcess.go

[2] sever/proces/userProcess.go [的 Login]

[3] common/mesage/message.go

[4] client/process/userMgr.go

[5] client/process/server.go

[6] client/process/server.go

18.5.9 实现功能-完成登录用可以群聊

步骤 1:步骤 1:当一个用户上线后,可以将群聊消息发给服务器,服务器可以接收到
思路分析:

代码实现:
[1] common/message/messag.go

[2] client/model/curUser.go

[3] client/process/smsProcess.go 增加了发送群聊消息

[4] 测试

步骤 2:服务器可以将接收到的消息,群发给所有在线用户(发送者除外)
思路分析:

代码实现:
[1] server/process/smsProcess.go

[2] server/main/processor.go

[3] client/process/smsMgr.go

[4] client/process/server.go

18.5.10 聊天的项目的扩展功能要求
1. 实现私聊.[点对点聊天]
2. 如果一个登录用户离线,就把这个人从在线列表去掉【】
3. 实现离线留言,在群聊时,如果某个用户没有在线,当登录后,可以接受离线的消息
4. 发送一个文件.

第 19 章Redis 的使用


19.1 Redis 基本介绍

19.1.1 Redis 的安装

19.1.2 Redis 操作的基本原理图

19.2 Redis 的安装和基本使用

19.2.1 Redis 的启动:

19.3 Redis 的操作指令一览

19.3.1 Redis 的基本使用:

说明: Redis 安装好后,默认有 16 个数据库,初始默认使用 0 号库, 编号是 0...15
1. 添加 key-val[set]
2. 查看当前 redis 的 所有 key[keys *]
3. 获取 key 对应的值. [get key]
4. 切换 redis 数据库 [select index]
5. 如何查看当前数据库的 key-val 数量 [dbsize]
6. 清空当前数据库的 key-val 和清空所有数据库的 key-val [flushdb flushall]

19.4 Redis 的 Crud 操作

19.4.1 Redis 的五大数据类型:
Redis 的五大数据类型是: String(字符串) 、Hash (哈希)、List(列表)、Set(集合)
和 zset(sorted set:有序集合)
19.4.2 String(字符串) -介绍
string 是 redis 最基本的类型,一个 key 对应一个 value。
string 类型是二进制安全的。除普通的字符串外,也可以存放图片等数据。
redis 中字符串 value 最大是 512M
举例,存放一个地址信息:
address 北京天安门
说明 :
key : address

value: 北京天安门

String(字符串) -CRUD
举例说明 Redis 的 String 字符串的 CRUD 操作.
set[如果存在就相当于修改,不存在就是添加]/get/del

19.4.3 String(字符串)-使用细节和注意事项
setex(set with expire)键秒值

mset[同时设置一个或多个 key-value 对]
mget[同时获取多个 key-val]

19.4.4 Hash (哈希,类似 golang 里的 Map)-介绍
基本的介绍
Redis hash 是一个键值对集合。var user1 map[string]string
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对
象。
举例,存放一个 User 信息:(user1)
user1 name "smith" age 30 job "golang coder"
说明 :
key : user1
name 张三
和 age 30 就是两对 field-value

19.4.5 Hash(哈希,类似 golang 里的 Map)-CRUD
举例说明 Redis 的 Hash 的 CRUD 的基本操作.
hset/hget/hgetall/hdel
演示添加 user 信息的案例 (name,age )

19.4.6 Hash-使用细节和注意事项
在给 user 设置 name 和 age 时,前面我们是一步一步设置,使用 hmset 和 hmget 可以一次性来设
置多个 filed 的值和返回多个 field 的值 。
hlen 统计一个 hash 有几个元素.
hexists key field
查看哈希表 key 中,给定域 field 是否存在

19.4.7 课堂练习

19.4.8 List(列表)-介绍
列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列
表的头部(左边)或者尾部(右边)。
List 本质是个链表, List 的元素 是有序的,元素的值可以重复.
举例,存放多个地址信息:
city
北京 天津
上海
说明 :
key : city
北京 天津
上海 就是三个元素
入门的案例

19.4.9 List(列表)-CRUD
举例说明 Redis 的 List 的 CRUD 操作。
lpush/rpush/lrange/lpop/rpop/del/
说明:
List 画图帮助学员理解(可以把 l 想象成一根管道.)

herosList 的演示

19.4.10 List-使用细节和注意事项

19.4.11 Set(集合) - 介绍
Redis 的 Set 是 string 类型的无序集合。
底层是 HashTable 数据结构, Set 也是存放很多字符串元素,字符串元素是无序
的,而且元素的值不能重复
举例,存放多个邮件列表信息:
email
sgg@sohu.com tom@sohu.com
说明 :
key : email
tn@sohu.com tom@sohu.com 就是二个元素
redis>sadd email  xx xxx

19.4.12 Set(集合)- CRUD
举例说明 Redis 的 Set 的 CRUD 操作.
sadd
smembers[取出所有值]
sismember[判断值是否是成员]
srem [删除指定值]
演示添加多个电子邮件信息的案例

19.4.13 Set 课堂练习
举例,存放一个商品信息:
包括 商品名、价格、生产日期。
完成对应的 crud 操作
19.5 Golang 操作 Redis
19.5.1 安装第三方开源 Redis 库
1) 使用第三方开源的 redis 库: github.com/garyburd/redigo/redis
2) 在使用 Redis 前,先安装第三方 Redis 库,在 GOPATH 路径下执行安装指令:
D:\goproject>go get github.com/garyburd/redigo/redis
3) 安装成功后,可以看到如下包

特别说明: 在安装 Redis 库前,确保已经安装并配置了 Git, 因为 是从 github 下载安装 Redis 库的,
需要使用到 Git。 如果没有安装配置过 Git,请参考: 如何安装配置 Git
19.5.2 Set/Get 接口
说明: 通过 Golang 添加和获取 key-value
【比如 name-tom~ 】

package main
import (
"fmt"
"github.com/garyburd/redigo/redis" //引入 redis 包
)
func main() {
//通过 go 向 redis 写入数据和读取数据
//1. 链接到 redis
conn, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("redis.Dial err=", err)
return
}
defer conn.Close() //关闭..
//2. 通过 go 向 redis 写入数据 string [key-val]
_, err = conn.Do("Set", "name", "tomjerry 猫猫")
if err != nil {
fmt.Println("set
err=", err)
return
}
//3. 通过 go 向 redis 读取数据 string [key-val]
r, err := redis.String(conn.Do("Get", "name"))
if err != nil {
fmt.Println("set
err=", err)
return
}
//因为返回 r 是 interface{}
//因为 name 对应的值是 string ,因此我们需要转换
//nameString := r.(string)
fmt.Println("操作 ok ", r)
}
View Code

19.5.3 操作 Hash
说明: 通过 Golang 对 Redis 操作 Hash 数据类型
对 hash 数据结构,field-val 是一个一个放入和读取
代码:

package main
import (
"fmt"
"github.com/garyburd/redigo/redis" //引入 redis 包
)
func main() {
//通过 go 向 redis 写入数据和读取数据
//1. 链接到 redis
conn, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("redis.Dial err=", err)
return
}
defer conn.Close() //关闭..
//2. 通过 go 向 redis 写入数据 string [key-val]
_, err = conn.Do("HSet", "user01", "name", "john")
if err != nil {
fmt.Println("hset
err=", err)
return
}
_, err = conn.Do("HSet", "user01", "age", 18)
if err != nil {
fmt.Println("hset
err=", err)
return
}
//3. 通过 go 向 redis 读取数据
r1, err := redis.String(conn.Do("HGet","user01", "name"))
if err != nil {
fmt.Println("hget
err=", err)
return
}
r2, err := redis.Int(conn.Do("HGet","user01", "age"))
if err != nil {
fmt.Println("hget
err=", err)
return
}
//因为返回 r 是 interface{}
//因为 name 对应的值是 string ,因此我们需要转换
//nameString := r.(string)
fmt.Printf("操作 ok r1=%v r2=%v \n", r1, r2)
}
View Code

对 hash 数据结构,field-val 是批量放入和读取

package main
import (
"fmt"
"github.com/garyburd/redigo/redis" //引入 redis 包
)
func main() {
//通过 go 向 redis 写入数据和读取数据
//1. 链接到 redis
conn, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("redis.Dial err=", err)
return
}
defer conn.Close() //关闭..
//2. 通过 go 向 redis 写入数据 string [key-val]
_, err = conn.Do("HMSet", "user02", "name", "john", "age", 19)
if err != nil {
fmt.Println("HMSet
err=", err)
return
}
//3. 通过 go 向 redis 读取数据
r, err := redis.Strings(conn.Do("HMGet","user02", "name", "age"))
if err != nil {
fmt.Println("hget
err=", err)
return
}
for i, v := range r {
fmt.Printf("r[%d]=%s\n", i, v)
}
}
View Code

19.5.4 批量 Set/Get 数据
说明: 通过 Golang 对 Redis 操作,一次操作可以 Set / Get 多个 key-val 数据
核心代码:

_, err = c.Do("MSet", "name", "尚硅谷", "address", "北京昌平~")
r, err := redis.Strings(c.Do("MGet", "name", "address"))
for _, v := range r {
fmt.Println(v)
}

19.5.5 给数据设置有效时间
说明: 通过 Golang 对 Redis 操作,给 key-value 设置有效时间
核心代码:
//给 name 数据设置有效时间为 10s
_, err = c.Do("expire", "name", 10)
19.5.6 操作 List
说明: 通过 Golang 对 Redis 操作 List 数据类型
核心代码:
_, err = c.Do("lpush", "heroList", "no1:宋江", 30, "no2:卢俊义", 28)
r, err := redis.String(c.Do("rpop", "heroList"))
19.5.7 Redis 链接池
说明: 通过 Golang 对 Redis 操作, 还可以通过 Redis 链接池, 流程如下:
1) 事先初始化一定数量的链接,放入到链接池
2) 当 Go 需要操作 Redis 时,直接从 Redis 链接池取出链接即可。
3) 这样可以节省临时获取 Redis 链接的时间,从而提高效率.
4) 示意图

5) 链接池使用的案例

package main
import (
"fmt"
"github.com/garyburd/redigo/redis"
)
//定义一个全局的 pool
var pool *redis.Pool
//当启动程序时,就初始化连接池
func init() {
pool = &redis.Pool{
MaxIdle: 8, //最大空闲链接数
MaxActive: 0, // 表示和数据库的最大链接数, 0 表示没有限制
IdleTimeout: 100, // 最大空闲时间
Dial: func() (redis.Conn, error) { // 初始化链接的代码, 链接哪个 ip 的 redis
return redis.Dial("tcp", "localhost:6379")
},
}
}
func main() {
//先从 pool 取出一个链接
conn := pool.Get()
defer conn.Close()
_, err := conn.Do("Set", "name", "汤姆猫~~")
if err != nil {
fmt.Println("conn.Do err=", err)
return
}
//取出
r, err := redis.String(conn.Do("Get", "name"))
if err != nil {
fmt.Println("conn.Do err=", err)
return
}
fmt.Println("r=", r)
//如果我们要从 pool 取出链接,一定保证链接池是没有关闭
//pool.Close()
conn2 := pool.Get()
_, err = conn2.Do("Set", "name2", "汤姆猫~~2")
if err != nil {
fmt.Println("conn.Do err~~~~=", err)
return
}
//取出
r2, err := redis.String(conn2.Do("Get", "name2"))
if err != nil {
fmt.Println("conn.Do err=", err)
return
}
fmt.Println("r=", r2)
//fmt.Println("conn2=", conn2)
}
View Code

第 20 章数据结构

20.1 数据结构(算法)的介绍

数据结构的介绍
1) 数据结构是一门研究算法的学科,只从有了编程语言也就有了数据结构.学好数据结构可以编写
出更加漂亮,更加有效率的代码。
2) 要学习好数据结构就要多多考虑如何将生活中遇到的问题,用程序去实现解决.
3) 程序 = 数据结构 + 算法

20.2 数据结构和算法的关系

算法是程序的灵魂,为什么有些网站能够在高并发,和海量吞吐情况下依然坚如磐石,大家可能会
说: 网站使用了服务器群集技术、数据库读写分离和缓存技术(比如 Redis 等),那如果我再深入的问
一句,这些优化技术又是怎样被那些天才的技术高手设计出来的呢?
大家请思考一个问题,是什么让不同的人写出的代码从功能看是一样的,但从效率上却有天壤之别,
拿在公司工作的实际经历来说, 我是做服务器的,环境是 UNIX,功能是要支持上千万人同时在线,
并保证数据传输的稳定, 在服务器上线前,做内测,一切 OK,可上线后,服务器就支撑不住了, 公
司的 CTO 对我的代码进行优化,再次上线,坚如磐石。那一瞬间,我认识到程序是有灵魂的,就是
算法。如果你不想永远都是代码工人,那就花时间来研究下算法吧!
本章着重讲解算法的基石-数据结构。
20.3 看几个实际编程中遇到的问题

 


20.4 稀疏 sparsearray 数组

20.4.1 先看一个实际的需求
编写的五子棋程序中,有存盘退出和续上盘的功能

分析按照原始的方式来的二维数组的问题
因为该二维数组的很多值是默认值 0, 因此记录了很多没有意义的数据
20.4.2 基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
1) 记录数组一共有几行几列,有多少个不同的值
2) 思想:把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
20.4.3 稀疏数组举例说明

20.4.4 应用实例
1) 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)
2) 把稀疏数组存盘,并且可以从新恢复原来的二维数组数
3) 整体思路分析

4) 代码实现

package main
import (
"fmt"
)
type ValNode struct {
row int
col int
val int
}
func main() {
//1. 先创建一个原始数组
var chessMap [11][11]int
chessMap[1][2] = 1 //黑子
chessMap[2][3] = 2 //蓝子
//2. 输出看看原始的数组
for _, v := range chessMap {
for _, v2 := range v {
fmt.Printf("%d\t", v2)
}
fmt.Println()
}
//3. 转成稀疏数组。想-> 算法
// 思路
//(1). 遍历 chessMap, 如果我们发现有一个元素的值不为 0,创建一个 node 结构体
//(2). 将其放入到对应的切片即可
var sparseArr []ValNode
//标准的一个稀疏数组应该还有一个 记录元素的二维数组的规模(行和列,默认值)
//创建一个 ValNode 值结点
valNode := ValNode{
row : 11,
col : 11,
val : 0,
}
sparseArr = append(sparseArr, valNode)
for i, v := range chessMap {
for j, v2 := range v {
if v2 != 0 {
//创建一个 ValNode 值结点
valNode := ValNode{
row : i,
col : j,
val : v2,
}
sparseArr = append(sparseArr, valNode)
}
}
}
//输出稀疏数组
fmt.Println("当前的稀疏数组是:::::")
for i, valNode := range sparseArr {
fmt.Printf("%d: %d %d %d\n", i, valNode.row, valNode.col, valNode.val)
}
//将这个稀疏数组,存盘 d:/chessmap.data
//如何恢复原始的数组
//1. 打开这个 d:/chessmap.data => 恢复原始数组.
//2. 这里使用稀疏数组恢复
// 先创建一个原始数组
var chessMap2 [11][11]int
// 遍历 sparseArr [遍历文件每一行]
for i, valNode := range sparseArr {
if i != 0 { //跳过第一行记录值
chessMap2[valNode.row][valNode.col] = valNode.val
}
}
// 看看 chessMap2 是不是恢复.
fmt.Println("恢复后的原始数据......")
for _, v := range chessMap2 {
for _, v2 := range v {
fmt.Printf("%d\t", v2)
}
fmt.Println()
}
}
View Code

对老师的稀疏数组的改进
1) 将构建的稀疏数组,存盘 chessmap.data
2) 在恢复原始二维数组,要求从文件 chessmap.data 读取。
20.5 队列(queue)
20.5.1 队列的应用场景

20.5.2 队列介绍

队列是一个有序列表,可以用数组或是链表来实现。
遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
示意图:(使用数组模拟队列示意图)

20.5.3 数组模拟队列

先完成一个非环形的队列(数组来实现)

思路分析:

 


代码实现:

package main
import (
"fmt"
"os"
"errors"
)
//使用一个结构体管理队列
type Queue struct {
maxSize int
array [5]int // 数组=>模拟队列
front int // 表示指向队列首
rear int // 表示指向队列的尾部
}
//添加数据到队列
func (this *Queue) AddQueue(val int) (err error) {
//先判断队列是否已满
if this.rear == this.maxSize - 1 { //重要重要的提示; rear 是队列尾部(含最后元素)
return errors.New("queue full")
}
this.rear++ //rear 后移
this.array[this.rear] = val
return
}
//从队列中取出数据
func (this *Queue) GetQueue() (val int, err error) {
//先判断队列是否为空
if this.rear == this.front { //队空
return -1, errors.New("queue empty")
}
this.front++
val = this.array[this.front]
return val ,err
}
//显示队列, 找到队首,然后到遍历到队尾
//
func (this *Queue) ShowQueue() {
fmt.Println("队列当前的情况是:")
//this.front 不包含队首的元素
for i := this.front + 1; i <= this.rear; i++ {
fmt.Printf("array[%d]=%d\t", i, this.array[i])
}
fmt.Println()
}
//编写一个主函数测试,测试
func main() {
//先创建一个队列
queue := &Queue{
maxSize : 5,
front : -1,
rear : -1,
}
var key string
var val int
for {
fmt.Println("1. 输入 add 表示添加数据到队列")
fmt.Println("2. 输入 get 表示从队列获取数据")
fmt.Println("3. 输入 show 表示显示队列")
fmt.Println("4. 输入 exit 表示显示队列")
fmt.Scanln(&key)
switch key {
case "add":
fmt.Println("输入你要入队列数")
fmt.Scanln(&val)
err := queue.AddQueue(val)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("加入队列 ok")
}
case "get":
val, err := queue.GetQueue()
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("从队列中取出了一个数=", val)
}
case "show":
queue.ShowQueue()
case "exit":
os.Exit(0)
}
}
}
View Code

对上面代码的小结和说明:
1) 上面代码实现了基本队列结构,但是没有有效的利用数组空间
2) 请思考,如何使用数组 实现一个环形的队列
20.5.4 数组模拟环形队列

分析思路:

1) 什么时候表示队列满 (tail + 1) % maxSize = hed
2) tail == head 表示空
3) 初始化时, tail = 0 head = 0
4) 怎么统计该队列有多少个元素 (tail + maxSize - head ) % maxSize

代码实现:

package main
import (
"fmt"
"errors"
"os"
)
//使用一个结构体管理环形队列
type CircleQueue struct {
maxSize int // 4
array [5]int // 数组
head
int //指向队列队首 0
tail int
//指向队尾 0
}
//如队列 AddQueue(push)
GetQueue(pop)
//入队列
func (this *CircleQueue) Push(val int) (err error) {
if this.IsFull() {
return errors.New("queue full")
}
//分析出 this.tail 在队列尾部,但是包含最后的元素
this.array[this.tail] = val //把值给尾部
this.tail = (this.tail + 1) % this.maxSize
return
}
//出队列
func (this *CircleQueue) Pop() (val int, err error) {
if this.IsEmpty() {
return 0, errors.New("queue empty")
}
//取出,head 是指向队首,并且含队首元素
val = this.array[this.head]
this.head = (this.head + 1) % this.maxSize
return
}
//显示队列
func (this *CircleQueue) ListQueue() {
fmt.Println("环形队列情况如下:")
//取出当前队列有多少个元素
size := this.Size()
if size == 0 {
fmt.Println("队列为空")
}
//设计一个辅助的变量,指向 head
tempHead := this.head
for i := 0; i < size; i++ {
fmt.Printf("arr[%d]=%d\t", tempHead, this.array[tempHead])
tempHead = (tempHead + 1) % this.maxSize
}
fmt.Println()
}
//判断环形队列为满
func (this *CircleQueue) IsFull() bool {
return (this.tail + 1) % this.maxSize == this.head
}
//判断环形队列是空
func (this *CircleQueue) IsEmpty() bool {
return this.tail == this.head
}
//取出环形队列有多少个元素
func (this *CircleQueue) Size() int {
//这是一个关键的算法.
return (this.tail + this.maxSize - this.head ) % this.maxSize
}
func main() {
//初始化一个环形队列
queue := &CircleQueue{
maxSize : 5,
head : 0,
tail : 0,
}
var key string
var val int
for {
fmt.Println("1. 输入 add 表示添加数据到队列")
fmt.Println("2. 输入 get 表示从队列获取数据")
fmt.Println("3. 输入 show 表示显示队列")
fmt.Println("4. 输入 exit 表示显示队列")
fmt.Scanln(&key)
switch key {
case "add":
fmt.Println("输入你要入队列数")
fmt.Scanln(&val)
err := queue.Push(val)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("加入队列 ok")
}
case "get":
val, err := queue.Pop()
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("从队列中取出了一个数=", val)
}
case "show":
queue.ListQueue()
case "exit":
os.Exit(0)
}
}
}
View Code

20.6 链表

20.6.1 链表介绍
链表是有序的列表,但是它在内存中是存储如下

20.6.2 单链表的介绍

单链表的示意图:

说明:一般来说,为了比较好的对单链表进行增删改查的操作,我们都会给他设置一个头结点, 头
结点的作用主要是用来标识链表头, 本身这个结点不存放数据。
20.6.3 单链表的应用实例
案例的说明:
使用带 head 头的单向链表实现 –水浒英雄排行榜管理
完成对英雄人物的增删改查操作, 注: 删除和修改,查找可以考虑学员独立完成
第一种方法在添加英雄时,直接添加到链表的尾部
代码实现:

package main
import (
"fmt"
)
//定义一个 HeroNode
type HeroNode struct {
no int
name string
nickname string
next
*HeroNode //这个表示指向下一个结点
}
//给链表插入一个结点
//编写第一种插入方法,在单链表的最后加入.[简单]
func InsertHeroNode(head *HeroNode, newHeroNode *HeroNode) {
//思路
//1. 先找到该链表的最后这个结点
//2. 创建一个辅助结点[跑龙套, 帮忙]
temp := head
for {
if temp.next == nil { //表示找到最后
break
}
temp = temp.next // 让 temp 不断的指向下一个结点
}
//3. 将 newHeroNode 加入到链表的最后
temp.next = newHeroNode
}
//给链表插入一个结点
//编写第 2 种插入方法,根据 no 的编号从小到大插入..【实用】
func InsertHeroNode2(head *HeroNode, newHeroNode *HeroNode) {
//思路
//1. 找到适当的结点
//2. 创建一个辅助结点[跑龙套, 帮忙]
temp := head
flag := true
//让插入的结点的 no,和 temp 的下一个结点的 no 比较
for {
if temp.next == nil {//说明到链表的最后
break
} else if temp.next.no >= newHeroNode.no {
//说明 newHeroNode 就应该插入到 temp 后面
break
} else if temp.next.no == newHeroNode.no {
//说明我们额链表中已经有这个 no,就不然插入.
flag = false
break
}
temp = temp.next
}
if !flag {
fmt.Println("对不起,已经存在 no=", newHeroNode.no)
return
} else {
newHeroNode.next = temp.next
temp.next = newHeroNode
}
}
//显示链表的所有结点信息
func ListHeroNode(head *HeroNode) {
//1. 创建一个辅助结点[跑龙套, 帮忙]
temp := head
// 先判断该链表是不是一个空的链表
if temp.next == nil {
fmt.Println("空空如也。。。。")
return
}
//2. 遍历这个链表
for {
fmt.Printf("[%d , %s , %s]==>", temp.next.no,
temp.next.name, temp.next.nickname)
//判断是否链表后
temp = temp.next
if temp.next == nil {
break
}
}
}
func main() {
//1. 先创建一个头结点,
head := &HeroNode{}
//2. 创建一个新的 HeroNode
hero1 := &HeroNode{
no : 1,
name : "宋江",
nickname : "及时雨",
}
hero2 := &HeroNode{
no : 2,
name : "卢俊义",
nickname : "玉麒麟",
}
hero3 := &HeroNode{
no : 3,
name : "林冲",
nickname : "豹子头",
}
hero4 := &HeroNode{
no : 3,
name : "吴用",
nickname : "智多星",
}
//3. 加入
InsertHeroNode2(head, hero3)
InsertHeroNode2(head, hero1)
InsertHeroNode2(head, hero2)
InsertHeroNode2(head, hero4)
// 4. 显示
ListHeroNode(head)
}
View Code

删除结点:

20.6.4 双向链表的应用实例

示意图

代码实现

package main
import (
"fmt"
)
//定义一个 HeroNode
type HeroNode struct {
no int
name string
nickname string
pre *HeroNode //这个表示指向前一个结点
next *HeroNode //这个表示指向下一个结点
}
//给双向链表插入一个结点
//编写第一种插入方法,在单链表的最后加入.[简单]
func InsertHeroNode(head *HeroNode, newHeroNode *HeroNode) {
//思路
//1. 先找到该链表的最后这个结点
//2. 创建一个辅助结点[跑龙套, 帮忙]
temp := head
for {
if temp.next == nil { //表示找到最后
break
}
temp = temp.next // 让 temp 不断的指向下一个结点
}
//3. 将 newHeroNode 加入到链表的最后
temp.next = newHeroNode
newHeroNode.pre = temp
}
//给双向链表插入一个结点
//编写第 2 种插入方法,根据 no 的编号从小到大插入..【实用】
func InsertHeroNode2(head *HeroNode, newHeroNode *HeroNode) {
//思路
//1. 找到适当的结点
//2. 创建一个辅助结点[跑龙套, 帮忙]
temp := head
flag := true
//让插入的结点的 no,和 temp 的下一个结点的 no 比较
for {
if temp.next == nil {//说明到链表的最后
break
} else if temp.next.no >= newHeroNode.no {
//说明 newHeroNode 就应该插入到 temp 后面
break
} else if temp.next.no == newHeroNode.no {
//说明我们额链表中已经有这个 no,就不然插入.
flag = false
break
}
temp = temp.next
}
if !flag {
fmt.Println("对不起,已经存在 no=", newHeroNode.no)
return
} else {
newHeroNode.next = temp.next //ok
newHeroNode.pre = temp//ok
if temp.next != nil {
temp.next.pre = newHeroNode //ok
}
temp.next = newHeroNode //ok
}
}
//删除一个结点[双向链表删除一个结点]
func DelHerNode(head *HeroNode, id int) {
temp := head
flag := false
//找到要删除结点的 no,和 temp 的下一个结点的 no 比较
for {
if temp.next == nil {//说明到链表的最后
break
} else if temp.next.no == id {
//说明我们找到了.
flag = true
break
}
temp = temp.next
}
if flag {//找到, 删除
temp.next = temp.next.next //ok
if temp.next != nil {
temp.next.pre = temp
}
} else {
fmt.Println("sorry, 要删除的 id 不存在")
}
}
//显示链表的所有结点信息
//这里仍然使用单向的链表显示方式
func ListHeroNode(head *HeroNode) {
//1. 创建一个辅助结点[跑龙套, 帮忙]
temp := head
// 先判断该链表是不是一个空的链表
if temp.next == nil {
fmt.Println("空空如也。。。。")
return
}
//2. 遍历这个链表
for {
fmt.Printf("[%d , %s , %s]==>", temp.next.no,
temp.next.name, temp.next.nickname)
//判断是否链表后
temp = temp.next
if temp.next == nil {
break
}
}
}
func ListHeroNode2(head *HeroNode) {
//1. 创建一个辅助结点[跑龙套, 帮忙]
temp := head
// 先判断该链表是不是一个空的链表
if temp.next == nil {
fmt.Println("空空如也。。。。")
return
}
//2. 让 temp 定位到双向链表的最后结点
for {
if temp.next == nil {
break
}
temp = temp.next
}
//2. 遍历这个链表
for {
fmt.Printf("[%d , %s , %s]==>", temp.no,
temp.name, temp.nickname)
//判断是否链表头
temp = temp.pre
if temp.pre == nil {
break
}
}
}
func main() {
//1. 先创建一个头结点,
head := &HeroNode{}
//2. 创建一个新的 HeroNode
hero1 := &HeroNode{
no : 1,
name : "宋江",
nickname : "及时雨",
}
hero2 := &HeroNode{
no : 2,
name : "卢俊义",
nickname : "玉麒麟",
}
hero3 := &HeroNode{
no : 3,
name : "林冲",
nickname : "豹子头",
}
InsertHeroNode(head, hero1)
InsertHeroNode(head, hero2)
InsertHeroNode(head, hero3)
ListHeroNode(head)
fmt.Println("逆序打印")
ListHeroNode2(head)
}
View Code

20.6.5 单向环形链表的应用场景

20.6.6 环形单向链表介绍

20.6.7 环形的单向链表的案例

完成对单向环形链表的添加结点,删除结点和显示.

package main
import (
"fmt"
)
//定义猫的结构体结点
type CatNode struct {
no int //猫猫的编号
name string
next *CatNode
}
func InsertCatNode(head *CatNode, newCatNode *CatNode) {
//判断是不是添加第一只猫
if head.next == nil {
head.no = newCatNode.no
head.name = newCatNode.name
head.next = head //构成一个环形
fmt.Println(newCatNode, "加入到环形的链表")
return
}
//定义一个临时变量,帮忙,找到环形的最后结点
temp := head
for {
if temp.next == head {
break
}
temp = temp.next
}
//加入到链表中
temp.next = newCatNode
newCatNode.next = head
}
//输出这个环形的链表
func ListCircleLink(head *CatNode) {
fmt.Println("环形链表的情况如下:")
temp := head
if temp.next == nil {
fmt.Println("空空如也的环形链表...")
return
}
for {
fmt.Printf("猫的信息为=[id=%d name=%s] ->\n", temp.no, temp.name)
if temp.next == head {
break
}
temp = temp.next
}
}
//删除一只猫
func DelCatNode(head *CatNode, id int) *CatNode {
temp := head
helper := head
//空链表
if temp.next == nil {
fmt.Println("这是一个空的环形链表,不能删除")
return head
}
//如果只有一个结点
if temp.next == head { //只有一个结点
if temp.no == id {
temp.next = nil
}
return head
}
//将 helper 定位到链表最后
for {
if helper.next == head {
break
}
helper = helper.next
}
//如果有两个包含两个以上结点
flag := true
for {
if temp.next == head { //如果到这来,说明我比较到最后一个【最后一个还没比较】
break
}
if temp.no ==id {
if temp == head { //说明删除的是头结点
head = head.next
}
//恭喜找到., 我们也可以在直接删除
helper.next = temp.next
fmt.Printf("猫猫=%d\n", id)
flag = false
break
}
temp = temp.next //移动 【比较】
helper = helper.next //移动 【一旦找到要删除的结点 helper】
}
//这里还有比较一次
if flag { //如果 flag 为真,则我们上面没有删除
if temp.no == id {
helper.next = temp.next
fmt.Printf("猫猫=%d\n", id)
}else {
fmt.Printf("对不起,没有 no=%d\n", id)
}
}
return head
}
func main() {
//这里我们初始化一个环形链表的头结点
head := &CatNode{}
//创建一只猫
cat1 := &CatNode{
no : 1,
name : "tom",
}
cat2 := &CatNode{
no : 2,
name : "tom2",
}
cat3 := &CatNode{
no : 3,
name : "tom3",
}
InsertCatNode(head, cat1)
InsertCatNode(head, cat2)
InsertCatNode(head, cat3)
ListCircleLink(head)
head = DelCatNode(head, 30)
fmt.Println()
fmt.Println()
fmt.Println()
ListCircleLink(head)
}
View Code

作业:
20.6.8 环形单向链表的应用实例
Josephu 问题
Josephu
问题为:设编号为 1,2,... n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1
开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,
直到所有人出列为止,由此产生一个出队编号的序列。
提示
用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后
由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又
从 1 开始计数,直到最后一个结点从链表中删除算法结束。
示意图说明

走代码:

package main
import (
"fmt"
)
//小孩的结构体
type Boy struct {
No int // 编号
Next *Boy // 指向下一个小孩的指针[默认值是 nil]
}
// 编写一个函数,构成单向的环形链表
// num :表示小孩的个数
// *Boy : 返回该环形的链表的第一个小孩的指针
func AddBoy(num int) *Boy {
first := &Boy{} //空结点
curBoy := &Boy{} //空结点
//判断
if num < 1
{
fmt.Println("num 的值不对")
return first
}
//循环的构建这个环形链表
for i := 1; i <= num; i++ {
boy := &Boy{
No : i,
}
//分析构成循环链表,需要一个辅助指针[帮忙的]
//1. 因为第一个小孩比较特殊
if i == 1 { //第一个小孩
first = boy //不要动
curBoy = boy
curBoy.Next = first //
} else {
curBoy.Next = boy
curBoy = boy
curBoy.Next = first //构造环形链表
}
}
return first
}
//显示单向的环形链表[遍历]
func ShowBoy(first *Boy) {
//处理一下如果环形链表为空
if first.Next == nil {
fmt.Println("链表为空,没有小孩...")
return
}
//创建一个指针,帮助遍历.[说明至少有一个小孩]
curBoy := first
for {
fmt.Printf("小孩编号=%d ->", curBoy.No)
//退出的条件?curBoy.Next == first
if curBoy.Next == first {
break
}
//curBoy 移动到下一个
curBoy = curBoy.Next
}
}
/*
设编号为 1,2,... n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)
的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,
数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列
*/
//分析思路
//1. 编写一个函数,PlayGame(first *Boy, startNo int, countNum int)
//2. 最后我们使用一个算法,按照要求,在环形链表中留下最后一个人
func PlayGame(first *Boy, startNo int, countNum int) {
//1. 空的链表我们单独的处理
if first.Next == nil {
fmt.Println("空的链表,没有小孩")
return
}
//留一个,判断 startNO <= 小孩的总数
//2. 需要定义辅助指针,帮助我们删除小孩
tail := first
//3. 让 tail 执行环形链表的最后一个小孩,这个非常的重要
//因为 tail 在删除小孩时需要使用到.
for {
if tail.Next == first { //说明 tail 到了最后的小孩
break
}
tail = tail.Next
}
//4. 让 first 移动到 startNo [后面我们删除小孩,就以 first 为准]
for i := 1; i <= startNo - 1; i++ {
first = first.Next
tail = tail.Next
}
fmt.Println()
//5. 开始数 countNum, 然后就删除 first 指向的小孩
for {
//开始数 countNum-1 次
for i := 1; i <= countNum -1; i++ {
first = first.Next
tail = tail.Next
}
fmt.Printf("小孩编号为%d 出圈 \n", first.No)
//删除 first 执行的小孩
first = first.Next
tail.Next = first
//判断如果 tail == first, 圈子中只有一个小孩.
if tail == first {
break
}
}
fmt.Printf("小孩小孩编号为%d 出圈 \n", first.No)
}
func main() {
first := AddBoy(500)
//显示
ShowBoy(first)
PlayGame(first, 20, 31)
}
View Code

20.7 排序

20.7.1 排序的介绍
排序是将一组数据,依指定的顺序进行排列的过程, 常见的排序:
1) 冒泡排序
2) 选择排序
3) 插入排序
4) 快速排序

20.7.2 冒泡排序

20.7.3 选择排序基本介绍
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,经过和其他元素重整,再依原则交换位置后达到排序的目的。
20.7.4 选择排序思想:
选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从 R[0]~R[n-1]中选取最小值,与 R[0]交换,第二次从 R[1]~R[n-1]中选取最小值,与 R[1]交换,第三次从 R[2]~R[n-1]中选取最小值,与 R[2]交换,...,第 i 次从 R[i-1]~R[n-1]中选取最小值,与 R[i-1]交换,..., 第 n-1 次从R[n-2]~R[n-1]中选取最小值,与 R[n-2]交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序
序列。
20.7.5 选择排序的示意图

20.7.6 代码实现

20.7.7 插入排序法介绍:

插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到
排序的目的。
20.7.8 插入排序法思想:
插入排序(Insertion Sorting)的基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表,
开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个
元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成
为新的有序表。
20.7.9 插入排序的示意图

20.7.10 插入排序法应用实例

 


20.7.11 插入排序的代码实现

20.7.12 快速排序法介绍

快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分
割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这
两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
20.7.13 快速排序法示意图

20.7.14 快速排序法应用实例

20.7.15 快速排序法的代码实现

package main
import (
"fmt"
)
//快速排序
//说明
//1. left 表示 数组左边的下标
//2. right 表示数组右边的下标
//3 array 表示要排序的数组
func QuickSort(left int, right int, array *[9]int) {
l := left
r := right
// pivot 是中轴, 支点
pivot := array[(left + right) / 2]
temp := 0
//for 循环的目标是将比 pivot 小的数放到 左边
// 比 pivot 大的数放到 右边
for ; l < r; {
//
pivot 的左边找到大于等于 pivot 的值
for ; array[l] < pivot; {
l++
}
//
pivot 的右边边找到小于等于 pivot 的值
for ; array[r] > pivot; {
r--
}
// 1 >= r 表明本次分解任务完成, break
if l >= r {
break
}
//交换
temp = array[l]
array[l] = array[r]
array[r] = temp
//优化
if array[l]== pivot
{
r--
}
if array[r]== pivot {
l++
}
}
// 如果
1== r, 再移动下
if l == r {
l++
r--
}
// 向左递归
if left < r {
QuickSort(left, r, array)
}
// 向右递归
if right > l {
QuickSort(l, right, array)
}
}
func main() {
arr := [9]int {-9,78,0,23,-567,70, 123, 90, -23}
fmt.Println("初始", arr)
//调用快速排序
QuickSort(0, len(arr) - 1, &arr)
fmt.Println("main..")
fmt.Println(arr)
}
View Code

20.7.16 三种排序方法的速度的分析
20.8 栈
20.8.1 看一个实际需求

20.8.2 栈的介绍
有些程序员也把栈称为堆栈, 即栈和堆栈是同一个概念
1) 栈的英文为(stack)
2) 栈是一个先入后出(FILO-First In Last Out)的有序列表。
3) 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允
许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
4) 根据堆栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相
反,最后放入的元素最先删除,最先放入的元素最后删除
20.8.3 栈的入栈和出栈的示意图

20.8.4 栈的应用场景
1) 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再
将地址取出,以回到原来的程序中。
2) 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变
量等数据存入堆栈中。
3) 表达式的转换与求值。
4) 二叉树的遍历。
5) 图形的深度优先(depth 一 first)搜索法。
20.8.5 栈的案例

代码实现

20.8.6 栈实现综合计算器

分析了实现的思路

代码实现

package main
import (
"fmt"
"errors"
"strconv"
)
//使用数组来模拟一个栈的使用
type Stack struct {
MaxTop int
// 表示我们栈最大可以存放数个数
Top int // 表示栈顶, 因为栈顶固定,因此我们直接使用 Top
arr [20]int // 数组模拟栈
}
//入栈
func (this *Stack) Push(val int) (err error) {
//先判断栈是否满了
if this.Top == this.MaxTop - 1 {
fmt.Println("stack full")
return errors.New("stack full")
}
this.Top++
//放入数据
this.arr[this.Top] = val
return
}
//出栈
func (this *Stack) Pop() (val int, err error) {
//判断栈是否空
if this.Top == -1 {
fmt.Println("stack empty!")
return 0, errors.New("stack empty")
}
//先取值,再 this.Top--
val = this.arr[this.Top]
this.Top--
return val, nil
}
//遍历栈,注意需要从栈顶开始遍历
func (this *Stack) List() {
//先判断栈是否为空
if this.Top == -1 {
fmt.Println("stack empty")
return
}
fmt.Println("栈的情况如下:")
for i := this.Top; i >= 0; i-- {
fmt.Printf("arr[%d]=%d\n", i, this.arr[i])
}
}
//判断一个字符是不是一个运算符[+, - , * , /]
func (this *Stack) IsOper(val int) bool {
if val == 42 || val == 43 || val == 45 || val == 47 {
return true
} else {
return false
}
}
//运算的方法
func (this *Stack) Cal(num1 int, num2 int, oper int) int{
res := 0
switch oper {
case 42 :
res = num2 * num1
case 43 :
res = num2 + num1
case 45 :
res = num2 - num1
case 47 :
res = num2 / num1
default :
fmt.Println("运算符错误.")
}
return res
}
//编写一个方法,返回某个运算符的优先级[程序员定义]
//[* / => 1 + - => 0]
func (this *Stack) Priority(oper int) int {
res := 0
if oper == 42 || oper == 47 {
res = 1
} else if oper == 43 || oper == 45 {
res = 0
}
return res
}
func main() {
//数栈
numStack := &Stack{
MaxTop : 20,
Top : -1,
}
//符号栈
operStack := &Stack{
MaxTop : 20,
Top : -1,
}
exp := "30+30*6-4-6"
//定义一个 index ,帮助扫描 exp
index := 0
//为了配合运算,我们定义需要的变量
num1 := 0
num2 := 0
oper := 0
result := 0
keepNum := ""
for {
//这里我们需要增加一个逻辑,
//处理多位数的问题
ch := exp[index:index+1] // 字符串.
//ch ==>"+" ===> 43
temp := int([]byte(ch)[0]) // 就是字符对应的 ASCiI 码
if operStack.IsOper(temp) { // 说明是符号
//如果 operStack
是一个空栈, 直接入栈
if operStack.Top == -1 { //空栈
operStack.Push(temp)
}else {
//如果发现 opertStack 栈顶的运算符的优先级大于等于当前准备入栈的运算符的优先级
//,就从符号栈 pop 出,并从数栈也 pop 两个数,进行运算,运算后的结果再重新入栈
//到数栈, 当前符号再入符号栈
if operStack.Priority(operStack.arr[operStack.Top]) >=
operStack.Priority(temp) {
num1, _ = numStack.Pop()
num2, _ = numStack.Pop()
oper, _ = operStack.Pop()
result = operStack.Cal(num1,num2, oper)
//将计算结果重新入数栈
numStack.Push(result)
//当前的符号压入符号栈
operStack.Push(temp)
}else {
operStack.Push(temp)
}
}
} else { //说明是数
//处理多位数的思路
//1.定义一个变量 keepNum string, 做拼接
keepNum += ch
//2.每次要向 index 的后面字符测试一下,看看是不是运算符,然后处理
//如果已经到表达最后,直接将 keepNum
if index == len(exp) - 1 {
val, _ := strconv.ParseInt(keepNum, 10, 64)
numStack.Push(int(val))
} else {
//向 index 后面测试看看是不是运算符 [index]
if operStack.IsOper(int([]byte(exp[index+1:index+2])[0])) {
val, _ := strconv.ParseInt(keepNum, 10, 64)
numStack.Push(int(val))
keepNum = ""
}
}
}
//继续扫描
//先判断 index 是否已经扫描到计算表达式的最后
if index + 1 == len(exp) {
break
}
index++
}
//如果扫描表达式 完毕,依次从符号栈取出符号,然后从数栈取出两个数,
//运算后的结果,入数栈,直到符号栈为空
for {
if operStack.Top == -1 {
break //退出条件
}
num1, _ = numStack.Pop()
num2, _ = numStack.Pop()
oper, _ = operStack.Pop()
result = operStack.Cal(num1,num2, oper)
//将计算结果重新入数栈
numStack.Push(result)
}
//如果我们的算法没有问题,表达式也是正确的,则结果就是 numStack 最后数
res, _ := numStack.Pop()
fmt.Printf("表达式%s = %v", exp, res)
}
View Code

20.9 递归
20.9.1 递归的一个应用场景[迷宫问题]

20.9.2 递归的概念

简单的说: 第归就是函数/方法自己调用自己,每次调用时传入不同的变量.第归有助于编程者解决
复杂的问题,同时可以让代码变得简洁。
20.9.3 递归快速入门
我列举两个小案例,来帮助大家理解递归,递归在讲函数时已经讲过(当时讲的相对比较简单),这
里在给大家回顾一下递归调用机制
1) 打印问题
2) 阶乘问题
3) 快速入门的示意图

20.9.4 递归用于解决什么样的问题

1) 各种数学问题如: 8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
2) 将用栈解决的问题-->第归代码比较简洁
20.9.5 递归需要遵守的重要原则
1) 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
2) 函数的局部变量是独立的,不会相互影响, 如果希望各个函数栈使用同一个数据,使用引用传递
3) 递归必须向退出递归的条件逼近【程序员自己必须分析】,否则就是无限递归,死龟了:)
4) 当一个函数执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁
20.9.6 举一个比较综合的案例,迷宫问题

走代码:

package main
import (
"fmt"
)
//编写一个函数,完成老鼠找路
//myMap *[8][7]int:地图,保证是同一个地图,使用引用
//i,j 表示对地图的哪个点进行测试
func SetWay(myMap *[8][7]int, i int, j int) bool {
//分析出什么情况下,就找到出路
//myMap[6][5] == 2
if myMap[6][5] == 2 {
return true
} else {
//说明要继续找
if myMap[i][j] == 0 { //如果这个点是可以探测
//假设这个点是可以通, 但是需要探测 上下左右
//换一个策略 下右上左
myMap[i][j] = 2
if SetWay(myMap, i + 1, j) { //
return true
} else if SetWay(myMap, i , j + 1) { //
return true
} else if SetWay(myMap, i - 1, j) { //
return true
} else if SetWay(myMap, i , j - 1) { //
return true
} else { // 死路
myMap[i][j] = 3
return false
}
} else { // 说明这个点不能探测,为 1,是强
return false
}
}
}
func main() {
//先创建一个二维数组,模拟迷宫
//规则
//1. 如果元素的值为 1 ,就是墙
//2. 如果元素的值为 0, 是没有走过的点
//3. 如果元素的值为 2, 是一个通路
//4. 如果元素的值为 3, 是走过的点,但是走不通
var myMap [8][7]int
//先把地图的最上和最下设置为 1
for i := 0 ; i < 7 ; i++ {
myMap[0][i] = 1
myMap[7][i] = 1
}
//先把地图的最左和最右设置为 1
for i := 0 ; i < 8 ; i++ {
myMap[i][0] = 1
myMap[i][6] = 1
}
myMap[3][1] = 1
myMap[3][2] = 1
myMap[1][2] = 1
myMap[2][2] = 1
//输出地图
for i := 0; i < 8; i++ {
for j := 0; j < 7; j++ {
fmt.Print(myMap[i][j], " ")
}
fmt.Println()
}
//使用测试
SetWay(&myMap, 1, 1)
fmt.Println("探测完毕....")
//输出地图
for i := 0; i < 8; i++ {
for j := 0; j < 7; j++ {
fmt.Print(myMap[i][j], " ")
}
fmt.Println()
}
}
View Code

课后思考题:
思考: 如何求出最短路径?
20.10哈希表(散列)
20.10.1 实际的需求
google 公司的一个上机题:
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址..),当输入该员工的 id 时,要求查找到该员工的 所有信息.
要求: 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
20.10.2 哈希表的基本介绍
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

20.10.3 使用 hashtable 来实现一个雇员的管理系统[增删改查]
应用实例 google 公司的一个上机题:
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址..),当输入该员工
的 id 时,要求查找到该员工的 所有信息.
要求:
1) 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
2) 添加时,保证按照雇员的 id 从低到高插入
思路分析
1) 使用链表来实现哈希表, 该链表不带表头[即: 链表的第一个结点就存放雇员信息]
2) 思路分析并画出示意图

3) 代码实现[增删改查(显示所有员工,按 id 查询)]

package main
import (
"fmt"
)
//定义 emp
type Emp struct {
Id int
Name string
Next *Emp
}
//方法待定..
//定义 EmpLink
//我们这里的 EmpLink 不带表头,即第一个结点就存放雇员
type EmpLink struct {
Head *Emp
}
//方法待定..
//1. 添加员工的方法, 保证添加时,编号从小到大
func (this *EmpLink) Insert(emp *Emp) {
cur := this.Head // 这是辅助指针
var pre *Emp = nil // 这是一个辅助指针 pre 在 cur 前面
//如果当前的 EmpLink 就是一个空链表
if cur == nil {
this.Head = emp //完成
return
}
//如果不是一个空链表,给 emp 找到对应的位置并插入
//思路是 让 cur 和 emp 比较,然后让 pre 保持在 cur 前面
for {
if cur != nil {
//比较
if cur.Id > emp.Id {
//找到位置
break
}
pre = cur //保证同步
cur = cur.Next
}else {
break
}
}
//退出时,我们看下是否将 emp 添加到链表最后
pre.Next = emp
emp.Next = cur
}
//显示链表的信息
func (this *EmpLink) ShowLink(no int) {
if this.Head == nil {
fmt.Printf("链表%d 为空\n", no)
return
}
//变量当前的链表,并显示数据
cur := this.Head // 辅助的指针
for {
if cur != nil {
fmt.Printf("链表%d 雇员 id=%d 名字=%s ->", no, cur.Id, cur.Name)
cur = cur.Next
} else {
break
}
}
fmt.Println() //换行处理
}
//定义 hashtable ,含有一个链表数组
type HashTable struct {
LinkArr [7]EmpLink
}
//给 HashTable 编写 Insert 雇员的方法.
func (this *HashTable) Insert(emp *Emp) {
//使用散列函数,确定将该雇员添加到哪个链表
linkNo := this.HashFun(emp.Id)
//使用对应的链表添加
this.LinkArr[linkNo].Insert(emp) //
}
//编写方法,显示 hashtable 的所有雇员
func (this *HashTable) ShowAll() {
for i := 0; i < len(this.LinkArr); i++ {
this.LinkArr[i].ShowLink(i)
}
}
//编写一个散列方法
func (this *HashTable) HashFun(id int) int {
return id % 7 //得到一个值,就是对于的链表的下标
}
func main() {
key := ""
id := 0
name := ""
var hashtable HashTable
for {
fmt.Println("===============雇员系统菜单============")
fmt.Println("input 表示添加雇员")
fmt.Println("show 表示显示雇员")
fmt.Println("find表示查找雇员")
fmt.Println("exit 表示退出系统")
fmt.Println("请输入你的选择")
fmt.Scanln(&key)
switch key {
case "input":
fmt.Println("输入雇员 id")
fmt.Scanln(&id)
fmt.Println("输入雇员 name")
fmt.Scanln(&name)
emp := &Emp{
Id : id,
Name : name,
}
hashtable.Insert(emp)
case "show":
hashtable.ShowAll()
case "exit":
default :
fmt.Println("输入错误")
}
}
}
View Code

 

posted @ 2019-05-18 22:14  麦奇  阅读(1097)  评论(0编辑  收藏  举报