Golang语言简介
图标上的动物
这个动物叫做金花鼠,英文名称为gordon
Go语言的由来
目的: 用更少的代码,更短的编译时间,创建运行更快的程序,享受更多的乐趣。
Go 语言起源 2007 年,并于 2009 年正式对外发布。它从 2009 年 9 月 21 日开始作为谷歌公司 20% 兼职项目,即相关员工利用 20% 的空余时间来参与 Go 语言的研发工作。该项目的三位领导者均是著名的 IT 工程师:Robert Griesemer,参与开发 Java HotSpot 虚拟机;Rob Pike,Go 语言项目总负责人,贝尔实验室 Unix 团队成员,参与的项目包括 Plan 9,Inferno 操作系统和 Limbo 编程语言;Ken Thompson,贝尔实验室 Unix 团队成员,C 语言、Unix 和 Plan 9 的创始人之一,与 Rob Pike 共同开发了 UTF-8 字符集规范。自 2008 年 1 月起,Ken Thompson 就开始研发一款以 C 语言为目标结果的编译器来拓展 Go 语言的设计思想。
这是一个由计算机领域 “发明之父” 所组成的黄金团队,他们对系统编程语言,操作系统和并行都有着非常深刻的见解
在 2008 年年中,Go 语言的设计工作接近尾声,一些员工开始以全职工作状态投入到这个项目的编译器和运行实现上。Ian Lance Taylor 也加入到了开发团队中,并于 2008 年 5 月创建了一个 gcc 前端。
Russ Cox 加入开发团队后着手语言和类库方面的开发,也就是 Go 语言的标准包。在 2009 年 10 月 30 日,Rob Pike 以 Google Techtalk 的形式第一次向人们宣告了 Go 语言的存在。
直到 2009 年 11 月 10 日,开发团队将 Go 语言项目以 BSD-style 授权(完全开源)正式公布了 Linux 和 Mac OS X 平台上的版本。Hector Chu 于同年 11 月 22 日公布了 Windows 版本。
作为一个开源项目,Go 语言借助开源社区的有生力量达到快速地发展,并吸引更多的开发者来使用并改善它。自该开源项目发布以来,超过 200 名非谷歌员工的贡献者对 Go 语言核心部分提交了超过 1000 个修改建议。在过去的 18 个月里,又有 150 开发者贡献了新的核心代码。这俨然形成了世界上最大的开源团队,并使该项目跻身 Ohloh 前 2% 的行列。大约在 2011 年 4 月 10 日,谷歌开始抽调员工进入全职开发 Go 语言项目。开源化的语言显然能够让更多的开发者参与其中并加速它的发展速度。Andrew Gerrand 在 2010 年加入到开发团队中成为共同开发者与支持者。
在 Go 语言在 2010 年 1 月 8 日被 Tiobe(闻名于它的编程语言流行程度排名)宣布为 “2009 年年度语言” 后,引起各界很大的反响。目前 Go 语言在这项排名中的最高记录是在 2017 年 1 月创下的第13名,流行程度 2.325%。
- 时间轴:
- 2007 年 9 月 21 日:雏形设计
- 2009 年 11 月 10日:首次公开发布
- 2010 年 1 月 8 日:当选 2009 年年度语言
- 2010 年 5 月:谷歌投入使用
- 2011 年 5 月 5 日:Google App Engine 支持 Go 语言
从 2010 年 5 月起,谷歌开始将 Go 语言投入到后端基础设施的实际开发中,例如开发用于管理后端复杂环境的项目。有句话叫 “吃你自己的狗食”,这也体现了谷歌确实想要投资这门语言,并认为它是有生产价值的。
Go 语言的官方网站是 golang.org,这个站点采用 Python 作为前端,并且使用 Go 语言自带的工具 godoc 运行在 Google App Engine 上来作为 Web 服务器提供文本内容。在官网的首页有一个功能叫做 Go Playground,是一个 Go 代码的简单编辑器的沙盒,它可以在没有安装 Go 语言的情况下在你的浏览器中编译并运行 Go,它提供了一些示例,其中包括国际惯例 “Hello, World!”。
Go语言的特点
从 C 语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型,调用参数传值,指针等 等,也保留了和 C 语言一样的编译执行方式及弱化的指针
举一个案例(体验):1
2
3
4//go 语言的指针的使用特点(体验)
func testPtr(num *int) {
*num = 20
}引入包的概念,用于组织程序结构,Go 语言的一个文件都要归属于一个包,而不能单独存在。
1 | package main //一个go文件需要在一个包里 |
垃圾回收机制,内存自动回收,不需开发人员管理
天然并发 (重要特点)
(1) 从语言层面支持并发,实现简单
(2) goroutine,轻量级线程,可实现大并发处理,高效利用多核。
(3) 基于 CPS 并发模型(Communicating Sequential Processes )实现吸收了管道通信机制,形成 Go 语言特有的管道 channel 通过管道 channel , 可以实现不同的 goroute 之间的相互通信。
函数可以返回多个值。举例: //写一个函数,实现同时返回 和,差
1
2
3
4
5
6//go 函数支持返回多个值
func getSumAndSub(n1 int, n2 int) (int, int ) {
sum := n1 + n2 //go 语句后面不要带分号.
sub := n1 - n2
return sum , sub
}新的创新:比如切片 slice、延时执行 defer
Golang环境配置
Mac 下搭建 Go 开发环境-安装和配置 SDK
安装SDK
安装包名称: go1.9.2.darwin-amd64.tar.gz
链接: https://pan.baidu.com/s/19GZeuyrJl8B7nByqtk5FDQ 提取码: 4cqv
上传位置: Mac 一般放在用户目录下 go_dev/go 下
1 | ~ mkdir -p go_dev/go |

Mac 下配置 Golang 环境变量
使用 root 用户,修改 /etc/profile 增加环境变量的配置
1 | ~ sudo vim /etc/profile |
Mac下如何开发Go程序
准备工作
打开VSCode,创建对应的目录

1 | package main |
生成可执行文件并运行
1 | ~ cd goproject/src/go_code/project01/main |
我们也可以使用-o指定当前生成的二进制文件的名称
1 | ~/goproject/src/go_code/project01/main go build -o hello1 hello.go |
生成linux平台可执行的二进制文件
1 | GOOS=linux GOARCH=amd64 go build -o hello1 hello.go |
直接运行Go文件
1 | ~/goproject/src/go_code/project01/main go run hello.go |
Go开发须知
Go程序开发的注意事项
1) Go 源文件以 “go” 为扩展名。
2) Go应用程序的执行入口是main()函数。这个是和其它编程语言(比如java/c)
3) Go语言严格区分大小写。
4) Go方法由一条条语句构成,每个语句后不需要分号(Go语言会在每行后自动加分号),这也体现出 Golang 的简洁性。
5) Go编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否则报错
6) Go语言定义的变量或者import的包如果没有使用到,代码不能编译通过。
7) 大括号都是成对出现的,缺一不可。
Go 语言的转义字符(escape char)
说明:常用的转义字符有如下:
1) \t : 表示一个制表符,通常使用它可以排版。
1) \n :换行符
3) \ :一个
4) " :一个”
5) \r :一个回车 fmt.Println(“天龙八部雪山飞狐\r 张飞”);
举例:
1 | package main |
输出如下:
注释
Go中的注释分为行注释与块注释
行注释://
块注释:/* 内容 */
代码风格
正确格式:
1 | package main |
错误格式:
1 | package main |
一行字符最长不超过 80 个字符,超过的请使用换行展示,尽量保持格式优雅
官方指南
在线调试地址: https://go.dev/play/
包使用查询: https://pkg.go.dev/
中文版在线标准库文档
地址: https://studygolang.com/pkgdoc
变量
Golang变量使用的三种方式
- 第一种:指定变量类型,声明后若不赋值,使用默认值

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

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

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

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

- 变量在同一个作用域(在一个函数或者在代码块)内不能重名
1 | package main |
- 变量=变量名+值+数据类型,这一点请大家注意,变量的三要素
- Golang 的变量如果没有赋初值,编译器会使用默认值, 比如 int 默认值 0,string 默认值为空串,小数默认为 0
变量的声明/初始化/赋值
声明变量
基本语法: var 变量名 数据类型
1 | //举例子 |
初始化变量
基本语法: var 变量名 数据类型 = 值
1 | //举例子 |
给变量赋值
先声明变量,再给变量赋值
1 | //举例子 |
+号的使用
当左右两边都是数值型时,则做加法运算
当左右两边都是字符串,则做字符串拼接
1 | package main |
数据类型

整数类型
| 类型 | 占用存储空间 | 表数范围 |
|---|---|---|
| int8 | 1字节 | -128-127 |
| int16 | 2字节 | -2^15-2^15-1 |
| int32 | 4字节 | -2^31-2^31-1 |
| int64 | 8字节 | -2^63-2^63-1 |
- 其他整数类型
| 类型 | 有无符号 | 占用存储空间 | 表数范围 | 备注 |
|---|---|---|---|---|
| int | 有 | 32位系统4个字节 64位系统8个字节 |
-2^31 ~ 2^31-1 -2^63 ~ 2^63-1 |
|
| uint | 无 | 32位系统4个字节 64位系统8个字节 |
0 ~ 2^32-1 0 ~ 2^64-1 |
|
| rune | 有 | 与int32一样 | -2^31 ~ 2^31-1 | 等价int32,表示一个Unicode码 |
| byte | 无 | 与uint8一样 | 0 ~ 255 | 当要存储字符时选用byte |
整数型使用细节
- Golang 各整数类型分:有符号和无符号,int uint 的大小和系统有关。
- Golang 的整型默认声明为 int 型
1 | package main |

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

- Golang程序中整型变量在使用时,遵守保小不保大的原则,即:在保证程序正确运行下,尽量使用占用空间小的数据类型。【如:年龄】
- bit: 计算机中的最小存储单位。byte:计算机中基本存储单元。[二进制再详细说] 1byte = 8 bit
浮点数类型
| 类型 | 占用存储空间 | 表数范围 |
|---|---|---|
| 单精度float32 | 4字节 | -3.403E38 ~ 3.403E38 |
| 双精度float64 | 8字节 | -1.798E308 ~ 1.798E308 |
- 关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数位+尾数位
说明:浮点数都是有符号的.
1 | package main |

- 尾数部分可能丢失,造成精度损失。 -123.0000901
1 | package main |

说明: float64 的精度比 float32 的要准确.如果我们要保存一个精度高的数,则应该选用 float64
- 浮点型的存储分为三部分:符号位+指数位+尾数位 在存储过程中,精度会有丢失
浮点型使用细节
- Golang浮点类型有固定的范围和字段长度,不受具体OS(操作系统)的影响
- Golang 的浮点型默认声明为 float64 类型。
- 浮点型常量有两种表示形式
- 十进制数形式:如:5.12 .512 (必须有小数点)
- 科学计数法形式:如:5.1234e2 = 5.12 * 10 的 2 次方 5.12E-2 = 5.12/10 的 2 次方
- 通常情况下,应该使用 float64 ,因为它比 float32 更精确。
字符类型
Golang 中没有专门的字符类型,如果要存储单个字符(字母),一般使用 byte 来保存。
字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。也 就是说对于传统的字符串是由字符组成的,而 Go 的字符串不同,它是由字节组成的。
1 | package main |

对上面代码说明:
1) 如果我们保存的字符在 ASCII 表的,比如[0-1, a-z,A-Z..]直接可以保存到 byte
2) 如果我们保存的字符对应码值大于 255,这时我们可以考虑使用 int 类型保存
3) 如果我们需要安装字符的方式输出,这时我们需要格式化输出,即 fmt.Printf(“%c”, c1).
字符类型使用细节
1) 字符常量是用单引号(‘’)括起来的单个字符。例如:var c1 byte = ‘a’ var c2 int = ‘中’ var c3 byte = ‘9’
2) Go 中允许使用转义字符 ‘\’来将其后的字符转变为特殊字符型常量。例如:var c3 char = ‘\n’ // ‘\n’表示换行符
3) Go 语言的字符使用 UTF-8 编码 ,如果想查询字符对应的 utf8 码值 http://www.mytju.com/classcode/tools/encode_utf8.asp
英文字母-1个字节 汉字-3个字节
4) 在 Go 中,字符的本质是一个整数,直接输出时,是该字符对应的 UTF-8 编码的码值。
5) 可以直接给某个变量赋一个数字,然后按格式化输出时%c,会输出该数字对应的 unicode 字符
6) 字符类型是可以进行运算的,相当于一个整数,因为它都对应有 Unicode 码.
布尔类型
布尔类型也叫 bool 类型,bool 类型数据只允许取值 true 和 false
bool类型占1个字节。
bool 类型适于逻辑运算,一般用于程序流程控制
string类型
字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本
Go语言的字符串的字节使用UTF-8编码标识Unicode文本,这样Golang统一使用UTF-8编码,中文 乱码问题不会再困扰程序员。
字符串一旦赋值了,字符串就不能修改了:在 Go 中字符串是不可变的。
字符串的两种表示形式:
(1) 双引号, 会识别转义字符
(2) 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果字符串拼接方式
1 | str := "hello" + "world" |
- 当一行字符串太长时,需要使用到多行字符串,可以如下处理
1 | package main |
基本数据类型的默认值
| 数据类型 | 默认值 |
|---|---|
| 整型 | 0 |
| 浮点型 | 0 |
| 字符串 | “” |
| 布尔类型 | false |
基本数据类型的相互转换
Golang 和 java / c 不同,Go 在不同类型的变量之间赋值时需要显式转换。也就是说 Golang 中数 据类型不能自动转换。
- 表达式 T(v) 将值 v 转换为类型 T
- T: 就是数据类型,比如 int32,int64,float32 等等
- v: 就是需要转换的变量
注意事项
1) Go中,数据类型的转换可以是从表示范围小–>表示范围大,也可以范围大—>范围小
2) 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化!
3) 在转换中,比如将 int64 转成 int8 【-128—127】 ,编译时不会报错,只是转换的结果是按 溢出处理,和我们希望的结果不一样。 因此在转换时,需要考虑范围.
基本数据类型转换为string
方式一:fmt.Sprintf
1 | package main |

方式二:strconv
1 | // 第二种方式 |

string转换为基本数据类型
strconv.Parsexxx
1 | package main |
注意事项
在将 String 类型转成 基本数据类型时,要确保 String 类型能够转成有效的数据,比如 我们可以 把 “123” , 转成一个整数,但是不能把 “hello” 转成一个整数,如果这样做,Golang 直接将其转成 0 , 其它类型也是一样的道理. float => 0 bool => false
指针
基本介绍
基本数据类型,变量存的就是值,也叫值类型
获取变量的地址,用&,比如: var num int, 获取 num 的地址:&num
分析一下基本数据类型在内存的布局.指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值
比如:var ptr *int = &num
举例说明:指针在内存的布局.
1 | package main |
案例演示
1) 写一个程序,获取一个 int 变量 num 的地址,并显示到终端
2) 将 num 的地址赋给指针 ptr , 并通过 ptr 去修改 num 的值.
1 | package main |

使用细节
1) 值类型,都有对应的指针类型, 形式为 *数据类型,比如 int 的对应的指针就是 *int, float32 对应的指针类型就是 *float32, 依次类推。
2) 值类型包括:基本数据类型 int 系列,float 系列, bool, string 、数组和结构体 struct
值类型和引用类型
1) 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
2) 引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型
使用特点
- 值类型
变量直接存储值,内存通常在栈中分配
- 引用类型
变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由 GC 来回收
- 内存的栈区和堆区示意图
标识符的命名规范
标识符概念
1) Golang 对各种变量、方法、函数等命名时使用的字符序列称为标识符
2) 凡是自己可以起名字的地方都叫标识符
命名规则
1) 由 26 个英文字母大小写,0-9 ,_ 组成
2) 数字不可以开头。var num int //ok var 3num int //error
3) Golang中严格区分大小写。
var num int
var Num int
说明:在 golang 中,num 和 Num 是两个不同的变量
4) 标识符不能包含空格。
5) 下划线”_”本身在 Go 中是一个特殊的标识符,称为空标识符。可以代表任何其它的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用
6) 不能以系统保留关键字作为标识符(一共有 25 个),比如 break,if 等等…
标识符命名注意事项
包名:保持 package 的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和 标准库不要冲突 fmt
变量名、函数名、常量名:采用驼峰法
举例:
var stuName string = “tom” 形式: xxxYyyyyZzzz …
var goodPrice float32 = 1234.5
如果
变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能
在本包中使用 ( 注:可以简单的理解成,首字母大写是公开的,首字母小写是私有的) ,在 golang 没有 public , private 等关键字。
系统保留关键字
| break | default | func | interface | select |
|---|---|---|---|---|
| case | defer | go | map | struct |
| chan | else | goto | package | switch |
| const | fallthrogh | if | range | type |
| continue | for | import | return | var |
系统的预定义标识符
| append | bool | byte | cap | close | complex |
|---|---|---|---|---|---|
| complex64 | complex128 | uint16 | copy | false | float32 |
| float64 | imag | int | int8 | init16 | uint32 |
| int32 | int64 | iota | len | make | new |
| nil | panic | uint64 | println | real | |
| recover | string | true | uint | uint8 | uintprt |
运算符
运算符的基本介绍
运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等
1) 算术运算符
2) 赋值运算符
3) 比较运算符/关系运算符 4) 逻辑运算符
4) 位运算符
5) 其它运算符
算术运算符
算术运算符是对数值类型的变量进行运算的,比如:加减乘除。在 Go 程序中使用的非常多
算术运算符的一览表
| 运算符 | 运算 | 范例 | 结果 |
|---|---|---|---|
| + | 正号 | +3 | 3 |
| - | 负号 | -4 | -4 |
| + | 加 | 5 + 5 | 10 |
| - | 减 | 6 - 4 | 2 |
| * | 乘 | 3 * 4 | 12 |
| / | 除 | 5 / 5 | 1 |
| % | 取模(取余) | 7 % 5 | 2 |
| ++ | 自增 | a=2 a++ | a=3 |
| – | 自减 | a=2 a– | a=1 |
| + | 字符串相加 | “He” + “llo” | “Hello” |
演示 / 的使用特点
1 | package main |
演示 % 的使用特点
1 | //演示 %的使用 |
关系运算符(比较运算符)
基本介绍
关系运算符的结果都是 bool 型,也就是要么是 true,要么是 false
关系表达式 经常用在 if 结构的条件中或循环结构的条件中
关系运算符一览图
| 运算符 | 运算 | 范例 | 结果 |
|---|---|---|---|
| == | 相等于 | 4==3 | false |
| != | 不等于 | 4!=3 | true |
| < | 小于 | 4<3 | false |
| > | 大于 | 4>3 | true |
| <= | 小于等于 | 4<=3 | false |
| >== | 大于等于 | 4>=3 | true |
逻辑运算符
基本介绍
用于连接多个条件(一般来讲就是关系表达式),最终的结果也是一个 bool 值
逻辑运算的说明
- 假定A值为True
- 假定B值为False
| 运算符 | 描述 | 实例 |
|---|---|---|
| && | 逻辑 与运算符,如果两边的操作数都是True,则为True,否则为False |
(A && B)为False |
| || | 逻辑 或运算符,如果两边的操作数有一个是True,则为True,否则为False |
(A || B)为True |
| ! | 逻辑 否运算符,如果条件为True,则逻辑为False,否则为True |
!(A && B)为True |
细节说明
1) && 也叫短路与:如果第一个条件为false,则第二个条件不会判断,最终结果为false
2) || 也叫短路或:如果第一个条件为true, 则第二个条件不会判断,最终结果为true
赋值运算符
赋值运算符就是将某个运算后的值,赋给指定的变量
赋值运算符的分类
| 运算符 | 描述 | 实例 |
|---|---|---|
| = | 简单地赋值运算符,将一个表达式的值赋给一个左值 | C = A+B: 将A + B表达式的结果赋值给C |
| += | 相加后再赋值 | C += A => 等于 C = C+A |
| -= | 相减后再赋值 | C -= A => 等于 C = C-A |
| *= | 相乘后再赋值 | C *= A => 等于 C = C*A |
| /= | 相除后再赋值 | C /= A => 等于 C = C/A |
| %= | 相余后再赋值 | C %= A => 等于C = C%A |
| <<= | 左移后赋值 | C <<= 2 等于 C = C<<2 |
| >>= | 右移后赋值 | C >>= 2 等于 C = C>>2 |
| &= | 按位与后赋值 | C &= 2 等于 C = C & 2 |
| ^= | 按位异或后赋值 | C ^= 2 等于 C = C ^ 2 |
| |= | 按位或后赋值 | C |= 2 等于 C = C | 2 |
赋值运算符的特点
运算顺序从右往左
赋值运算符的左边 只能是变量,右边 可以是变量、表达式、常量值
复合赋值运算符等价于下面的效果
比如:a+=3 等价于 a=a+3
位运算符
| 运算符 | 描述 |
|---|---|
| & | 按位与运算符”&”是双目运算符,其功能是参与运算的两数各对应的二进位相与 运算规则是: 有一个为1,结果为1,否则为0 |
| | | 按位或运算符”|”是双目运算符,其功能是参与运算的两数各对应的二进位相或 运算规则是: 同时为1,结果为1,否则为0 |
| ^ | 按位异或运算符”^”是双目运算符,其功能是参与运算的两数各对应的二进位相异或 运算规则是: 当二进位不同时,结果为1,否则为0 |
| << | 左移运算符”<<”是双目运算符,其功能是把”<<”左边的运算数的各二进位全部左移若干位,高位丢弃,低位补0,左移n位就是乘以2的n次方. |
| >> | 右移运算符”>>”是双目运算符,其功能是把”>>”左边的运算数的各二进位全部右移若干位,右移n位就是除以2的n次方. |
其它运算符说明
| 运算符 | 描述 | 实例 |
|---|---|---|
| & | 返回变量存储地址 | &a;将给出变量的实际地址 |
| * | 指针变量 | *a;是一个指针变量 |
运算符的优先级
运算符的优先级的一览表
从上到下,优先级依次递减
| 分类 | 描述 | 关联性 |
|---|---|---|
| 后缀 | () [] -> . ++ – | 左到右 |
| 单目 | + - ! ~ (type) * & sizeof | 右到左 |
| 乘法 | * / % | 左到右 |
| 加法 | + - | 左到右 |
| 移位 | << >> | 左到右 |
| 关系 | < <= > >= | 左到右 |
| 相等 | == != | 左到右 |
| 按位AND | & | 左到右 |
| 按位XOR | ^ | 左到右 |
| 按位OR | | | 左到右 |
| 逻辑AND | && | 左到右 |
| 逻辑OR | || | 左到右 |
| 赋值运算符 | = += -= *= /= %= >>= <<= &= ^= |= | 右到左 |
| 逗号 | , | 左到右 |
键盘输入语句
介绍
在编程中,需要接收用户输入的数据,就可以使用键盘输入语句来获取。InputDemo.go
步骤
导入
fmt包调用
fmt包的fmt.Scanln()或者fmt.Scanf()
官方文档查询使用方法: https://pkg.go.dev/fmt#Scan
演示案例
要求:可以从控制台接收用户信息,【姓名,年龄,薪水, 是否通过考试 】。
- 使用 fmt.Scanln() 获取
1 | package main |
- 使用 fmt.Scanf() 获取
1 | package main |
进制
对于整数,有四种表示方式:
- 二进制: 0,1 ,满 2 进 1。
在 golang 中,不能直接使用二进制来表示一个整数,它沿用了 c 的特点。 - 十进制: 0-9 ,满 10 进 1。
- 八进制: 0-7 ,满 8 进 1. 以数字 0 开头表示。
- 十六进制: 0-9及A-F,满16进1. 以0x或0X开头表示。此处的 A-F 不区分大小写。
1 | package main |
进制的图示
| 十进制 | 十六进制 | 八进制 | 二进制 |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 10 |
| 3 | 3 | 3 | 11 |
| 4 | 4 | 4 | 100 |
| 5 | 5 | 5 | 101 |
| 6 | 6 | 6 | 110 |
| 7 | 7 | 7 | 111 |
| 8 | 8 | 10 | 1000 |
| 9 | 9 | 11 | 1001 |
| 10 | A | 12 | 1010 |
| 11 | B | 13 | 1011 |
| 12 | C | 14 | 1100 |
| 13 | D | 15 | 1101 |
| 14 | E | 16 | 1110 |
| 15 | F | 17 | 1111 |
| 16 | 10 | 20 | 10000 |
| 17 | 11 | 21 | 10001 |
进制的转换
- 其他进制转十进制
- 二进制转十进制
- 八进制转十进制\
- 十六进制转十进制
- 十进制转其它进制
- 十进制转二进制
- 十进制转八进制
- 十进制转十六进制
- 二进制转其它进制
- 二进制转八进制
- 二进制转十六进制
- 其他进制转二进制
- 八进制转二进制
- 十六进制转二进制
其它进制转十进制
二进制如何转十进制
规则: 从最低位开始(右边开始),将每个位上的数提取出来,乘以2的(位数-1)次方,然后求和
- 案例: 请将二进制 1011 转换成十进制
1 | 1011 = 1*2^(1-1) + 1*2^(2-1) + 0*2^(3-1) + 1*2^(4-1) |
八进制如何转十进制

规则: 从最低位开始,将每个位上的数提取出来,乘以8的(位数-1)次方,然后求和
- 案例: 请将0123转换为十进制
1 | 0123 = 3*8^(1-1) + 2*8^(2-1) + 1*8^(3-1) + 0*8^(4-1) |
十六进制转十进制

规则: 从最低位开始,将每个位上的数提取出来,乘以16的(位数-1)次方,然后求和
- 案例: 请将0x34A转成十进制
1 | 0x34A = 10*16^(1-1) + 4*16^(2-1) + 3*16^(3-1) |
十进制转其它进制
十进制转二进制
规则: 将该数不断除以2,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制
- 案例: 请将56转成二进制
1 | 56/2=28 - 0 |
十进制转八进制
规则: 将该数不断除以8,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制
- 案例: 请将156转成八进制
1 | 156/8=19 - 4 |
十进制转十六进制
规则: 将该数不断除以16,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制
- 案例: 请将356转成十六进制
1 | 356/16=22 - 4 |
二进制转八进制/十六进制
二进制转八进制
规则: 将二进制数每三位一组(从低位开始组合),转成对应的八进制即可
- 案例: 请将二进制 11010101 转成八进制
1 | 11010101 => (11) (010) (101) => 0325 |
二进制转十六进制
规则: 将二进制数每四位一组(从低位开始组合),转成对应的十六进制即可
- 案例: 请将二进制 11010101转成十六进制
1 | 11010101 => (1101) (0101) => 0x135 => 0xD5 |
八进制/十六进制转二进制
八进制转二进制
规则: 将八进制数每1位,转成对应的一个三位数即可
- 案例: 请将0237转成二进制
1 | 0237 => (010) (011) (111) => 10011111 |
十六进制转二进制
规则: 将八进制数每1位,转成对应的一个四位数即可
- 案例: 请将0x237转成二进制
1 | 0x237 => (0010) (0011) (0111) => 1000110111 |
位运算
原码、反码、补码
精简6句话:
- 二进制的最高位是符号位: 0表示正数,1表示负数
- 1 => [0000 0001]
- -1 => [1000 0001]
- 正数的源码/反码/补码都一样
- 负数的反码=原码符号位不变,其他位取反
- 1 => 原码[0000 0001]
- 1 => 反码[0000 0001]
- 1 => 补码[0000 0001]
- -1 => 原码[1000 0001]
- -1 => 反码[1111 1110]
- -1 => 补码[1111 1111]
- 负数的补码=反码+1
- 0的反码/补码都是0
- 计算机运算时以补码方式进行运算
位运算符和移位运算符
- Golang中有3个位运算,它们的运算规则是:
- 按位与&: 两位全为1,结果为 1,否则为 0
- 按位或|: 两位有一个为 1,结果为 1,否则为 0
- 按位异或 ^: 两位一个为 0,一个为 1,结果为 1,否则为 0
注意: 整数的反码补码都一样,所以
补码=原码,负数的时候需要用补码先推回到反码,再用反码推回原码
| 计算值 | 原码 | 反码 | 补码 |
|---|---|---|---|
| 2 | 0000 0010 | 0000 0010 | 0000 0010 |
| 3 | 0000 0011 | 0000 0011 | 0000 0011 |
| 2&3 | - | - | 0000 0010(2) |
| 2|3 | - | - | 0000 0011(3) |
| 2^3 | - | - | 0000 0001(1) |
| -2 | 1000 0010 | 1111 1101 | 1111 1110 |
| 2 | 0000 0010 | 0000 0010 | 0000 0010 |
| -2^2 | 1000 0100(-4) | 1111 1011(补码-1) | 1111 1100 |
Golang中有2个移位运算符: **
**右移运算符 >>: 低位溢出,符号位不变,并用符号位补溢出的高位
左移运算符 <<: 符号位不变,低位补 0案例演示
1
2a := 1 >> 2 // 0000 0001 => 0000 0000 = 0
c := 1 << 2 // 0000 0001 => 0000 0100 = 4
流程控制
这个章节不写案例,没啥好写的,不懂的最好去查一下相关案例,能更加快速的上手
顺序控制
代码中,没有判断,也没有跳转.因此程序按照默认的流程执行,即顺 序控制。
这个不必过多介绍,这个不明白啥意思别往下看了,你学不会了兄弟-_-||
分支控制
单分支
- 基本语法
1 | if 条件 { |
双分支
注意: 下面的
else不可以换行,必须为}esle{
- 基本语法
1 | if 条件 { |
多分支
- 基本语法
1 | if 条件 { |
需要注意的是,if语句的判断条件中,不能只出现赋值语句,例如
if a=5,但是可以写成if a=5;a>4
嵌套分支
在一个分支结构中又完整的嵌套了另一个完整的分支结构,里面的分支的结构称为内层分支外面的分支结构称为外层分支。
1 | if 条件 { |
switch分支
- switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上到下逐一测 试,直到匹配为止。
- 匹配项后面也不需要再加 break
基本语法
1 | switch 表达式 { |
总结:
1) switch的执行的流程是,先执行表达式,得到值,然后和case的表达式进行比较,如果相等,
就匹配到,然后执行对应的 case 的语句块,然后退出 switch 控制。
2) 如果 switch 的表达式的值没有和任何的 case 的表达式匹配成功,则执行 default 的语句块。执行后退出 switch 的控制.
3) golang的case后的表达式可以有多个,使用逗号间隔.
4) golang 中的 case 语句块不需要写 break , 因为默认会有,即在默认情况下,当程序执行完 case 语
句块后,就直接退出该 switch 控制结构。
案例
1 | package main |

也可以写成下面这样
1 | package main |
- switch 穿透-fallthrough: 如果在 case 语句块后增加 fallthrough ,则会继续执行下一个 case,也 叫 switch 穿透
1 | package main |

循环控制
for循环
方式一
- 基本语法
1 | for 循环变量初始化;循环条件;循环变量迭代{ |
- 例子: 打印十遍
Hello World
1 | package main |

方式二
将变量初始化和变量迭代写到其它位置
- 基本语法
1 | for 循环判断条件 { |
- 例子: 打印十遍
Hello World
1 | package main |
方式三: 无限循环
等价 for ; ; {} 是一个无限循环, 通常需要配合 break 语句使用
- 基本语法
1 | for { |
- 例子:输出1-10
1 | package main |

for-range
可以方便的遍历字符串与数组
- 例子(传统方式): 按照索引顺序,输出字符串的单个字符
1 | package main |

- 例子(for-range): 字符串遍历
1 | package main |

对于传统的字符串遍历来说,如果字符串中含有中文,会报错
原因是传统的对字符串的遍历是按照字节来遍历,而一个汉字在 utf8 编码是对应 3 个字节。
如何解决?需要要将 str 转成 []rune 切片
1 | package main |
for-range的方式处理中文不会出问题
while 和 do..while
while
Go 语言没有 while 和 do…while 语法,这一点需要注意一下,如果我们需要使用类似其它语
言(比如 java / c 的 while 和 do…while ),可以通过 for 循环来实现其使用效果。
1 | package main |
就是上面for循环中说的for ; ; {} 是一个无限循环
do..while
1 | package main |
- 练习: 利用
*输出金字塔
1 | package main |
break
这个很好理解,主动退出循环嘛,这里不做过多解释,但是有一个标签的功能需要说一下
- break语句出现在多层嵌套的语句块中时,可以通过
标签指明要终止的是哪一层语句块 - break默认跳出最近的 for 循环
1 | package main |

continue
continue 语句用于结束本次循环,继续执行下一次循环。
continue 语句出现在多层嵌套的循环语句体中时,可以通过
标签指明要跳过的是哪一层循环 , 这个和前面的 break 标签的使用的规则一样.
这里就不演示了
放个截图

goto
1) Go 语言的 goto 语句可以无条件地转移到程序中指定的行。
2) goto语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能。
3) 在 Go 程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序
都产生困难
- 一个案例演示快速说明,麻利滴!
1 | import "fmt" |

return
return 使用在方法或者函数中,表示跳出所在的方法或函数,在讲解函数的时候,会详细的介绍。
说明:
1) 如果 return 是在普通的函数,则表示跳出该函数,即不再执行函数中 return 后面代码,也可以理解成终止函数。
2) 如果 return 是在 main 函数,表示终止 main 函数,也就是说终止程序。
函数
因为本人有一定基础,这里就不做太详细的解释,直接上干货,兄弟们!
基本语法
函数可以有返回值,也可以没有
1 | func 函数名 [形参列表] [返回值列表]{ |
快速入门
1 | package main |

包
在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如 main.go 中,去使用 utils.go 文件中的函数,如何实现? => 包
现在有两个程序员共同开发一个 Go 项目,程序员 xiaoming 希望定义函数 Cal ,程序员 xiaoqiang 也想定义函数也叫 Cal。两个程序员为此还吵了起来,怎么办? => 包
原理
包的本质实际上就是创建不同的文件夹,来存放程序文件。

- 说明: go 的每一个文件都是属于一个包的,也就是说 go 是以包的形式来管理文件和项目目录结构
三大作用
- 区分相同名字的函数、变量等标识符
- 当程序文件很多时,可以很好的管理项目
- 控制函数、变量等访问范围,即作用域
说明
打包基本语法
1 | package 包名 |
引入包的基本语法
1 | import "包的路径" |
快速入门
注意: golang在找包的时候,默认会在
/usr/local/go/src/寻找,所以文件要放在golang的src目录下,我目前main.go的绝对路径是/usr/local/go/src/golang_study/08-hanshu/main,所以下面调用utils包时,只需要从golang_study开始写就可以了
- 目录格式

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

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

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

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

打包生成可执行文件命令如下:
1 | go build -o bin/my.exe golang_study/08-hanshu/main |


函数的调用机制
为了让大家更好的理解函数调用过程, 看两个案例,并画出示意图,这个很重要
- 传入一个数+1
1 | package main |

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

- 在调用一个函数时,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间 和其它的栈的空间区分开来
- 在每个函数对应的栈中,数据空间是独立的,不会混淆
- 当一个函数调用完毕(执行完毕)后,程序会销毁这个函数对应的栈空间
return语句
基本语法与说明
Golang函数支持返回多个值,这一点是其他编程语言没有的
1 | func (形参列表) (返回值类型列表){ |
说明:
- 如果返回多个值时,在接收时,如果希望忽略某个值,则使用
_符号表示占位忽略 - 如果返回值只有一个,
(返回值类型列表)可以不写
案例: 计算两个数的和and差
1 | package main |

函数的递归
一个函数在函数体内又调用了本身,我们称为递归调用
快速通过代码理解一下
1 | package main |
执行结果

???
为什么是 2 2 3 ???
递归说白了就是套娃,优先执行最里边的,然后由里到外依次执行,所以你会看到 2 2 3 这个结果
什么?还不明白?
那我给你解释一下上面的代码
- test(4) – 第一步
- 符合if判断条件
- n– 此时n由4-1变为了3
- test(3)
- n=3
- 符合if判断条件
- test(3) – 第二步
- 符合if判断条件
- n– 此时n由3-1变为了2
- test(2)
- n=2
- 符合if判断条件
- test(2) – 第三步
- 不符合if判断条件
- n=2
由下到上(从里到外)输出n的结果,就是 2 2 3
原来如此
将上面的案例代码改变一下
1 | package main |
请问,输出结果是什么?
经过我掐指一算,n=2
是的,结果只是n=2
总结
函数递归需要遵守的重要原则:
1) 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
2) 函数的局部变量是独立的,不会相互影响
3) 递归必须向退出递归的条件逼近,否则就是无限递归,死龟了:)
4) 当一个函数执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁
递归函数练习
- 给出一个整数n,求斐波那契数列中的第n位是什么
1 | package main |

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

函数的注意事项和细节
1) 函数的形参列表可以是多个,返回值列表也可以是多个。
2) 形参列表和返回值列表的数据类型可以是值类型和引用类型。
3) 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其
它包文件使用,类似 public , 首字母小写,只能被本包文件使用,其它包文件不能使用,类似 privat
4) 函数中的变量是局部的,函数外不生效
5) 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。可以参考函数的调用机制
6) 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。
1 | package main |
结果为
1 | n= 30 |
- Go函数不支持函数重载
- 在 Go 中,
函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
1 | package main |

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

- 为了简化数据类型定义,Go 支持
自定义数据类型
基本语法:
- type 自定义数据类型名 数据类型 // 理解: 相当于一个别名
案例1:
- type myInt int // 这时 myInt 就等价 int 来使用了.
1 | package main |
案例2:
- 自定义函数类型
1 | package main |
- 支持对函数返回值命名
1 | package main |
- 使用 _ 标识符,忽略返回值
1 | func main() { |
- Go支持可变参数
1 | package main |
init函数
每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也 就是说 init 会在 main 函数前被调用。
1 | package main |

init函数的注意事项和细节
- 如果一个文件同时包含
全局变量定义,init 函数和main 函数,则执行的流程为:
- 全局变量定义
- init函数
- main 函数
1 | package main |

从上图可以看到,函数test()先被执行了,也就是说全局变量age首先被执行
- init函数最主要的作用,就是完成一些初始化的工作
- 分析下图的执行顺序

匿名函数
Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考 虑使用匿名函数,匿名函数也可以实现多次调用。
- 案例一
在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。
1 | package main |
- 案例二
将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
1 | package main |
全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
1 | package main |
闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
1 | package main |

我表示很懵逼啊,什么玩意儿???
- 对上面代码的说明和总结
1) AddUpper 是一个函数,返回的数据类型是fun (int) int
2) 闭包的说明: 返回的是一个匿名函数, 但是这个匿名函数引用到函数外的 n ,因此这个匿名函数就和n 形成一个整体,构成闭包
3) 大家可以这样理解: 闭包是类,函数是操作,n 是字段。函数和它使用到 n 构成闭包。
4) 当我们反复的调用f 函数时,因为 n 是初始化一次,因此每调用一次就进行累计。
5) 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。
- 更改一下上面的代码
1 | package main |

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

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

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

当执行两个
defer语句的时候,这两行被放入了defer栈中,所以它并没有参与到下面的计算之中,等函数执行完毕后,才会根据先入后出的原则依次执行,所以我们会看到这样的结果
defer 的最佳实践
defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。
模拟代码:
1 | func test() { |
- 使用defer的好处:
1) 在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是
锁资源), 可以执行 defer file.Close() defer connect.Close()
2) 在 defer 后,可以继续使用创建资源.
3) 当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源.
4) 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。
字符串常用系统函数
统计字符串长度: len()
1 | var str string = "Hello" |
字符串遍历
1 | str := "Hello北京" |
字符串转整数
1 | n, err := strconv.Atoi("hello") |
整数转字符串
1 | str = strconv.Itoa(12345) |
字符串转[]byte
1 | var bytes = []byte("Hello go") |
[]byte转字符串
1 | str = string([]byte{12,34,56}) |
十进制转其它进制
1 | str = strconv.FormatInt(123, 2) |
查找子串是否在指定的字符串中
1 | b := strings.Contains("seafood", "foo") |
统计子串个数
1 | num := strings.Count("ceheese", "e") |
不区分大小写的字符串比较
1 | b = strings.EqualFold("abc", "Abc") |
获取指定字符的索引
1 | // 返回子串在字符串第一次出现的 index 值,如果没有返回-1 |
1 | // 返回子串在字符串最后一次出现的 index,如没有返回-1 |
字符串的替换
1 | // strings.Replace("go go hello", "go", "go 语言", n) |
分隔字符串
1 | // 按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组 |
大小写转换
1 | str = "golang Hello" |
去除两边空格
1 | str = strings.TrimSpace(" a b c d ") |
去除两遍指定字符
1 | str = strings.Trim("! a b c d! ", " !") |
判断字符串开头/结束内容
- 判断字符串是否以指定的字符串开头: strings.HasPrefix(“ftp://192.168.10.1", “ftp”) //true
- 判断字符串是否以指定的字符串结束: strings.HasSuffix(“NLT_abc.jpg”, “abc”) //false
时间和日期相关函数
在编程中,程序员会经常使用到日期相关的函数,比如:统计某段代码执行花费的时间等等。
- 获取当前时间
1 | package main |
结果为
- 获取其他日期信息
1 | package main |
结果为

- 时间格式化输出
Printf 或者 Sprintf
1 | //格式化输出 |
time.Format()
1 | // Format(),里面必须写成 2006-01-02 15:04:05 ! |
- 时间的常量
1 | const ( |
常量的作用:
在程序中可用于获取指定时间单位的时间,比如想得到 100 毫秒
1 | 100 * time.Millisecond |
也可以结合Sleep使用
1 | time.Sleep(time.Millisecond * 100) //等待0.1秒 |
- 时间戳
1 | fmt.Printf("unix时间戳=%v, unixnano时间戳=%v \n", now.Unix(), now.UnixNano()) |
返回结果:
unix时间戳=1659074372, unixnano时间戳=1659074372349450000
内置函数
文档地址: https://studygolang.com/pkgdoc
len: 用来求长度,比如
string、array、slice、map、channelnew: 用来分配内存,主要用来分配值类型,比如
int、float32,struct…返回的是指针
new的使用
1 | package main |
结果为
num1的类型为int, num1的值为100, num1的地址为0xc0000140a0
num2的类型=*int, num2的值=0xc0000140a8, num2的地址=0xc00000e030, num2指针指向的值=100
- make
用来分配内存,主要用来分配引用类型,比如channel、map、slice。
错误处理
Go 中可以抛出一个
panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理
defer+recover 处理错误
1 | package main |

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

因为传入
test02中的配置文件名为config2.ini,触发了if判断,所以panic已经输出并终止程序,所以不会看到下面的内容了
数组与切片
数组
数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型。
快速入门
- 一个养鸡场有 6 只鸡,它们的体重分别是 3kg,5kg,1kg,3.4kg,2kg,50kg 。请问这六只鸡的总体重是
多少?平均体重是多少? 请你编一个程序。使用数组解决!
1 | package main |

数组定义与内存布局
- 数组的定义
var 数组名 [数组大小]数据类型
example:
var a [5]int
- 数组的内存布局(
重要)
1 | package main |

对上图的总结:
1) 数组的地址可以通过数组名来获取 &intArr
2) 数组的第一个元素的地址,就是数组的首地址,后面依次加n个字节
3) 数组的各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8, int32->4…
数组的使用
- 从终端循环输入5个成绩,保存到float64数组并输出
1 | package main |

- 四种初始化数组的方式
1 | package main |

数组的遍历
常规遍历
见前面的数组的使用
for-range结构遍历
这是 Go 语言一种独有的结构,可以用来遍历访问数组的元素。
- 语法结构
for index, value := range array01 {
…
}
- 案例
1 | package main |
数组的注意事项和细节
- 数组是多个
相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化 - var arr []int 这时 arr 就是一个 slice 切片,切片后面专门讲解,不急哈.
- 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。
- 数组创建后,如果没有赋值,有默认值(零值)
- 数值类型数组:默认值为 0
- 字符串数组: 默认值为 “”
- bool 数组: 默认值为 false
- 使用数组的步骤
- 1 声明数组并开辟空间
- 2 给数组各个元素赋值(默认零值)
- 3 使用数组
- 数组的下标是从 0 开始的
- 数组下标必须在指定范围内使用,否则报 panic:数组越界,比如
var arr [5]int则有效下标为0-4 - Go的数组属值类型,在默认情况下是值传递,因此会进行值拷贝。数组间不会相互影响(就是说如果一个函数直接更改数组中的某一个值,只要不直接修改地址的值,原值就不会改变)
1 | func test(arr [3]int) { |
- 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
1 | func test(arr *[3]int) { |
- 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度

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

案例二
- 请求出一个数组的最大值,并得到对应的下标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package main
import "fmt"
func main() {
var intArr = [...]int{1, -1, 9, 90, 11, 33}
maxVal := intArr[0]
maxValIndex := 0
for i := 1; i < len(intArr); i++ {
if maxVal < intArr[i] {
maxVal = intArr[i]
maxValIndex = i
}
}
fmt.Printf("maxVal=%v, maxValIndex=%v \n", maxVal, maxValIndex)
}
案例三
- 请求出一个数组的和和平均值。for-range
1 | package main |
- 案例四
- 要求:随机生成五个数,并将其反转打印
1 | package main |

切片
先看一个需求:我们需要一个数组用于保存学生的成绩,但是学生的个数是不确定的,请问怎么办?解决方案 => 使用切片。
基本介绍
1) 切片的英文是 slice
2) 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
3) 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
4) 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
5) 切片定义的基本语法:var 切片名 []类型
快速入门
1 | package main |

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

总结:
- slice 是一个
引用类型,改变切片中的内容,对应的数组内容也会发生改变,因为指向的是同一个地址 - slice 从底层来说,其实就是一个数据结构(struct 结构体)
1 | type slice struct { |
如果更改切片的内容,对应的数组内容也会发生改变,因为指向的是同一个地址
1 | package main |

切片的使用
方式一
第一种方式:定义一个切片,然后让切片去引用一个已经创建好的数组,比如前面的案例就是这样的。
1 | var intArr [5]int = [...]int{1, 22, 33, 66, 99} //创建数组 |
方式二
通过 make 来创建切片.
基本语法:var 切片名 []type = make([]type, len, [cap])
参数说明:
- type: 就是==数据类型==
- len : 大小
- *cap : *指定切片容量(可选, 如果你分配了 cap,则要求 cap>=len)
1 | package main |

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

方式一与方式二的区别

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

切片的注意事项和细节
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[:]
cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素。
切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用
切片可以继续切片
用 append 内置函数,可以对切片进行动态追加
1 | package main |

- 切片的拷贝操作
切片使用 copy 内置函数完成 拷贝,举例说明
1 | package main |

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

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

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

- string 是
不可变的,也就说不能通过str[0] = 'z'方式来修改字符串 - 如果需要修改字符串,可以先将
string->[]byte/ 或者[]rune-> 修改 -> 重写转成 string
1 | package main |
排序和查找
冒泡排序

1 | package main |

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

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

- 代码实现
1 | package main |

二维数组
快速入门
- 案例

1 | package main |

使用方式
- 方式一: 先声明再赋值
语法: var 数组名 [大小][大小]类型
比如: var arr [2][3]int , 再赋值。
使用演示
二维数组在内存的存在形式(重点)
1 | //详细的解释一下上面的图片 |
从上图可以看出,arr2[0]与arr2[1]两个地址相差了24个字节(底层都是使用16进制,满16进一位,150+24=168)
而且arr2[0]与arr2[0][0]的地址是相同的
- 方式二: 直接初始化
声明:
1 | var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}} |
赋值(有默认值,比如 int 类型的就是 0)
1 | var arr3 [2][3]int = [2][3]int{{1, 2, 3}, {4, 5, 6}} |
说明:
二维数组在声明时也对应有四种写法[和一维数组类似]
1 | var 数组名 大小类型 = 大小类型{{初值..},{初值..}} |
二维数组的遍历
1 | package main |

map
map 是 key-value 数据结构,又称为字段或者关联数组。类似其它编程语言的集合,在编程中是经常使用到。
map的声明
基本语法
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
map 声明的举例
1 | var a map[string]string |
注意: 声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用。
案例演示
1 | package main |
对上面代码的说明:
1) map在使用前一定要make
2) map的key是不能重复,如果重复了,则以最后这个key-value为准
3) map的value是可以相同的.
4) map 的 key-value 是无序
5) make内置函数数目
map的使用
- 方式一
1 | //第一种方式 |
- 方式二
1 | //第二种方式 |
- 方式三
1 | //第三种方式 |
案例
演示一个 key-value 的 value 是 map 的案例
比如:我们要存放 3 个学生信息, 每个学生有 name 和 sex 信息
思路: map[string]map[string]string
1 | package main |
map的增删改查
map的增加和更新
map增加和更新:
map[“key”] = value //如果 key 还没有,就是增加,如果 key 存在就是修改。
1 | package main |

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

细节说明
如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以
遍历一下 key, 逐个删除或者
map = make(...),make 一个新的,让原来的成为==垃圾==,被gc回收
- 方式一: 遍历删除
1 | for k, _ := range caties { |
- 方式二: 重新make暴力删除
1 | cities = make(map[string]string) |
map的查找
1 | package main |

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

遍历一个复杂的结构
1 | package main |

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

map的排序
- golang中没有一个专门的方法针对map的key进行排序
- golang中的map默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样.
- golang中map的排序,是先将key进行排序,然后根据key值遍历输出即可
1 | package main |

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

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

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

- 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。
创建结构体变量和访问结构体字段
- 方式一: 直接声明
1 | var person Person |
- 方式二: { }
1 | p2 := Person{"poker", "26"} |
- 方式三: &
1 | //var person *Person = new (Person) |
- 方式四: &{ }
1 | //var person *Person = &Person{} |
- 说明
- 第
3种和第4种方式返回的是结构体指针。 - 结构体指针访问字段的标准方式应该是:
(*结构体指针).字段名,比如 (*person).Name = “tom” - 但 go 做了一个简化,也支持
结构体指针.字段名, 比如 person.Name = “tom”。更加符合程序员使用的习惯,go 编译器底层对 person.Name 做了转化 (*person).Name。
- 第
struct 类型的内存分配机制
前面提到过,struct结构体的类型为值类型,当我们拷贝struct的一个实例时,会划分出一个新的地址空间存放值,所以两者不会影响
1 | package main |

从上面的结果来看,p1与p2根本毫无关联,谁修改谁都不会变化
那么如何做才能产生关联呢?请看下面的代码!
1 | package main |

可以看到,p1.Name与p2.Name同时发生了变化,因为两者指向了同一个内存地址
结构体的注意事项与细节说明
- 结构体的所有字段在内存中是连续的
1 | package main |

可以看到,上面结果中第一行和第二行的地址是连续的,但是第三行并不一定是连续的,a0和b0在十六进制上差了16个字节
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
1 | package main |

- 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
1 | package main |
- struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
1 | package main |

方法
在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名..),Person 结构体还有一些行为比如:可以说话、跑步…, 通过学习,还可以做算术题。这时就要用方法才能完成。
Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。
小试牛刀
1 | type A struct { |
- 说明
func (a A) test() {}表示A结构体有一方法,方法名为test(a A)体现test方法是和A类型绑定的
案例
1 | package main |

- 说明
1) test方法和Person类型绑定
2) test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
3) func (p Person) test() {}… p 表示哪个 Person 变量调用,这个 p 就是它的副本, 这点和函数传参非 常相似。
4) p 这个名字,有程序员指定,不是固定, 比如修改成 person 也是可以
方法快速入门
- 给 Person 结构体添加 speak 方法,输出 xxx 是一个好人
1 | func (p Person) speak() { |
- 给 Person 结构体添加 jisuan 方法,可以计算从 1+..+1000 的结果, 说明方法体内可以函数一样, 进行各种运算
1 | func (p Person) jisuan() { |
- 给 Person 结构体 jisuan2 方法,该方法可以接收一个数 n,计算从 1+…+n 的结果
1 | func (p Person) jisuan2(n int) { |
- 给 Person 结构体添加 getSum 方法,可以计算两个数的和,并返回结果
1 | func (p Person) getSum(n1 int, n2 int) int { |
main主体调用方法
1 | func main() { |

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

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

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


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

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

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

- 编写一个方法,提供 m 和 n 两个参数,方法中打印一个 m*n 的矩形
1 | package main |
- 定义小小计算器结构体(Calcuator),实现加减乘除四个功能 实现形式
- 实现形式1: 分四个方法完成:
- 实现形式2: 用一个方法搞定
1 | package main |

方法和函数区别
调用方式不一样
函数的调用方式:
函数名(实参列表)方法的调用方式:
变量.方法名(实参列表)
对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
1 | package main |

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

总结:
1) 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
2) 如果是和值类型,比如 (p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则
是地址拷贝。
面向对象编程应用实例
步骤
1) 声明(定义)结构体,确定结构体名
2) 编写结构体的字段
3) 编写结构体的方法
学生案例
1) 编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、 int、float64 类型。
2) 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
3) 在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出
1 | package main |
创建结构体变量时指定字段值
方式一
1 | package main |
方式二
1 | //方式二, 返回结构体的指针类型 |
工厂模式
Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
为什么会有工厂模式
例如现在有一个结构体
1 | package model |
因为这个结构体的名称的首字母为大写,所以我们可以在外部进行调用
1 | package main |
那么如果这个结构体的名称首字母为小写时,其他文件就没有办法进行引用
1 | package model |
所以就出现了”工厂模式”
工厂模式的使用方法
- 案例
model外部模块
1 | package model |
main主函数进行调用
1 | package main |

type student struct {
Name string
Score float64
}如果这一段代码中的
Score改为score的话,外部同样无法进行调用,这里我们应该如何处理呢?
model外部模块
1 | package model |
main主函数
1 | package main |
面向对象编程思想-抽象
我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。
案例

1 | package main |
测试

面向对象编程三大特性-封装
Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,下面我们一一进行详细的讲解 Golang 的三大特性是如何实现的。
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
封装的好处
1) 隐藏实现细节
2) 提可以对数据进行验证,保证安全合理(Age)
如何实现封装
1) 对结构体中的属性进行封装
2) 通过方法,包实现封装
封装的步骤
1) 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
2) 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数。
3) 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
1 | func (var 结构体类型名) SetXxx(参数列表) (返回值列表) { |
- 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
1 | func (var 结构体类型名) GetXxx() { |
快速入门案例
请大家看一个程序(person.go),不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证。设计: model 包(person.go) main 包(main.go 调用 Person 结构体)
model外部模块
1 | package model |
main
1 | package main |

练习案例
1) 创建程序,在 model 包中定义 Account 结构体:在 main 函数中体会 Golang 的封装性。
2) Account结构体要求具有字段:账号(长度在6-10之间)、余额(必须>20)、密码(必须是六
3) 通过 SetXxx 的方法给 Account 的字段赋值。(同学们自己完成
4) 在 main 函数中测试
model外部模块
1 | package model |
main
1 | package main |

面向对象编程三大特性-继承
为什么使用继承
- 通过一个案例来说明
model外部模块
1 | package main |
main
1 | package model |

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

也就是说: 在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
嵌套匿名结构体的基本语法
1 | type Goods struct { |
快速入门案例
model外部函数
1 | package model |
main
1 | package main |

继承的深入讨论
- 结构体可以使用
嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,
都可以使用
1 | package main |
- 匿名结构体字段访问可以简化
1 | func main() { |
对上面代码的总结:
(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找..如果都找不到就报错
- 当
结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
1 | package main |

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

- 如果一个
struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
1 | package main |
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,
直接指定各个匿名结构体字段的值
1 | package main |

- 下面这段代码,会输出什么结果
1 | type Monster struct { |

说明:
1) 如果一个结构体有 int 类型的匿名字段,就不能有第二个。
2) 如果需要有多个 int 的字段,则必须给 int 字段指定名字
面向对象编程三大特性-多重继承
多重继承其实在前面的案例中已经体现过了
如一个
struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
- 案例说明
1 | type A struct { |
接口
按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在 Golang 中多态特性主要是通过接口来体现的。
接口快速入门
1 | package main |

理解接口是什么东西了吗?
我的理解就是,如果一个
结构体想要用其它结构体的函数,就需要声明一个接口,接口中写入跟被使用结构体同名的函数名称,然后在目前结构体的方法中声明接口的一个实例,然后使用该实例来调用被使用结构体的方法,可能理解起来比较抽象interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)
基本语法

小结说明
1) 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
2) Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字
注意事项和细节
接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
1 | package main |
接口中所有的方法都
没有方法体,即都是没有实现的方法。在 Golang 中,一个
自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。一个
自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型只要是
自定义数据类型,就可以实现接口,不仅仅是结构体类型。
1 | package main |
- 一个
自定义类型可以实现多个接口
1 | package main |
Golang
接口中不能有任何变量一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法也全部实现。
1 | package main |
interface类型默认是一个
指针(引用类型),如果没有对interface初始化就使用,那么会输出nil空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口。
接口编程的最佳实践
实现对 Hero 结构体切片的排序: sort.Sort(data Interface)
根据官网提供的sort方法我们可以知道:
1.接口类型已经在Sort包中定义好了
2.调用接口中抽象方法的函数已经在Sort自己的包中定义好了(
sort.Sort(接口实例)这个就是)3.需要我们自己实现 Len() Less() Swap() 三个方法来实现Sort接口中的抽象类
4.我们只需要直接使用 sort.Sort() 传入自己结构体的实例即可


https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/sort/sort.go;l=14
下面是官网 sort.Sort() 中的一个例子,我们可以进行套用

1 | package main |

实现接口 vs 继承
现在有一只猴子,它只会爬树,但是他想学会飞翔和游泳
请使用接口实现这个案例
1 | package main |

说明:
- 继承的价值主要在于:解决代码的复用性和可维护性。
- 接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。
面向对象编程-多态
变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
快速入门
在前面的 Usb 接口案例,Usb usb ,既可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口多态特性
详情参考本文:接口快速入门
接口体现多态的两种形式
- 多态参数
在前面的 Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口多态。
- 多态数组
演示一个案例:给 Usb 数组中,存放 Phone 结构体 和 Camera 结构体变量
1 | package main |
类型断言

基本介绍
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
1 | package main |

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

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

最佳实践二
写一函数,循环判断传入参数的类型:
1 | package main |

项目
家庭收支记账软件项目
项目开发流程说明

项目需求说明
1) 模拟实现基于文本界面的《家庭记账软件》
2) 该软件能够记录家庭的收入、支出,并能够打印收支明细表
项目代码实现-面向过程
目前只实现其过程,等完成这个项目需求之后,我们再将其改为面向对象方式实现
目前只在一个main.go中实现功能
功能一:实现主菜单
1 | package main |

功能二:显示明细和登记收入
- 定义变量来记录余额(balance)、每次收支的金额(money), 每次收支的说明(note)
1 | //key接收用户输入的操作 |
- 收入明细实现
1 | switch key { |
功能三:登记支出的功能
1 | case "3": |

代码改进
- 用户输入
q退出时,给出提示确定要退出吗?(y/n),必须输入正确的y/n,否则循环输入指令,直到输入 y 或者 n
1 | case "q": |
- 当没有任何收支明细时,提示
当前没有收支明细... 来一笔吧!
下面是完整的一段代码
1 | package main |
项目代码实现-面向对象
实现思路:
- 更改上面面向过程的代码来实现
- 创建新的结构体,将各个功能分割为该结构体的方法
- 将各个方法存放到各自单独的文件中,由main主函数进行调用
- 总体代码
自行将不同的部分放入不同的文件中即可(需要自己引入包),也可以都放入main.go中执行
1 | package main |
客户信息关系系统项目
项目需求分析
1) 模拟实现基于文本界面的《客户信息管理软件》。
2) 该软件能够实现对客户对象的插入、修改和删除(用切片实现),并能够打印客户明细表
项目界面设计
- 主菜单界面

- 添加客户界面

- 修改客户界面

- 删除客户界面

- 客户列表界面

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

功能实现
显示主菜单和退出软件功能
- model/customer.go
1 | package model |
- service/customerService.go
1 | package service |
- view/customerView.go
1 | package main |
显示客户列表的功能
- model/customer.go
1 | //返回用户的信息,格式化的字符串 |
- service/customerService.go
1 | //编写一个函数,可以返回 *CustomerService |
- view/customerView.go
1 | //显示所有客户的信息 |

添加客户的功能
- 实现思路

- model/customer.go
增加下面的内容(不手动指定id号,让其自动累加)
1 | //第二种创建Customer实例方法,不带id |
- service/customerService.go
1 | //添加客户端到customer切片 |
- view/customerView.go
1 | //得到用户的输入,信息构建新的客户 |

删除客户的功能
- 实现思路

- model/customer.go(没有变化)
- service/customerService.go
1 | //根据客户的id删除客户的信息 |
view/customerView.go
1 | //得到用户的id后,删除该id对应的客户 |

完善退出确认功能
- view/customerView.go
1 | //退出软件 |

修改客户信息
修改客户信息,需要在
customerService与customerView中进行编写
- service/customerService.go
1 | //根据客户提供的id,对客户的相关个人信息进行修改 |
- view/customerView.go
1 | //修改客户信息 |

至此,客户信息关系系统完成~
文件操作
打开/关闭文件用到的方法
func Open
1 | func Open(name string) (*File, error) |
打开指定文件进行读取。如果成功,则可以使用返回文件上的方法进行读取;关联的文件描述符具有O_RDONLY模式。如果有错误,它的类型将是* pathror。
func (*File) Close
1 | func (f *File) Close() error |
关闭关闭文件,使其无法用于I/O。在支持SetDeadline的文件中,任何等待的I/O操作将被取消,并立即返回ErrClosed错误。如果Close已经被调用,将返回一个错误。
演示案例
1 | package main |
执行结果
1 | file=&{0xc0000ae120} |
读文件操作应用实例
- 读取文件的内容并显示在终端(带缓冲区的方式)
使用 os.Open, file.Close, bufio.NewReader(), reader.ReadString 函数和方法.
1 | package main |
执行结果
1 | 123123 |
- 读取文件的内容并显示在终端(使用 ioutil 一次将整个文件读入到内存中),这种方式
适用于文件不大的情况。
相关方法和函数: ioutil.ReadFile
1 | package main |
执行结果
1 | 123123 |
写文件操作应用实例
func OpenFile
1 | func OpenFile(name string, flag int, perm FileMode) (*File, error) |
OpenFile是广义的open调用;大多数用户会使用Open或Create。它打开指定标志(O_RDONLY等)的命名文件。如果文件不存在,并且传递了O_CREATE标志,则使用perm模式(在umask之前)创建它。如果成功,就可以使用返回File上的方法进行I/O操作。如果有错误,它的类型将是* pathror。
- 第一个参数: 表示文件的路径
- 第二个参数: 选项如下(可组合使用)
1 | const ( |
- 第三个参数: 权限控制(同Linux)
1 | r => 4 |
方式一
- 创建一个新文件,写入内容 5 句 “hello, Gardon”
1 | package main |
- 打开一个
存在的文件,将原来的内容覆盖成新的内容 10 句 “你好,世界!”
1 | package main |
- 打开一个
存在的文件,在原来的内容追加内容 ‘ABC! ENGLISH!’
1 | package main |
- 打开一个
存在的文件,将原来的内容读出显示在终端,并且追加 5 句”hello,北京!”
1 | package main |
方式二
- 编程一个程序,将一个文件的内容,写入到另外一个文件。
- 注:这两个文件已经存在了
说明: 使用 ioutil.ReadFile / ioutil.WriteFile 完成写文件的任务.
1 | package main |
判断文件是否存在
golang判断文件或文件夹是否存在的方法为 os.Stat() 函数返回的错误值进行判断:
- 如果返回的错误为nil, 说明文件或文件夹存在
- 如果返回的错误类型使用 os.IsNotExist() 判断为true, 说明文件或文件夹不存在
- 如果返回的错误为其他类型,则不确定是否存在
1 | func PathExists(path string) (bool, error) { |
文件编程应用实例
拷贝文件
说明: 将一个文件拷贝到另外一个文件
所需函数: func Copy(dst Writer, src Reader) (written int64, err error)
注意: Copy 函数是 io 包提供的
1 | package main |
统计英文、数字、空格和其他字符数量
1 | package main |
命令行参数
os.Args 是一个 string 的切片,用来存储所有的命令行参数
1 | package main |
执行结果

flag 包用来解析命令行参数
说明: 前面的方式是比较原生的方式,对解析参数不是特别的方便,特别是带有指定参数形式的命令行。
比如: cmd>main.exe -f a.txt -p 200 -u root 这样的形式命令行,go 设计者给我们提供了 flag 包,可以方便的解析命令行参数,而且参数顺序可以随意
1 | package main |

json
基本介绍
JSON易于机器解析和生成,并有效的提升网络传输效率,通常程序在网络传输时,会先将数据(结构体/map等)序列化成json字符串,到接收方得到json字符串时,在反序列化恢复成原来的数据类型(结构体/map等)

json数据格式说明
在json语言中,一切都是对象.因此,任何的数据类型都可以通过json来表示,例如字符串,数字,对象,数组,map,结构体等
1 | [{"key1":val1, "key2":val2},{"key1":val1, "key2":val2, "key3":[val3, val4]}] |
json的序列化
json 序列化是指,将有 key-value 结构的数据类型(比如结构体、map、切片)序列化成 json 字符串的操作。
- 应用案例
1 | package main |

- 注意事项
对于结构体的序列化,如果我们希望序列化后的 key 的名字,又我们自己重新制定,那么可以给 struct 指定一个 tag 标签.
1 | //定义一个结构体 |
json的反序列化
json 反序列化是指,将 json 字符串反序列化成对应的数据类型(比如结构体、map、切片)的操作
- 应用案例
1 | package main |

对上面代码的说明
1) 在反序列化一个json字符串时,要确保反序列化后的数据类型和原来序列化前的数据类型一致。
1) 如果 json 字符串是通过程序获取到的,则不需要再对 " 转义处理。
单元测试
在工作中,我们经常会确认一个函数或模块的执行结果是否正确
1 | package main |
传统方式
在 main 函数中,调用 addUpper 函数,看看实际输出的结果是否和预期的结果一致,如果一致,则说明函数正确,否则函数有错误,然后修改错误
1 | package main |

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

- utils/cal.go
1 | package main |
- utils/cal_test.go
1 | package main |

- 运行原理

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

- monster/monster.go
1 | package monster |
- monster/monster_test.go
1 | package monster |

执行后会在指定位置生成monster.ser文件

文件内容如下

goroutine 和 channel
goroutine(协程)
进程与线程
- 进程就是程序在操作系统中的一次执行过程, 是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例, 是程序执行的基本单位
- 一个进程可以任意创建和销毁多个线程,同一个进程中的多个线程可以并发执行
- 一个程序至少有一个进程,一个进程至少有一个线程
程序、进程和线程的关系
1 | 程序 => 进程1 => {线程1, 线程2...} |
并发与并行
- 多线程程序在单核上运行,就是
并发 - 多线程程序在多核上运行,就是
并行

Go协程与Go主线程
Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]。
Go协程的特点
1) 有独立的栈空间
2) 共享程序堆空间
3) 调度由用户控制
4) 协程是轻量级的线程

goroutine快速入门
请编写一个程序,完成如下功能:
1) 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 “hello,world”
2) 在主线程中也每隔一秒输出”hello,golang”, 输出 10 次后,退出程序
3) 要求主线程和 goroutine 同时执行.
4) 画出主线程和协程执行流程图
1 | package main |
main 主线程和 test 协程同时执行

执行流程图

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

- 当前程序有三个M,如果三个M都在一个CPU运行,就是
并发,如果在不同的CPU运行就是并行 - M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有三个,M3协程队列有两个
- 从上图可以看到: Go的协程是
轻量级的线程,是逻辑态的,Go可以容易的起上万个协程 - 其他程序c/java的多线程,往往是内核态的,比较重量级,
千个线程可能耗光CPU
MPG模式-状态二

- 分成两个部分来看
- 原来的情况是 M0 主线程正在执行G0协程,另外有三个协程在队列等待
- 如果G0协程阻塞,比如读取文件或者数据库等
- 这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写
- 这样的MPG调度模式,可以既让G0执行,同时也不会让队列的其他协程一直阻塞,仍然可以并发/并行执行
- 等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒
设置golang运行的CPU数量
为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目
1 | package main |

- go1.8后: 默认让程序运行在多个核上,可以不用设置了
- go1.8前: 还是要设置一下,可以更高效的利用CPU
channel(管道)
- 先看一个需求
需求: 现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中, 最后显示出来, 要求使用 toroutine 完成
*思路:* *1.编写一个函数, 来计算各个数的阶乘,放入到 map 中* *2.我们启动的协程多个,统计的将结果放入到 map 中* *3.map 应该做出一个全局的*
1 | package main |
此时运行会报错

报错的原因是资源竞争,因为我的笔记本电脑只有8个CPU,当200个协程同时进行写操作时,就会出现这个问题

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

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

channel的声明
1 | var intChan chan int (intChan 用于存放 int 数据) |
channel 是
引用类型channel 必须初始化才能写入数据, 即
make 后才能使用
管道是有类型的,intChan 只能写入 整数 int
channel应用案例
1 | package main |

当我们从intChan取出第一个数据时, intChan的长度就 -1
正如我们所料, 当程序执行到num3时,就报错了
channel的注意事项
channel中只能存放指定的数据类型
channle的数据放满后,就不能再放入了
如果从 channel 取出数据后,可以继续放入
在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock
读写channel案例演示
- 创建一个intChan, 最多可以存放3个int, 演示存3个数据到intChan, 然后再取出这三个int
1 | package main |
- 创建一个mapChan,最多可以存放10个map[string]string, 的 key-value,演示写入和读取
1 | package main |

- 创建一个catChan,最多可以存放10个Cat结构体变量,演示写入和读取的方法
1 | package main |

- 创建一个catChan,最多可以存放10个 *Cat 结构体变量,演示写入和读取的方法
1 | package main |

- 创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的方法
1 | package main |

- 观察以下代码,会出现什么问题?
1 | package main |
代码改进
1 | package main |

channel的遍历和关闭
channel的关闭
使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据
1 | package main |
channel的遍历
channel 支持 for–range 的方式进行遍历,请注意两个细节:
在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
- 演示案例
1 | package main |
- 应用实例1

请完成goroutine和channel协同工作的案例,具体要求:
1) 开启一个writeData协程,向管道intChan中读取writeData写入的数据
2) 开启一个readData协程,从管道intChan中读取writeData写入的数据
3) 注意: writeData和readData操作的是同一个管道
4) 主线程需要等待writeData和readData协程都完成工作才能退出管道
1 | package main |

- 应用实例2-阻塞
如果将上面代码中的
readData协程注释掉,不进行读操作,会怎么样?
1 | func main() { |
这样做的话,程序只是向管道内写入数据,而没有进行读取,就会出现阻塞deadlock, 原因是intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在 writeData 的 channel <- i
- 应用实例3
要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有 goroutine 和 channel 的知识后,就可以完成了 [测试数据: 80000]
传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。
使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine 去完成,完成任务时间短。

1 | package main |
执行结果:

channel注意事项
- channel可以声明为只读,或者只写性质
1 | package main |
- channel只读和只写的最佳实践案例
1 | package main |

- 使用 select 可以解决从管道取数据的阻塞问题
1 | package main |

- goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题
如果我们启动了一个协程, 但是这个协程出现了panic, 如果我们没有捕获到这和panic,就会造成整个程序崩溃,这时我们可以在 goroutine 中使用 recover 来捕获 panic, 进行处理,这样即使这个协程发生了问题,但是主线程仍然不受影响,可以继续执行
1 | package main |

反射
反射的基本介绍
1) 反射可以在运行时动态获取变量的各种信息, 比如变量的类型(type),类别(kind)
2) 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
3) 通过反射,可以修改变量的值,可以调用关联的方法。
4) 使用反射,需要 import (“reflect”)
包反射实现运行时反射,允许程序操作具有任意类型的对象。典型的用法是使用静态类型接口{}获取一个值,并通过调用TypeOf提取其动态类型信息,它返回一个type。
对ValueOf的调用将返回一个表示运行时数据的Value。Zero接受一个Type并返回一个Value,表示该类型的0值。

应用场景
常见的反射应用场景有以下两种
- 不知道接口调用哪个
函数,根据传入参数在运行时确定调用的具体接口,这种需要对函数或方法反射,例如以下这种桥接模式,比如我前面提出问题
1 | func bridge(funcPtr interface{}, args ...interface{}) |
第一个参数 funcPtr 以接口的形式传入函数指针,函数参数 args 以可变参数的形式传入,bridge 函数中可以用反射来动态执行 funcPtr 函数
- 对结构体序列化时, 如果结构体有指定Tag,也会使用到反射生成对应的字符串
1 | package main |
反射重要的函数和概念
- reflect.TypeOf(变量名), 获取变量名的类型,返回reflect.Type类型
- reflect.ValueOf(变量名), 获取变量的值,返回reflect.Value类型;reflect.Value 是一个结构体类型,通过 reflect.Value, 可以获取到关于该变量的很多信息
- 变量、interface{} 和 reflect.Value 是可以相互转换的,这点在实际开发中,会经常使用到


反射的快速入门
请编写一个案例,演示对(结构体类型、interface{}、reflect.Value)进行反射的基本操作
1 | package main |

可以参考文章golang反射案例
反射的注意事项
- reflect.Value.Kind,获取变量的类别,返回的是一个常量

- Type 和 Kind 的区别
Type 是类型, Kind 是类别, Type 和 Kind 可能是相同的,也可能是不同的.
比如(相同): var num int = 10, num的Type是int , Kind也是int
比如(不同): var stu Student, stu 的 Type 是 pkg1.Student , Kind 是 struct
1 | package main |
执行结果
1 | n的Type=int64 n的Kind=uint32 |
通过反射可以在让变量在
interface{}和reflect.Value之间相互转换使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如 x 是 int类型,那么就应该使用
reflect.Value(x).Int(), 而不能使用其他的,否则报panic通过反射的来修改变量, 注意当使用
SetXxx方法来设置需要通过对应的指针类型来完成, 这样才能改变传入的变量的值, 同时需要使用到reflect.Value.Elem()
1 | package main |
执行结果
1 | val type=reflect.Value |
- reflect.Value.Elem() 应该如何理解?
- Elem() 用于获取指针指向变量
反射最佳实践
使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值
1 | package main |

个人练习
要求:
- 编写一个 Cal 结构体,有两个字段 Num1和Num2
- Cal的方法 GetSub(name string)
- 使用反射遍历 Cal 结构体所有的字段信息
- 使用反射机制完成对 GetSub 的调用, 输出形式为 “tom完成了减法运行, 8 - 3 = 5”
1 | package main |

TCP编程
Golang 的主要设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端程序必不可少也是至关重要的一部分。
网络编程有两种:
1) TCPsocket编程,是网络编程的主流。之所以叫Tcpsocket编程,是因为底层是基于Tcp/ip协
议的. 比如: QQ 聊天
2) b/s结构的http编程,我们使用浏览器去访问服务器时,使用的就是http协议,而http底层依
旧是用 tcp socket 实现的。 比如: 京东商城 【这属于 go web 开发范畴 】
TCP/IP协议
TCP/IP(Transmission Control Protocol/Internet Protocol)的简写,中文译名为传输控制协议/因特网互联协议,又叫网络通讯协议,这个协议是 Internet 最基本的协议、Internet 国际互联网络的基础,简单地说,就是由网络层的 IP 协议和传输层的 TCP 协议组成的。
OSI与TCP/IP参考模型


端口
- 0号端口是保留端口
- 1-1024是固定端口(程序员不要使用)
- 22 => SSH远程登录协议
- 23 => telnet
- 21 => ftp
- 25 => smtp
- 80 => iis
- 7 => echo
- 1025-65535是动态端口(程序员可以使用)
TCP-SOCKET编程快速入门
服务端的处理流程
- 监听端口 8888
- 接收客户端的 TCP 连接, 建立客户端和服务器端的链接
- 创建 goroutine, 处理该链接的请求(通常客户端会通过链接发送请求包)
客户端的处理流程
- 建立与服务端的链接
- 发送请求数据(终端), 接收服务器端返回的结果数据
- 关闭链接
流程示意图

代码实现
服务端代码
1 | package main |
客户端代码
1 | package main |
海量用户即时通讯系统
开发流程
- 需求分析
- 设计阶段
- 编码实现
- 测试阶段
- 实施
需求分析
数据库选择
项目要保存用户信息和消息数据,因此需要使用到数据库,这里我们选择使用 Redis 数据库

实现功能
客户端登录菜单
- 功能: 显示客户端菜单
- 界面
二级菜单
- 显示在线用户列表
- 发送信息
- 信息列表
- 退出系统
代码实现
client/main.go
1 | package main |
client/login.go
1 | package main |

客户端用户登录
步骤一
思路分析:
先确定消息 Message 的格式和结构
然后根据上图的分析完成代码

server/main.go
1 | package main |
common/message/message.go
1 | package message |
client/main.go
和前面的代码一样,没有修改client/login.go
1 | package main |
步骤二
接下来需要完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes), 判断用户的合法性,并返回相应的 LoginResMes
思路分析:
- 让客户端发送消息本身
- 服务器端接受到消息, 然后反序列化成对应的消息结构体.
- 服务器端根据反序列化成对应的消息, 判断是否登录用户是合法, 返回 LoginResMes
- 客户端解析返回的 LoginResMes,显示对应界面
- 这里我们需要做函数的封装
- 代码实现:
修改client/login.go
1 | package main |
代码解释:
下图为 login.go 中修改/添加的部分

修改 server/main.go
1 | package main |
代码解释:
下图为 server/ming.go 中新增的函数

新增的 readPkg 函数封装了以前函数 proc 中for循环中的内容,目前的内容如下图

代码测试执行结果如下图

步骤三
- 能够完成登录,并提示相应信息
修改 common/message/message.go
1 | package message |
修改server/main.go
1 | package main |
下图为server/main.go代码中在之前基础上增加的内容



在client下新建
utils.go文件
1 | package main |
修改
client/login.go文件
1 | package main |
下图为 client/login.go 中在原来的基础上所增加的内容

执行测试结果:
client端

server端

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

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

现在根据各个文件,完成的任务不同,将 main.go 的代码剥离到对应的文件中即可
修改
server/utils/utils.go
1 | package utils |
- 修改
server/process/userProcess.go
1 | package process |
- 修改
server/main/processor.go
1 | package main |
- 修改
server/main/main.go
1 | package main |
步骤五
修改客户端代码
- 示意图如下:

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

将 server/utils.go 拷贝到 client/utils/utils.go
创建了server/process/userProcess.go
说明: 该文件就是在原来的 login.go 做了一个改进,即封装到 UserProcess 结构体
1 | package process |
上面代码中变化的内容如下图

- 创建了server/process/server.go
1 | package process |
上面代码中增加的内容如下图

- 修改 client/main/main.go
1 | package main |
上面代码中变化的内容如下图

Redis操作
Redis手动添加用户
示意图如下

json数据格式如下
1 | {"type":"LoginMes","data":"{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"poker\"}"} |
具体redis操作如下
1 | 127.0.0.1:6379> hset users 100 "{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"poker\"}" |
代码实现Redis添加用户
- 如输入的用户名密码在 Redis 中存在则登录,否则退出系统,并给出相应的提示信息:
- 用户不存在,你也可以重新注册,再登录
- 输入的密码不正确
- 首先创建对应的文件

Redis
Redis 是一个NoSql数据库,不是传统的关系型数据库
Redis 全程为 REmote Dictionary Server
Redis性能非常高,单机能够达到 15w qps, 通常用来做缓存,也可以持久化.
Redis是完全开源免费的, 高性能的分布式内存数据库,基于内存运行并支持持久化的NoSql数据库
官网:
操作原理图

Redis的安装
我的笔记本为 MAC OS, 这里只演示 Mac 上安装 Redis 的过程
参考链接: MAC安装redis的简单方法
1 | brew install redis@6.2 |

启动redis
1 | brew services start redis |

检查端口号6379
1 | netstat -AaLlnW | grep 6379 |

客户端访问
1 | redis-cli |
Redis的基本使用
使用手册参考:Redis命令参考
说明: Redis 安装好后,默认有 16 个数据库,初始默认使用 0 号库, 编号(index)是 0…15
- 添加 key-val [set]
1 | 127.0.0.1:6379> set key1 hello |
- 查看当前 redis 的 所有 key [keys *]
1 | 127.0.0.1:6379> keys * |
- 获取key对应的值 [getkey]
1 | 127.0.0.1:6379> get key1 |
- 切换 redis 数据库 [select index]
1 | 127.0.0.1:6379> select 1 |
- 如何查看当前数据库的key-val数量 [dbsize]
1 | 127.0.0.1:6379> set key2 imau |
- 清空当前数据库的key-val和清空所有数据库的 key-val [flushdb flushall]
- flushdb => 清空当前index的数据库
- flushall => 清空左右index的数据库
Redis 的 Crud 操作
Redis 的五大数据类型
Redis 的五大数据类型是: String(字符串) 、Hash (哈希)、List(列表)、Set(集合) 和 zset(sorted set:有序集合)
String(字符串)
string 是 redis 最基本的类型,一个 key 对应一个 value。
string 类型是
二进制安全的。除普通的字符串外,也可以存放图片等数据。redis 中字符串 value 最大是
512M
1 | # 定义一个key和value |
- setex(set with expire)键秒值
1 | # setex [key] [n秒] [value] |
mset[同时设置一个或多个 key-value 对]
mget[同时获取多个 key-value]
1 | 127.0.0.1:6379> mset m1 hello m2 world |
Hash (哈希)
类似 golang 里的 Map
Redis hash 是一个键值对集合。var user1 map[string]string
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
1 | 127.0.0.1:6379> hset user1 name poker |
- hmset(同时设定多个)
- hmget(同时查看多个)
1 | 127.0.0.1:6379> hmset user2 name jenny age 25 job "good coder " |
- hexists(判定哈希表中key的field是否存在)
1 | 127.0.0.1:6379> hexists user2 name |
List(列表)
列表是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部或者尾部
List 本质是个链表, List 的元素是有序的,元素的值可以重复
1 | # 从左到右依次传入数据 |
特别说明
- List 数据可以从左或右添加数据
- 如果 List 的数据全部移除,那么对应的键也会删除
Set(集合)
sadd [添加值]
smembers [取出所有值]
sismember [判断值是否是成员]
srem [删除指定值]
1 | 127.0.0.1:6379> sadd emails tom@sohu.com |
Golang 操作 Redis
安装第三方开源 Redis 库
使用第三方开源的 Redis 库: go-redis
安装方式:
1 | 1.切换到当前go文件目录中 |
安装过程碰到了很多莫名其妙的问题,没有来得及记录,有问题及时百度~

注意: 官方源码文档中给出的快速使用示例,代码跟引入的包有些出入,因为go-redis版本原因, 有些地方使用方式不同
注: 以下内容通过官方文档的定义进行的编写
Set/Get-String字符串操作
通过 Golang 添加和获取 key-value
在redis中添加两个键值
1 | redis-cli |
快速入门
1 | package main |

MSet/MGet操作-String字符串操作
MSet/MGet 数据操作数据
通过 Golang 对 Redis 操作,一次操作可以 Set / Get 多个 key-val 数据
MSet的官方定义如下图, 参数为一个接口类型的切片,我们可以传入一个 string 类型的切片或者使用for循环依次遍历map切片,传入一对键值字符串(我感觉后者更方便管理,代码也更加简洁,一会儿看下面的代码就可以了)

MGet的官方定义如下图, 参数为一个string类型的切片,我们可以传入一个 string 类型的切片

MGet方法的返回值 *SliceCmd是一个空的结构体(众所周知,空的结构体更加灵活,可以随意调用定义的各种函数),里面已经定义了多个函数,其中Result返回值, 返回的是一个接口切片和error信息

1 | package main |

HSet/HGet-Hash操作
通过 Golang 对 Redis 操作 Hash 数据类型
1 | package main |

HMSet/HMGet-Hash操作
对 hash 数据结构,field-val 是批量放入和读取
根据官方文档, HMSet传入的是一个map切片,我们需要自己定义然后传参即可

MHGet方法传参时,传入的是一个 []string数组

HMGet方法的返回值 *SliceCmd是一个空的结构体,里面已经定义了多个函数,其中Result返回值, 返回的是一个接口切片和error信息

1 | package main |

列表操作
列表中比较常用的指令有: LPush/RPush/LInsert/RInsert/LRange遍历 等
- LPush/RPush
从下图可以得知,values传入的值是一个 接口切片,可以传入任意类型数据,这里我们直接传入任意数量的字符串


可以使用LRange遍历列表的数据

- LInsert/RInsert
下面图中的定义可能会有人看不懂
- key => 键值
- op => after/before
- pivot => 指定的数据位置
- value => 需要添加的数组

举个例子
1 | rdb.LPush("hobby","篮球") |
上代码
1 | package main |

Set-集合操作
SAdd用来向集合中添加键值

SMembers用来输出集合中的数据

SIsMember 用于判断一个值在集合中是否存在,从下图可以看出,该方法返回一个*BoolCmd

*BoolCmd的Result方法会返回一个bool类型的值

上代码
1 | package main |

Redis链接池
说明: 通过 Golang 对 Redis 操作, 还可以通过 Redis 链接池, 流程如下:
1) 事先初始化一定数量的链接,放入到链接池
2) 当 Go 需要操作 Redis 时,直接从 Redis 链接池取出链接即可。2) 这样可以节省临时获取 Redis 链接的时间,从而提高效率

1 | package main |
