一、计算机介绍
1.1、计算机发展
根据计算机所采用的物理器件的发展,一般把电子计算机的发展分成四个阶段。
-
电子管计算机时代
-
晶体管计算机时代
-
集成电路计算机时代
-
大规模集成电路计算机时代
世界上第一台通用计算机ENIAC
于1946年2月14日在美国宾夕法尼亚大学诞生。发明人是美国人莫克利(JohnW.Mauchly
)和艾克特(J.PresperEckert
)。它是一个庞然大物,用了18000个电子管,占地170平方米,重达30吨,耗电功率约150千瓦,每秒钟可进行5000次运算,这在现在看来微不足道,但在当时却是破天荒的。 ENIAC
以电子管作为元器件,所以又被称为电子管计算机,是计算机的第一代。电子管计算机由于使用的电子管体积很大,耗电量大,易发热,因而工作的时间不能太长。
ENIAC
奠定了电脑的发展基础,开辟了计算机科学技术的新纪元,有人将其称为人类第三次产业革命开始的标志。
从50年代中期到60年代后期,采用的主要器件逐步由电子管改为晶体管,缩小了体积,降低了功耗,提高了速度和可靠性,降低了价格。从60年代中期到70年代前期,计算机采用集成电路作为基本器件,功耗、体积、价格进一步下降,速度和可靠性相应的提高。70年代初,半导体存储器问世,迅速取代了磁芯存储器,并不断向大容量、高速度发展,计算机正式进入大规模集成电路时代。
1.2、计算机硬件组成
其中,CPU包括运算器和控制器,相当于计算机的大脑,是计算机的运算核心和控制核心。
(1) 运算器是用来进行数据运算加工的。运算器的主要部件:
(1)数据缓冲器:分为输入缓冲和输出缓冲,输入缓冲暂时存放外设送过来的数据,输出缓冲暂时存放送往外设的数据.
(2)ALU(算数逻辑单元):是运算器的主要部件,能完成常见的位运算(左移,右移,与,或非等)和算术运算(加减乘除等).
(3)状态字寄存器:存放运算状态(条件码,进位,溢出,结果正负等)和运算控制信息.
(4)通用寄存器:暂时存放或传送数据或指令,保存ALU的运算中间结果.
(2) 控制器是是计算机的指挥中心,负责决定执行程序的顺序,给出执行指令时机器各部件所需要的操作控制命令,用于协调和控制计算机的运行。控制器的主要部件:
(1)程序计数器(Program Counter):简称PC,用来存储从内存提取的下一条指令的地址.当CPU执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为"取指令".与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址,此后经过分析指令,执行指令,完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令,保证程序能够连续地执行下去.
(2)时序发生器:用于发送时序脉冲,CPU依据不同的时序脉冲有节奏地进行工作,类似于CPU的节拍器.
(3)指令编译器:用于翻译指令及控制传输指令包含的数据.
(4)指令寄存器:用于缓存从内存或高速缓存里取出的指令,CPU执行指令时,就可以从指令寄存器中取出相关指令来进行执行.
(5)主存地址寄存器:保存当前CPU正要访问的内存单元的地址,通过总线跟主存通信.
(6)主存数据寄存器:保存当前CPU正要读或写的主存数据,通过总线与主存通信.
(7)通用寄存器:用于暂时存放或传送数据或指令.
储存器可分为内储存器和外储存器两部分:内存属于内储存器,内存是CPU与硬盘之间的桥梁,只负责在CPU与硬盘之间做数据预存加速的功能。断电后即会被清除。输入设备的数据是从设备接口进去到端口缓冲器的,再经主板的输入输出总线(I/O总线)直接到CPU的,不经过内存。
外储存器是指除计算机内存及CPU缓存以外的储存器,此类储存器一般断电后仍然能保存数据。常见的外存储器有硬盘、软盘、光盘、U盘等。
输入设备就是键盘、鼠标、麦克风、扫描仪等等,向电脑输入信息。输入设备则相反,电脑向外部输出信息,如显示器、打印、音像、写入外存等。
1.3、冯-诺伊曼计算机
提到计算机,就不得不提及在计算机的发展史上做出杰出贡献的著名应用数学家冯诺依曼(Von Neumann,是他带领专家提出了一个全新的存储程序的通用电子计算机方案。这个方案规定了新机器由5个部分组成:运算器、逻辑控制装置、存储器、输入和输出。并描述了这5个部分的职能和相互关系。 这个方案与早期的ENIAC
相比,有两个重大改进:一是采用二进制;二是提出了“存储程序”的设计思想,即用记忆数据的同一装置存储执行运算的命令,使程序的执行可以自动地从一条指令进入下一条指令。 这个概念被誉为计算机史.上的一个里程碑。计算机的存储程序和程序控制原理因此称为冯诺依曼原理,即可编程、可存储的计算机。按照上述原理设计制造的计算机称为冯诺依曼机。
总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束, 按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。总线是一种内部结构,它是cpu
、内存、输入、输出设备传递信息的公用通道,主机的各个部件通过总线相连接,外部设备通过相应的接口电路再与总线相连接,从而形成了计算机硬件系统。
如果说主板(Mother Board)是一座城市,那么总线就像是城市里的公共汽车(bus),能按照固定行车路线,传输来回不停运作的比特(bit)。一条线路在同一时间内都仅能负责传输一个比特。因此,必须同时采用多条线路才能传送更多数据,而总线可同时传输的数据数就称为宽度(width),以比特为单位,总线宽度愈大,传输性能就愈佳。
二、编程语言介绍
2.1、什么是编程语言
编程语言是用来控制计算机的一系列指令(Instruction),它有固定的格式和词汇(不同编程语言的格式和词汇不一样)。就像我们中国人之间沟通需要汉语,英国人沟通需要英语一样,人与计算机之间进行沟通需要一门语言作为介质,即编程语言。
编程语言的发展经历了机器语言(指令系统)=>汇编语言=>高级语言(C、java、Go
等)。
- 计算机在设计中规定了一组指令(二级制代码),这组指令的集和就是所谓的机器指令系统,用机器指令形式编写的程序称为机器语言。
- 但由于机器语言的千上万条指令难以记忆,并且维护性和移植性都很差,所以在机器语言的基础上,人们提出了采用字符和十进制数代替二进制代码,于是产生了将机器语言符号化的汇编语言。
- 虽然汇编语言相较于机器语言简单了很多,但是汇编语言是机器指令的符号化,与机器指令存在着直接的对应关系,无论是学习还是开发,难度依然很大。所以更加接近人类语言,也更容易理解和修改的高级语言就应运而生了,高级语言的一条语法往往可以代替几条、几十条甚至几百条汇编语言的指令。因此,高级语言易学易用,通用性强,应用广泛。
2.2、编译型语言与解释性语言
计算机是不能理解高级语言的,更不能直接执行高级语言,它只能直接理解机器语言,所以使用任何高级语言编写的程序若想被计算机运行,都必须将其转换成计算机语言,也就是机器码。而这种转换的方式分为编译和解释两种。由此高级语言也分为编译型语言和解释型语言。
使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。
编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe
格式的文件,以后要再运行时,直接使用编译结果即可,如直接运行exe
文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。
1、一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高;
2、与特定平台相关,一般无法移植到其他平台;
使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。
1.解释型语言每次运行都需要将源代码解释称机器码并执行,执行效率低;
2.只要平台提供相应的解释器,就可以运行源代码,所以可以方便源程序移植;
三、Go语言介绍
3.1、了解Go语言
Go(又称 Golang)是 Google 的 Rob Pike与Robert Griesemer, 及 Ken Thompson 在2007开发的一种静态强类型、编译型语言,并在 2009 年正式对外发布。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美 C / C++代码的速度,而且更加安全、支持并行进程。作为出现在21世纪的语言,其近C的执行性能和近解析型语言的开发效率,以及近乎于完美的编译速度,已经风靡全球!
3.2、Go编译器的下载与安装
3.2.1、下载SDK
1
2
|
-- 官网:https://golang.google.cn/
-- go中文网:https://studygolang.com/dl
|
3.2.2、安装
1
|
执行命令 go version // 查看安装go的版本
|
在默认情况下,Go 将会被安装在目录 c:\go 下,但如果你在安装过程中修改安装目录,则需要手动修改所有的环境变量的值。
3.2.3、配置变量
GOPATH对应创建的文件夹中里面,手动创建如下3个目录
src 存储go的源代码(需要我们自己手动创建)
pkg 存储编译后生成的包文件 (自动生成)
bin存储生成的可执行文件(自动生成)
3.3、第一个Go程序
1
2
3
4
5
6
7
|
package main
import "fmt"
func main() {
fmt.Println("Hello, Yuan!")
}
|
3.3.1、程序语法解析
(1)main包和main函数
Go语言以“包”作为管理单位,每个 Go 源文件必须先声明它所属的包,所以我们会看到每个 Go 源文件的开头都是一个 package 声明。
Go语言的包与文件夹是一一对应的。一个Go语言程序必须有且仅有一个 main 包。main 包是Go语言程序的入口包,如果一个程序没有 main 包,那么编译时将会出错,无法生成可执行文件。
**(2)import **
在包声明之后,是 import 语句,用于导入程序中所依赖的包,导入的包名使用双引号""
包围,格式如下:
其中 import 是导入包的关键字,name 为所导入包的名字。
导入的包中不能含有代码中没有使用到的包,否则Go编译器会报编译错误
也可以使用一个 import 关键字导入多个包,此时需要用括号( )
将包的名字包围起来,并且每个包名占用一行
1
2
3
4
|
import(
"p1"
"p2"
)
|
3.3.2、程序编译执行
我们上面给大家介绍过,Go语言是像C语言一样的编译型的静态语言,所以在运行Go语言程序之前,先要将其编译成二进制的可执行文件。
可以通过Go语言提供的go build
或者go run
命令对Go语言程序进行编译:
(1) go build
命令可以将Go语言程序代码编译成二进制的可执行文件,但是需要我们手动运行该二进制文件;
1、如果是普通包,当你执行go build之后,它不会产生任何文件。【非main包】
2、如果是main包,当你执行go build之后,它就会在当前目录下生成一个可执行文件exe
3、你也可以指定编译输出的文件名。我们可以指定go build -o xxxx.exe
,默认情况是你的package名(main包),或者是第一个源文件的文件名(main包)
(2)除了使用go build
命令外,Go语言还为我们提供了go run
命令,go run
命令将编译和执行指令合二为一,会在编译之后立即执行Go语言程序,但是不会生成可执行文件。
1
|
go run fileName // fileName 参数必须是同一 main 包下的所有源文件名,并且不能为空。
|
3.4、IDE的安装与使用
3.4.1、安装Goland
下载地址:https://www.jetbrains.com/go/download/#section=windows
3.4.2、IDE的快捷键
快捷键 | 作用 |
Ctrl + / |
单行注释 |
Ctrl + Shift + / |
多行注释 |
Ctrl + D |
复制当前光标所在行 |
Ctrl + X |
删除当前光标所在行 |
Ctrl + Alt + L |
格式化代码 |
Ctrl + Shift + F |
全局查找 |
Ctrl + Alt + left/right |
返回至上次浏览的位置 |
Ctrl + W |
快速选中代码 |
Ctrl + R |
替换 |
四、基础语法
4.1、变量
变量概念源于数学,在计算机中表示用来存储值或者存储计算结果的抽象概念。
变量本质上是一种对内存地址的引用,让你能够把程序中准备使用的每一段数据都赋给一个简短、易于记忆的名字进行操作。
4.1.1、声明变量
和C语言一样,Go语言也是通过var关键字进行声明,不同的是变量名放在类型前,具体格式如下
1
2
3
4
5
6
7
|
var x int
var s string
var b bool
fmt.Println(x) // 0
fmt.Println(s) // ""
fmt.Println(b) // false
|
声明未赋值,系统默认赋该类型零值
如果声明多个变量,可以进行简写
1
2
3
4
5
6
7
8
|
// 声明多个相同类型变量
var x,y int
// 声明多个不同类型变量
var (
x int
s string
b bool
)
|
4.1.2、变量赋值
变量赋值有两种方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 先声明再赋值
var x int
x = 10 // 不要 重复声明 : var x = 10
fmt.Println(x)
// 直接声明赋值
// var y string= "hello yuan!"
var y = "hello yuan!"
fmt.Println(y)
// 声明赋值精简版
s := "hi,yuan!" // 1、编译器会自动根据右值类型推断出左值的对应类型,等同于var s = "hi,yuan!"。2、该变量之前不能声明,否则重复声明
fmt.Println(s)
// 多重赋值
var v1,v2 int
v1,v2 = 1,2
|
4.1.3、匿名变量
匿名变量即没有命名的变量,在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_
表示。
1
2
3
4
5
6
|
a,b,c :=4,5,6
fmt.Println(a,b,c)
// 如果只想接受第个变量,可以对前两个变量匿名
_,_,x := 4,5,6
fmt.Println(x)
|
匿名变量不占用命名空间,不会分配内存
让代码非常清晰,基本上屏蔽掉了可能混淆代码阅读者视线的内容,从而大幅降低沟通的复杂度和代码维护的难度。
4.1.4、变量命名规则
变量命名是需要遵循一定的语法规范的,否则编译器不会通过。
1、变量名称必须由数字、字母、下划线组成。
2、标识符开头不能是数字。
3、标识符不能是保留字和关键字。
4、建议使用驼峰式命名,当名字有几个单词组成的时优先使用大小写分隔
5、变量名尽量做到见名知意。
6、变量命名区分大小写
go语言中有25个关键字,不能用于自定义变量名
1
2
3
4
5
|
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
|
还有30多个预定义的名字,用于内建的常量、类型和函数
1
2
3
4
5
6
7
8
9
10
11
|
内建常量:
true false iota nil
内建类型:
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
内建函数:
make len cap new append copy close delete
complex real imag
panic recover
|
4.2、语句分隔符
就像我们写作文一样,一定要有像逗号或者句号这样的语句分隔符,否则无法断句根本不能理解,编程语言也一样,需要给解释器或者编译器一个语句分割,让它知道哪里到哪里是一个语句,才能再去解析语句。
在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分。这一点是借鉴的python语法特性。
1
2
3
4
5
6
7
8
9
|
//var name = "yuan";var age = 18 // 不推荐
//fmt.Println(name)
//fmt.Println(age) // 不报错但是不推荐
// 推荐写法
var name = "yuan" // 换行即分隔符
var age = 18
fmt.Println(name)
fmt.Println(age)
|
4.3、注释
注释就是对代码的解释和说明,其目的是让人们能够更加轻松地了解代码。注释是开发人员一个非常重要的习惯,也是专业的一种表现。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾。
注释只是为了提高可读性,不会被计算机编译。
1
2
3
4
5
|
// 单行注释
var name = "yuan" // 声明了一个name变量
/*
多行注释
*/
|
4.4、基本数据类型
基本数据类型包含整形和浮点型,布尔类型以及字符串,这几种数据类型在几乎所有编程语言中都支持。
4.4.1、整形
具体类型 | 取值范围 |
int8 |
-128到127 |
uint8 |
0到255 |
int16 |
-32768到32767 |
uint16 |
0到65535 |
int32 |
-2147483648到2147483647 |
uint32 |
0到4294967295 |
int64 |
-9223372036854775808到9223372036854775807 |
uint64 |
0到18446744073709551615 |
uint |
与平台相关,32位操作系统上就是uint32 ,64位操作系统上就是uint64 |
int |
与平台相关,32位操作系统上就是int32 ,64位操作系统上就是int64 |
1
2
3
|
var x int
x = 9223372036854775809
fmt.Print(x) // overflows int
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 十进制转化
var a int = 10
fmt.Printf("%d \n", a) // 10 占位符%d表示十进制
fmt.Printf("%b \n", a) // 1010 占位符%b表示二进制
fmt.Printf("%o \n", a) // 12 占位符%o表示八进制
fmt.Printf("%x \n", a) // a 占位符%x表示十六进制
// 八进制转化
var b int = 020
fmt.Printf("%o \n", b) // 20
fmt.Printf("%d \n", b) // 16
fmt.Printf("%x \n", b) // 10
fmt.Printf("%b \n", b) // 10000
// 十六进制转化
var c = 0x12
fmt.Printf("%d \n", c) // 18
fmt.Printf("%o \n", c) // 22
fmt.Printf("%x \n", c) // 12
fmt.Printf("%b \n", c) // 10010
|
4.4.2、浮点型
float类型分为float32
和float64
两种类型,这两种浮点型数据格式遵循 IEEE 754 标准。
类型 | 最大值 | 最小非负数 |
float32 |
3.402823466385288598117041834516925440e +38 |
1.401298464324817070923729583289916131280e -45 |
float64 |
1.797693134862315708145274237317043567981e +308 |
4.940656458412465441765687928682213723651e -324 |
1
2
3
4
5
6
7
8
9
10
|
var f1 float32
f1 = 3.14
fmt.Println(f1,reflect.TypeOf(f1))
var f2 float64
f2 = 3.14
fmt.Println(f2,reflect.TypeOf(f2))
var f3 = 3.14
fmt.Println(f3,reflect.TypeOf(f2)) // 默认64
|
1
2
3
4
5
|
var f1 = 2e10 // 即使是整数用科学技术表示也是浮点型
fmt.Println(f1,reflect.TypeOf(f1))
var f2 = 2e-2
fmt.Println(f2,reflect.TypeOf(f2))
|
4.4.3、布尔类型
布尔类型是最基本数据类型之一,只有两个值:true和false,分别代表逻辑判断中的真和假,主要应用在条件判断中。
1
2
3
4
5
6
7
|
fmt.Println(1==1)
fmt.Println(2<1)
fmt.Println(5>3)
b:= 3 > 1
fmt.Println(b,reflect.TypeOf(b))
|
4.4.4、字符串
字符串是最基本也是最常用的数据类型,是通过双引号将多个字符按串联起来的一种数据,用于展示文本。
1
2
|
var s = "hello yuan"
fmt.Println(s)
|
单引号只能标识字符
字符串在内存中是一段连续存储空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var s = "hello yuan!"
fmt.Println(s)
// 索引取值 slice[index]
a:= s[2]
fmt.Println(string(a))
// 切片取值slice[start:end], 取出的元素数量为:结束位置 - 开始位置;
b1:=s[2:5] //
fmt.Println(b1)
b2:=s[0:] // 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
fmt.Println(b2)
b3:=s[:8] // 当缺省开始位置时,表示从连续区域开头到结束位置;
fmt.Println(b3)
|
索引从零开始计数
1
2
3
4
|
var s1 = "hello"
var s2 = "yuan"
var s3 = s1+s2 // 生成一个新的字符串
fmt.Println(s3)
|
Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。
转义符 | 含义 |
\r |
回车符(返回行首) |
\n |
换行符(直接跳到下一行的同列位置) |
\t |
制表符 |
\' |
单引号 |
\" |
双引号 |
\\ |
反斜杠 |
举个例子,我们要打印一个Windows平台下的一个文件路径:
1
2
3
4
5
6
7
|
package main
import (
"fmt"
)
func main() {
fmt.Println("str := \"c:\\Code\\lesson01\\go.exe\"")
}
|
Go语言中要定义一个多行字符串时,就必须使用反引号
字符:
1
2
3
4
5
|
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)
|
反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// 整形和浮点型
fmt.Printf("%b\n", 12) // 二进制表示:1100
fmt.Printf("%d\n", 12) // 十进制表示:12
fmt.Printf("%o\n", 12) // 八进制表示:14
fmt.Printf("%x\n", 12) // 十六进制表示:c
fmt.Printf("%X\n", 12) // 十六进制表示:c
fmt.Printf("%f\n", 3.1415) // 有小数点而无指数:3.141500
fmt.Printf("%.4f\n", 3.1415) // 3.1415
fmt.Printf("%.3f\n", 3.1415) // 3.142
fmt.Printf("%g\n", 3.1415) // 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0)输出: 3.1415
fmt.Printf("%e\n", 1000.25) // 科学计数法: 1.000250e+03
// 布尔占位
fmt.Printf("%t\n", true)
// 字符串
fmt.Printf("my name is %s,my age is %d","yuan\n",18)
// 普通占位符
fmt.Printf("%T \n", "yuan") // 相应值的类型
fmt.Printf("%T \n", true) // 相应值的类型
fmt.Printf("%%\n")
type student struct {
name string
age int
}
fmt.Printf("%v\n","yuan") // 相应值的默认格式(the value in a default format)
fmt.Printf("%v\n",123)
fmt.Printf("%v\n",true)
fmt.Printf("%v\n",student{"yuan",18}) // {yuan 18}
fmt.Printf("%#v\n","yuan") // 具体描述(a Go-syntax representation of the value),比如打印结构体时,会添加字段名
fmt.Printf("%#v\n",123)
fmt.Printf("%#v\n",true)
fmt.Printf("%#v\n",student{"yuan",18}) // main.student{name:"yuan", age:18}
|
注意,这里格式化输出只能用Printf()
函数
方法 | 介绍 |
len(str) |
求长度 |
strings.Split |
分割 |
strings.contains |
判断是否包含 |
strings.HasPrefix,strings.HasSuffix |
前缀/后缀判断 |
strings.Index(),strings.LastIndex() |
子串出现的位置 |
strings.Join(a[]string, sep string) |
join操作 |
4.4.5、指针类型(核心类型)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var x = 10
fmt.Printf("%p\n",&x)
x = 100
fmt.Printf("%p\n",&x)
fmt.Println(*&x)
var y = 11
var z = y
fmt.Printf("%p\n",&y)
fmt.Printf("%p\n",&z)
// 当使用等号=将一个变量的值赋给另一个变量时,如 j = i ,实际上是在内存中将 i 的值进行了拷贝,可以通过 &i 获取变量 i 的内存地址。此时如果修改某个变量的值,不会影响另一个。
|
4.4.6、类型转换
Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
强制类型转换的基本语法如下:
其中,Type表示要转换的类型。表达式包括变量、复杂算子和函数返回值等.
1
2
3
4
5
6
7
8
9
10
11
12
|
var a int8
a= 100
fmt.Println(reflect.TypeOf(int64(a)))
fmt.Println(reflect.TypeOf(float64(a)))
// 字节转换字符,不能直接转为字符串
fmt.Println(string(a),reflect.TypeOf(string(a)))
// 数字与字符串之间的转换需要借助strconv标准库
x:= strconv.Itoa(112)
fmt.Println(x,reflect.TypeOf(x))
y,_ := strconv.Atoi("1200")
fmt.Println(y,reflect.TypeOf(y))
|
4.5、运算符
一个程序的最小单位是一条语句,一条语句最少包含一条指令,而指令就是对数据做运算,我们已经学完基本数据类型了,知道如何构建和使用一些最简单的数据,那么我们能对这些数据做什么运算呢,比如fmt.Println(1+1)
这条语句包含两个指令,首先是计算1+1的指令,1就是数据,+就是算术运算符中的相加,这样计算机就可以帮我们执行这个指令计算出结果,然后执行第二个指令,即将计算结果2打印在终端,最终完成这条语句。
4.5.1、算数运算符
运算符 | 描述 |
+ |
相加 |
- |
相减 |
* |
相乘 |
/ |
相除 |
% |
求余 |
++ |
自增 |
– |
自减 |
注意:
1
2
3
4
5
|
a := 1
a ++ // 注意:不能写成 ++ a 或 -- a 必须放在右边使用
// b := a++ // 此处为错误的用法,不能写在一行,要单独作为语句使用
fmt.Println(a) // 2
|
4.5.2、关系运算符
运算符 | 描述 |
== |
检查两个值是否相等,如果相等返回 True 否则返回 False。 |
!= |
检查两个值是否不相等,如果不相等返回 True 否则返回 False。 |
> |
检查左边值是否大于右边值,如果是返回 True 否则返回 False。 |
< |
检查左边值是否小于右边值,如果是返回 True 否则返回 False。 |
>= |
检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 |
<= |
检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 |
4.5.3、逻辑运算符
运算符 | 描述 |
&& |
逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。 |
|| |
逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。 |
! |
逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 |
4.5.4、赋值运算符
运算符 | 描述 |
= |
简单的赋值运算符,将一个表达式的值赋给一个左值 |
+= |
相加后再赋值 |
-= |
相减后再赋值 |
*= |
相乘后再赋值 |
/= |
相除后再赋值 |
%= |
求余后再赋值 |
«= |
左移后赋值 |
»= |
右移后赋值 |
&= |
按位与后赋值 |
^= |
按位异或后赋值 |
|= |
按位或后赋值 |
五、流程控制语句
程序是由语句构成,而流程控制语句 是用来控制程序中每条语句执行顺序的语句。可以通过控制语句实现更丰富的逻辑以及更强大的功能。几乎所有编程语言都有流程控制语句,功能也都基本相似。
其流程控制方式有
这里最简单最常用的就是顺序结构,即语句从上至下一一执行。
5.1、分支语句
顺序结构的程序虽然能解决计算、输出等问题,但不能做判断再选择。对于要先做判断再选择的问题就要使用分支结构。
5.1.1、单分支语句
语法:
1
2
3
|
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}
|
1
2
3
4
5
|
var is_login = false
if is_login {
fmt.Println("hi,yuan!")
}
|
5.1.2、双分支语句
双分支语句顾名思义,二条分支二选一执行!
1
2
3
4
5
6
7
8
|
// 用于条件判断
var is_login = false
if is_login {
fmt.Println("hi,yuan!")
} else {
fmt.Println("请登录!")
}
|
5.1.3、if
多分支语句
多分支即从比双分支更多的分支选择一支执行。
1
2
3
4
5
6
7
8
9
10
11
|
var score = 99
if score > 90 {
fmt.Println("成绩优秀!")
} else if score > 80 {
fmt.Println("成绩良好!")
} else if score > 60 {
fmt.Println("成绩及格!")
} else {
fmt.Println("成绩不及格!")
}
|
不管多少条分支只能执行一条分支!
5.1.4、switch
多分支语句
语法:
1
2
3
4
5
6
7
8
|
switch var {
case val1:
...
case val2:
...
default:
...
}
|
switch语句也是多分支选择语句,执行哪一代码块,取决于switch后的值与哪一case值匹配成功,则执行该case后的代码块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/*
给出如下选项,并根据选项进行效果展示:
输入1:则输出"普通攻击"
输入2:则输出"超级攻击"
输入3:则输出"使用道具"
输入3:则输出"逃跑"
*/
var choice int
fmt.Println("请输入选择:")
fmt.Scanln(&choice)
//fmt.Println(choice,reflect.TypeOf(choice))
switch choice {
case 1:fmt.Println("普通攻击")
case 2:fmt.Println("超级攻击")
case 3:fmt.Println("使用道具")
case 4:fmt.Println("逃跑")
default:fmt.Println("输入有误!")
}
|
1、switch比if else更为简洁
2、执行效率更高。switch…case会生成一个跳转表来指示实际的case分支的地址,而这个跳转表的索引号与switch变量的值是相等的。从而,switch…case不用像if…else那样遍历条件分支直到命中条件,而只需访问对应索引号的表项从而到达定位分支的目的。
3、到底使用哪一个选择语句,代码环境有关,如果是范围取值,则使用if else语句更为快捷;如果是确定取值,则使用switch是更优方案。
switch同时支持多条件匹配:
1
2
3
4
|
switch{
case 1,2:
default:
}
|
5.2、循环语句
在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。一组被重复执行的语句称之为循环体,能否继续重复,决定循环的终止条件。
与其它主流编程语言不同的的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构。
5.2.1、for循环
通过关系表达式或逻辑表达式控制循环
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
count := 0 // 初始化语句
for count<10{ // 条件判断
fmt.Println(count)
count ++ // 步进语句
}
/*
无限循环
for true{
}
*/
|
将初始化语句、条件判断以及步进语句格式固定化的循环方式,本质上和上面的for循环没有区别。
1
2
3
4
|
for init;condition;post {
// 循环体语句
}
|
-
init
: 初始化语句,一般为赋值表达式,给控制变量赋初值;
-
condition
:条件判断,一般是关系表达式或逻辑表达式,循环控制条件;
-
post
: 步进语句,一般为赋值表达式,给控制变量增量或减量。
1
2
3
|
for i := 1; i < 10; i++ {
fmt.Println(i)
}
|
执行流程:
1
2
3
4
5
|
// 遍历字符串
hello := "HelloYuan"
for k, v := range hello {
fmt.Printf("%d,%c\n", k, v)
}
|
案例:计算1-100的和
1
2
3
4
5
|
var s = 0
for i := 1; i <= 100; i++ {
s += i
}
fmt.Println(s)
|
5.2.2、退出循环
如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用break或continue关键字。
当 break 关键字用于 for 循环时,会终止循环而执行整个循环语句后面的代码。break 关键字通常和 if 语句一起使用,即满足某个条件时便跳出循环,继续执行循环语句下面的代码。
1
2
3
4
5
6
|
for i := 0; i < 10; i++ {
if i==8{
break
}
fmt.Println(":",i)
}
|
不同于break退出整个循环,continue指的是退出当次循环。
1
2
3
4
5
6
|
for i := 0; i < 10; i++ {
if i==8{
continue
}
fmt.Println(":",i)
}
|
5.2.3、循环嵌套
在一个循环体语句中又包含另一个循环语句,称为循环嵌套
在控制台上打印一个五行五列的矩形,如下图所示
*****
*****
*****
*****
*****
1
2
3
4
5
6
7
|
for i := 0; i < 5; i++ {
for j:=0;j<5;j++ {
fmt.Print("*")
}
fmt.Print("\n")
}
|
在控制台上打印一个如下图所示的三角形
*
**
***
****
*****
1
2
3
4
5
6
|
for i := 0; i < 5; i++ {
for j := 0; j <= i; j++ {
fmt.Printf("*")
}
fmt.Println()
}
|
5.3、练习题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/*
(1) 斐波那契数列指的是一个数列 0, 1, 1, 2, 3, 5, 8, 13,特别指出:第0项是0,第1项是第一个1。从第三项开始,每一项都等于前两项之和。
(2) 计算索引为10的斐波那契数列对应的值
(3) 求1+2!+3!+4!+……+10!的和
(4) 程序随机内置一个位于一定范围内的数字作为猜测的结果,由用户猜测此数字。用户每猜测一次,由系统提示猜测结果:太大了、太小了或者猜对了,直到用户猜对结果或者猜测次数用完导致失败。设定一个理想数字比如:66,让用户三次机会猜数字,如果比66大,则显示猜测的结果大了;
如果比66小,则显示猜测的结果小了;只有等于66,显示猜测结果正确,退出循环。最多三次都没有猜测正确,退出循环,并显示‘都没猜对,继续努力’。
(5) 打印菱形小星星
*
***
*****
*******
*********
***********
***********
*********
*******
*****
***
*
*/
|
参考代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
var n int = 6
for i := 1; i <= n; i++ {
for k := 1; k <= n-i; k++ {
fmt.Print(" ")
}
for j := 1; j <= 2*i-1; j++ {
fmt.Print("*")
}
fmt.Println()
}
for i := n - 1; i >= 1; i-- {
for k := 1; k <= n-i; k++ {
fmt.Print(" ")
}
for j := 1; j <= 2*i-1; j++ {
fmt.Print("*")
}
fmt.Println()
}
|
六、容器数据类型
6.1、数组
我们之前学习过变量,当存储一个学生名字时可以name="yuan"
,但是如果班级有三十人,每个人的名字都想存储到内存中怎么办呢?总不能用三十个变量分别存储吧,这时数组就可以发挥作用了。
数组其实是和字符串一样的序列类型,不同于字符串在内存中连续存储字符,数组用[]
的语法将同一类型的多个值存储在一块连续内存中。
6.1.1、数组的声明语法
1
2
3
4
5
|
var names [5]string
fmt.Println(names,reflect.TypeOf(names)) // [ ] [5]string
var ages [5]int
fmt.Println(ages,reflect.TypeOf(ages)) // [0 0 0 0 0] [5]int
|
1、数组的长度是固定的
2、元素可以是任意基本类型,但必须是同一类型!
6.1.2、数组初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
var names [5]string
var ages [5]int
names[0] = "张三"
names[1] = "李四"
names[2] = "王五"
names[3] = "赵六"
names[4] = "孙七"
fmt.Println(names) // [张三 李四 王五 赵六 孙七]
ages[0] = 23
ages[1] = 24
ages[2] = 25
ages[3] = 26
ages[4] = 27
fmt.Println(ages) // [23 24 25 26 27]
|
1
2
3
4
|
var names = [3]string{"张三","李四","王五"}
var ages = [3]int{23,24,25}
fmt.Println(names) // [张三 李四 王五]
fmt.Println(ages) // [23 24 25]
|
1
2
3
4
|
var names = [...]string{"张三","李四","王五"}
var ages = [...]int{23,24,25}
fmt.Println(names,reflect.TypeOf(names)) // [张三 李四 王五] [3]string
fmt.Println(ages,reflect.TypeOf(ages)) // [23 24 25] [3]int
|
1
2
|
var names = [...]string{0:"张三",2:"王五"}
fmt.Println(names) // [张三 王五]
|
6.1.3、访问数组数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var names = [...]string{"张三","李四","王五","赵六","孙七"}
// 索引取值
fmt.Println(names[2])
// 切片取值
fmt.Println(names[0:4])
fmt.Println(names[0:])
fmt.Println(names[:3])
// 循环取值
for i:=0;i<len(names);i++{
fmt.Println(i,names[i])
}
for k,v := range names{
fmt.Println(k,v)
}
|
6.2、切片(slice)
切片是一个动态数组,因为数组的长度是固定的,所以操作起来很不方便,比如一个names数组,我想增加一个学生姓名都没有办法,十分不灵活。所以在开发中数组并不常用,切片类型才是大量使用的。
6.2.1、生成切片的方式
切片语法:
1
2
3
4
5
6
7
8
9
10
11
12
|
slice [start : end]
/*
切片的概念和数组息息相关,切片中有三个要素:
slice:表示目标切片对象;
start:对应目标切片对象的索引;
end:对应目标切片的结束索引。
*/
|
1、取出的元素数量为:结束位置 - 开始位置;
2、取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)]
获取;
3、当缺省开始位置时,表示从连续区域开头到结束位置;
4、当缺省结束位置时,表示从开始位置到整个连续区域末尾;
5、两者同时缺省时,与切片本身等效;
6、两者同时为 0 时,等效于空切片,一般用于切片复位。
除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:
var name []Type
其中 name 表示切片的变量名,Type 表示切片对应的元素类型。
1
2
|
var names = []string{"张三","李四","王五"}
fmt.Println(names,reflect.TypeOf(names)) // [张三 李四 王五 赵六 孙七] []string
|
6.2.2、值类型和引用类型
数据类型从存储方式分为两类:值类型和引用类型!
- (1) 值类型:基本数据类型(
int,float,bool,string
)以及数组和struct
都属于值类型。
特点:变量直接存储值,内存通常在栈中分配,栈在函数调用完会被释放。值类型变量声明后,不管是否已经赋值,编译器为其分配内存,此时该值存储于栈上。
1
2
3
4
|
var a int //int类型默认值为 0
var b string //string类型默认值为 nil空
var c bool //bool类型默认值为false
var d [2]int //数组默认值为[0 0]
|
当使用等号=将一个变量的值赋给另一个变量时,如 j = i ,实际上是在内存中将 i 的值进行了拷贝,可以通过 &i 获取变量 i 的内存地址。此时如果修改某个变量的值,不会影响另一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 整型赋值
var a =10
b := a
b = 101
fmt.Printf("a:%v,a的内存地址是%p\n",a,&a)
fmt.Printf("b:%v,b的内存地址是%p\n",b,&b)
//数组赋值
var c =[3]int{1,2,3}
d := c
d[1] = 100
fmt.Printf("c:%v,c的内存地址是%p\n",c,&c)
fmt.Printf("d:%v,d的内存地址是%p\n",d,&d)
// 字符串处理
s:="hello world"
ns:=strings.ToUpper(s)
fmt.Printf("s:%v,c的内存地址是%p\n",s,&s)
fmt.Printf("ns:%v,ns的内存地址是%p\n",ns,&ns)
|
- (2) 引用类型:指针,
slice,map,chan,interface
等都是引用类型。特点:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。
变量直接存放的就是一个内存地址值,这个地址值指向的空间存的才是值。所以修改其中一个,另外一个也会修改(同一个内存地址)。
引用类型必须申请内存才可以使用,make()是给引用类型申请内存空间。
6.2.3、切片原理
1
2
3
4
5
6
7
|
var names = [...]string{"张三","李四","王五","赵六","孙七"}
s1:= names[0:3]
s2:= names[2:5]
fmt.Println(s1) // [张三 李四 王五]
fmt.Println(s2) // [王五 赵六 孙七]
s1[2] = "yuan"
fmt.Println(s2) // 思考?
|
为什么切片s2
会受到切片s1
的影响?根本原因是切片是引用类型!
接下来我们通过切片的存储原理来理解引用类型!
切片的构造根本是对一个具体数组通过切片起始指针,切片长度以及最大容量三个参数确定下来的
1
2
3
4
5
|
type SliceHeader struct {
Data uintptr // 指针,指向底层数组中切片指定的开始位置
Len int // 长度,即切片的长度
Cap int // 最大长度(容量),也就是切片开始位置到数组的最后位置的长度
}
|
举例:
1
2
3
4
5
|
arr := [8]int{12,23,34,45,56,67,78,89} // 数组8
s1:=arr[2:6]
fmt.Println(s1) // [34 45 56 67]
fmt.Println(len(s1)) // 4
fmt.Println(cap(s1)) // 6
|
练习题:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var a=[...]int{1,2,3,4,5,6}
a1:=a[0:3]
a2:=a[0:5]
a3:=a[1:5]
a4:=a[1:]
a5:=a[:]
a6:=a3[1:2]
fmt.Printf("a1的长度%d,容量%d\n",len(a1),cap(a1))
fmt.Printf("a2的长度%d,容量%d\n",len(a2),cap(a2))
fmt.Printf("a3的长度%d,容量%d\n",len(a3),cap(a3))
fmt.Printf("a4的长度%d,容量%d\n",len(a4),cap(a4))
fmt.Printf("a5的长度%d,容量%d\n",len(a5),cap(a5))
fmt.Printf("a6的长度%d,容量%d\n",len(a6),cap(a6))
|
如果没有数组,则是对最初始的切片构建数组存储!
6.2.4、make函数
变量的声明我们可以通过var关键字,然后就可以在程序中使用。当我们不指定变量的默认值时,这些变量的默认值是他们的零值,比如int类型的零值是0,string类型的零值是"",引用类型的零值是nil。
对于例子中的两种类型的声明,我们可以直接使用,对其进行赋值输出。但是如果我们换成引用类型呢?
1
2
3
4
|
// arr := []int{}
var arr [] int // 如果是 var arr [2] int
arr[0] = 1
fmt.Println(arr)
|
从这个提示中可以看出,对于引用类型的变量,我们不光要声明它,还要为它分配内容空间。
对于值类型的声明不需要,是因为已经默认帮我们分配好了。要分配内存,就引出来今天的make函数。make也是用于chan
、map
以及切片的内存创建,而且它返回的类型就是这三个类型本身。
如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
1
|
make( []Type, size, cap )
|
其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。 示例如下:
1
2
3
4
5
|
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))
fmt.Println(cap(a), cap(b))
|
使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
1
2
3
4
5
|
a := make([]int, 5)
b:=a[0:3]
a[0] = 100
fmt.Println(a)
fmt.Println(b)
|
6.2.5、append扩容(重点)
上面我们已经讲过,切片作为一个动态数组是可以添加元素的,添加方式为内建方法append。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// 案例1
a := []int{12,23,34}
fmt.Println(len(a))
fmt.Println(cap(a))
c:=append(a, 45)
a[0] = 100
fmt.Println(a)
fmt.Println(c)
// 案例2
a := make([]int,3,10)
fmt.Println(a)
b:=append(a,45,56)
fmt.Println(b)
a[0] = 100
fmt.Println(a)
fmt.Println(b)
// 案例3
l := make([]int,5,10)
v1:= append(l, 1)
fmt.Println(v1)
fmt.Printf("%p\n",&v1)
v2:= append(l, 2)
fmt.Println(v2)
fmt.Printf("%p\n",&v2)
fmt.Println(v1)
/*
l 0 0 0 0 0 _ _ _ _ _
v1 0 0 0 0 0 1 _ _ _ _
v2 0 0 0 0 0 2 _ _ _ _*/
|
1、每次 append 操作都会检查 slice 是否有足够的容量,如果足够会直接在原始数组上追加元素并返回一个新的 slice,底层数组不变,但是这种情况非常危险,极度容易产生 bug!而若容量不够,会创建一个新的容量足够的底层数组,先将之前数组的元素复制过来,再将新元素追加到后面,然后返回新的 slice,底层数组改变而这里对新数组的进行扩容
2、扩容策略:如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。
注意
1
2
3
4
|
var x = make([]int,5,10) // 指针:0 长度5 容量10
var y = append(x, 100)
fmt.Println(x)
fmt.Println(y)
|
面试题
1
2
3
4
5
6
7
|
arr := [4]int{10, 20, 30, 40}
s1 := arr[0:2]
s2 := s1
s3 := append(append(append(s1, 1),2),3)
s1[0] = 1000
fmt.Println(s2[0])
fmt.Println(s3[0])
|
6.2.6、插入删除元素
1
2
3
|
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
|
在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
1
2
3
|
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
|
每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
1
2
3
4
5
|
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
|
要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
思考题:
1
2
3
4
5
|
a:=[...]int{1,2,3}
b:=a[:]
b =append(b[:1],b[2:]...)
fmt.Println(a)
fmt.Println(b)
|
6.2.7、切片元素排序
1
2
3
4
5
|
a:=[]int{10,2,3,100}
sort.Ints(a)
fmt.Println(a)
sort.Sort(sort.Reverse(sort.IntSlice(a[:])))
fmt.Println(a)
|
6.3、map(映射)类型
通过切片,我们可以动态灵活存储管理学生姓名、年龄等信息,比如
1
2
3
4
|
names := []string{"张三","李四","王五"}
ages := []int{23,24,25}
fmt.Println(names)
fmt.Println(ages)
|
但是如果我想获取张三的年龄,这是一个再简单不过的需求,但是却非常麻烦,我们需要先获取张三的切片索引,再去ages切片中对应索引取出,前提还得是姓名年龄按索引对应存储。
所以在主流编程语言中都会存在一种映射(key-value)类型,在JS
中叫json
对象类型,在python中叫字典(dict
)类型,而在Go语言中则叫Map类型。
6.3.1、map的声明
不同于切片根据索引查找值,map类型是根据key查找值。
map 是引用类型,声明语法:
1
|
var map_name map[key_type]value_type
|
其中:
map_name
为 map 的变量名。
key_type
为键类型。
value_type
是键对应的值类型。
1
2
|
var info map[string]string
fmt.Println(info) // map[]
|
6.3.2、map的初始化
1
2
3
4
5
|
// var info map[string]string // 没有默认空间
info := make(map[string]string)
info["name"] = "yuan"
info["age"] = "23"
fmt.Println(info) // map[age:23 name:yuan]
|
map的键值是无序的!
1
2
|
info := map[string]string{"name": "yuan", "age": "23","gender":"male"}
fmt.Println(info) // map[age:18 gender:male name:yuan]
|
6.3.3、访问map
1
2
3
4
5
6
7
8
9
10
|
info := map[string]string{"name": "yuan", "age": "18","gender":"male"}
//val:= info["name"]
val,is_exist:= info["name"] // 判断某个键是否存在map数据中
if is_exist{
fmt.Println(val)
fmt.Println(is_exist)
}else {
fmt.Println("键不存在!")
}
|
1
2
3
|
for k,v :=range info{
fmt.Println(k,v)
}
|
6.3.4、map 容量
和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:
1
|
make(map[keytype]valuetype, cap)
|
例如:
1
|
map2 := make(map[string]float, 100)
|
当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
6.3.5、删除元素
一个内置函数 delete(),用于删除容器内的元素
1
2
3
|
info := map[string]string{"name": "yuan", "age": "18","gender":"male"}
delete(info,"gender")
fmt.Println(info)
|
如果想清空一个map,最优方式即创建一个新的map!
6.3.6、map的灵活运用
1
2
3
4
5
6
7
8
9
10
11
12
|
// 案例1
data := map[string][]string{"hebei":[]string{"廊坊市","石家庄","邯郸"},"beijing":[]string{"朝阳","丰台","海淀"}}
fmt.Println(data["beijing"][1])
// 案例2
info := map[int]map[string]string{1001:{"name":"yuan","age":"23"},1002:{"name":"alvin","age":"33"}}
fmt.Println(info[1002]["name"])
// 案例3
infor2 := []map[string]string{{"name":"alvin1","sid":"1001"},{"name":"alvin2","sid":"1002"},{"sid":"1003","name":"alvin3"}}
fmt.Println(infor2[2]["sid"])
|
七、函数
设计一个程序:
先打印一个六层菱形然后计算一下1-100的和后再打印一个菱形
期待结果:
*
***
*****
*******
*********
***********
*********
*******
*****
***
*
5050
*
***
*****
*******
*********
***********
*********
*******
*****
***
*
如果没有函数,我们的实现方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
// 1、打印菱形
var n int = 6
for i := 1; i <= n; i++ {
for k := 1; k <= n-i; k++ {
fmt.Print(" ")
}
for j := 1; j <= 2*i-1; j++ {
fmt.Print("*")
}
fmt.Println()
}
for i := n - 1; i >= 1; i-- {
for k := 1; k <= n-i; k++ {
fmt.Print(" ")
}
for j := 1; j <= 2*i-1; j++ {
fmt.Print("*")
}
fmt.Println()
}
// 2、计算1-100的和
var s = 0
for i := 1; i <= 100; i++ {
s += i
}
fmt.Println(s)
// 3、再打印菱形
//var n int = 6
for i := 1; i <= n; i++ {
for k := 1; k <= n-i; k++ {
fmt.Print(" ")
}
for j := 1; j <= 2*i-1; j++ {
fmt.Print("*")
}
fmt.Println()
}
for i := n - 1; i >= 1; i-- {
for k := 1; k <= n-i; k++ {
fmt.Print(" ")
}
for j := 1; j <= 2*i-1; j++ {
fmt.Print("*")
}
fmt.Println()
}
|
相信大家一定看出来了,这种方式会出现大量重复代码,对于阅读和维护整个程序都会变得十分麻烦。
这时候,函数就出现了!
简单说,函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,避免大量重复的代码。
刚才的程序函数版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
package main
import "fmt"
func print_ling(){
// 打印菱形
var n int = 6
for i := 1; i <= n; i++ {
for k := 1; k <= n-i; k++ {
fmt.Print(" ")
}
for j := 1; j <= 2*i-1; j++ {
fmt.Print("*")
}
fmt.Println()
}
for i := n - 1; i >= 1; i-- {
for k := 1; k <= n-i; k++ {
fmt.Print(" ")
}
for j := 1; j <= 2*i-1; j++ {
fmt.Print("*")
}
fmt.Println()
}
}
func cal_sum() {
// 计算1-100的和
var s = 0
for i := 1; i <= 100; i++ {
s += i
}
fmt.Println(s)
}
func main() {
// 打印菱形
print_ling()
// 求和计算
cal_sum()
// 打印菱形
print_ling()
}
|
7.1、函数声明
go语言是通过func
关键字声明一个函数的,声明语法格式如下
1
2
3
4
|
func 函数名(形式参数) (返回值) {
函数体
return 返回值 // 函数终止语句
}
|
其中:
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 形式参数:参数由参数变量和参数变量的类型组成,多个参数之间使用
,
分隔。
- 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用
()
包裹,并用,
分隔。
- 函数体:实现指定功能的代码块。
1
2
3
4
5
6
7
8
9
|
func cal_sum100() {
// 计算1-100的和
var s = 0
for i := 1; i <= 100; i++ {
s += i
}
fmt.Println(s)
}
|
7.2、函数调用
声明一个函数并不会执行函数内代码,只是完成一个一个包裹的作用。真正运行函数内的代码还需要对声明的函数进行调用,一个函数可以在任意位置多次调用。调用一次,即执行一次该函数内的代码。
调用语法:
案例:
1
2
3
4
5
6
7
8
9
|
func cal_sum100() {
// 计算1-100的和
var s = 0
for i := 1; i <= 100; i++ {
s += i
}
fmt.Println(s)
}
cal_sum100()
|
7.3、函数参数
7.3.1、什么是参数
什么是参数,函数为什么需要参数呢?
上面我们将计算1-100的和通过函数实现了,但是完成新的需求:
分别计算并在终端打印1-100的和,1-150的和以及1-200的和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
package main
import "fmt"
func cal_sum100() {
// 计算1-100的和
var s = 0
for i := 1; i <= 100; i++ {
s += i
}
fmt.Println(s)
}
func cal_sum150() {
// 计算1-100的和
var s = 0
for i := 1; i <= 150; i++ {
s += i
}
fmt.Println(s)
}
func cal_sum200() {
// 计算1-100的和
var s = 0
for i := 1; i <= 200; i++ {
s += i
}
fmt.Println(s)
}
func main() {
cal_sum100()
cal_sum150()
cal_sum200()
}
|
这样当然可以实现,但是是不是依然有大量重复代码,一会发现三个函数出了一个变量值不同以外其他都是相同的,所以为了能够在函数调用的时候动态传入一些值给函数,就有了参数的概念。
参数从位置上区分分为形式参数和实际参数。
1
2
3
4
5
6
|
// 函数声明
func 函数名(形式参数1 参数1类型,形式参数2 参数2类型,...){
函数体
}
// 调用函数
函数名(实际参数1,实际参数2,...)
|
函数每次调用可以传入不同的实际参数,传参的过程其实就是变量赋值的过程,将实际参数按位置分别赋值给形参。
还是刚才的案例,用参数实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import "fmt"
func cal_sum(n int) {
// 计算1-100的和
var s = 0
for i := 1; i <= n; i++ {
s += i
}
fmt.Println(s)
}
func main() {
cal_sum(100)
cal_sum(101)
cal_sum(200)
}
|
这样是不是就灵活很多了呢,所以基本上一个功能强大的函数都会有自己需要的参数,让整个业务实现更加灵活。
7.3.2、位置参数
位置参数,有时也称必备参数,指的是必须按照正确的顺序将实际参数传到函数中,换句话说,调用函数时传入实际参数的数量和位置都必须和定义函数时保持一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 函数声明 两个形参:x,y
func add_cal(x int,y int){
fmt.Println(x+y)
}
func main() {
// 函数调用,按顺序传参
// add_cal(2) // 报错
// add_cal(232,123,12) // 报错
add_cal(100,12)
}
|
7.3.3、不定长参数
如果想要一个函数能接收任意多个参数,或者这个函数的参数个数你无法确认,就可以使用不定长参数,也叫可变长参数。Go语言中的可变参数通过在参数名后加...
来标识。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package main
import "fmt"
func sum(nums ...int) { //变参函数
fmt.Println("nums",nums)
total := 0
for _, num := range nums {
total += num
}
fmt.Println(total)
}
func main() {
sum(12,23)
sum(1,2,3,4)
}
|
注意:可变参数通常要作为函数的最后一个参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
import "fmt"
func sum(base int, nums ...int) int {
fmt.Println(base, nums)
sum := base
for _, v := range nums {
sum = sum + v
}
return sum
}
func main() {
ret := sum(10,2,3,4)
fmt.Println(ret)
}
|
go的函数强调显示表达的设计哲学,没有默认参数
7.4、函数返回值
7.4.1、返回值的基本使用
函数的返回值是指函数被调用之后,执行函数体中的代码所得到的结果,这个结果通过 return 语句返回。return 语句将被调函数中的一个确定的值带回到主调函数中,供主调函数使用。函数的返回值类型是在定义函数时指定的。return 语句中表达式的类型应与定义函数时指定的返回值类型必须一致。
1
2
3
4
5
6
|
func 函数名(形参 形参类型)(返回值类型){
// 函数体
return 返回值
}
变量 = 函数(实参) // return 返回的值赋值给某个变量,程序就可以使用这个返回值了。
|
同样是设计一个加法计算函数:
1
2
3
4
5
6
7
8
|
func add_cal(x,y int) int{
return x+y
}
func main() {
ret := add_cal(1,2)
fmt.Println(ret)
}
|
7.4.2、无返回值
声明函数时没有定义返回值,函数调用的结果不能作为值使用
1
2
3
4
5
6
7
8
9
10
|
func foo(){
fmt.Printf("hi,yuan!")
return // 不写return默认return空
}
func main() {
// ret := foo() // 报错:无返回值不能将调用的结果作为值使用
foo()
}
|
7.4.3、返回多个值
函数可以返回多个值
1
2
3
4
5
6
7
8
|
func get_name_age() (string, int) {
return "yuan",18
}
func main() {
a, b := get_name_age()
fmt.Println(a, b)
}
|
7.4.4、返回值命名
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return
关键字返回。
1
2
3
4
5
|
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return // return sum sub
}
|
7.5、作用域
所谓变量作用域,即变量可以作用的范围。
作用域(scope)通常来说,程序中的标识符并不是在任何位置都是有效可用的,而限定这个标识符的可用性的范围就是这个名字的作用域。
变量根据所在位置的不同可以划分为全局变量和局部变量
- 局部变量 :写在{}中或者函数中或者函数的形参, 都是局部变量
1、局部变量的作用域是从定义的那一行开始, 直到遇到 } 结束或者遇到return为止
2、局部变量, 只有执行了才会分配存储空间, 只要离开作用域就会自动释放
3、局部变量存储在栈区
4、局部变量如果没有使用, 编译会报错。全局变量如果没有使用, 编译不会报错
5、:=只能用于局部变量, 不能用于全局变量
1、全局变量的作用域是从定义的那一行开始, 直到文件末尾为止
2、全局变量, 只要程序一启动就会分配存储空间, 只有程序关闭才会释放存储空间,
3、全局变量存储在静态区(数据区)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func foo() {
// var x =10
x = 10
fmt.Println(x)
}
var x = 30
func main() {
// var x = 20
foo()
fmt.Println(x)
}
|
注意,if,for语句具备独立开辟作用域的能力:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// if的局部空间
if true{
x:=10
fmt.Println(x)
}
fmt.Println(x)
// for的局部空间
for i:=0;i<10 ;i++ {
}
fmt.Println(i)
|
7.6、匿名函数
匿名函数,顾名思义,没有函数名的函数。
匿名函数的定义格式如下:
1
2
3
|
func(参数列表)(返回参数列表){
函数体
}
|
匿名函数可以在使用函数的时候再声明调用
1
2
3
4
5
6
7
8
9
10
|
//(1)
(func() {
fmt.Println("yuan")
})()
//(2)
var z =(func(x,y int) int {
return x + y
})(1,2)
fmt.Println(z)
|
也可以将匿名函数作为一个func
类型数据赋值给变量
1
2
3
4
5
6
7
|
var f = func() {
fmt.Println("yuan")
}
fmt.Println(reflect.TypeOf(f)) // func
f() // 赋值调用调用
|
Go语言不支持在函数内部声明普通函数,只能声明匿名函数。
1
2
3
4
5
6
7
|
func foo() {
fmt.Println("foo功能")
f := func(){
fmt.Println("bar功能")
}
fmt.Println(f)
}
|
7.7、高阶函数
一个高阶函数应该具备下面至少一个特点:
- 将一个或者多个函数作为形参
- 返回一个函数作为其结果
首先明确一件事情:函数名亦是一个变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import "fmt"
func add_cal(x int,y int){
fmt.Println(x+y)
}
func main() {
var a = add_cal
a(2,3)
fmt.Println(a)
fmt.Println(add_cal)
}
|
结论:函数参数是一个变量,所以,函数名当然可以作为一个参数传入函数体,也可以作为一个返回值。
7.7.1、函数参数
比如你在一个订单函数中需要调用支付函数怎么办?
有同学立刻想到是不是可以这样写呢:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func pay() {
fmt.Printf("完成支付功能")
}
func order_handler() {
fmt.Printf("订单功能!")
pay()
}
func main() {
order_handler()
}
|
但是,如果支付方式有多种,比如阿里支付和微信支付怎么办呢?所以,这就涉及到高阶函数的使用,将具体的支付函数变量作为参数传入订单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package main
import "fmt"
func ali_pay() {
fmt.Printf("ali_pay\n")
}
func wechat_pay() {
fmt.Printf("wechat_pay\n")
}
func order_handler(pay_func func()) {
fmt.Printf("订单功能!\n")
pay_func()
}
func main() {
order_handler(wechat_pay)
order_handler(ali_pay)
}
|
注意如果函数参数亦有参数该怎么写呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import "fmt"
func add(x,y int ) int {
return x+y
}
func mul(x,y int ) int {
return x*y
}
// 双值计算器
func cal(x,y int,f func(x,y int) int) int{
return f(x,y)
}
func main() {
ret1 := cal(12,3,add)
fmt.Println(ret1)
ret2 := cal(12,3,mul)
fmt.Println(ret2)
}
|
7.7.2、函数返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package main
import (
"fmt"
)
func foo() func(){
inner := func() {
fmt.Printf("函数inner执行")
}
return inner
}
func main() {
foo()()
}
|
7.8、闭包
7.8.1、闭包函数
闭包并不只是一个go中的概念,在函数式编程语言中应用较为广泛。
首先看一下维基上对闭包的解释:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包。
价值:能够动态灵活的创建以及传递函数,体现出函数式编程的特点。所以在一些场合,我们就多了一种编码方式的选择,适当的使用闭包可以使得我们的代码简洁高效。
1
2
3
4
5
6
7
8
9
10
11
12
|
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
fmt.Println(adder()(100))
}
|
7.8.2、装饰函数
在go语言中没有装饰器的概念(没有@语法糖),可以使用闭包函数来实现装饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package main
import (
"fmt"
"time"
)
func timer(f func(x,y int)) (func(int,int)) {
wrapper := func(x,y int) {
time_before:=time.Now().Unix()
f(x,y)
time_after:=time.Now().Unix()
fmt.Println("运行时间:",(time_after - time_before))
}
return wrapper
}
func add(x,y int) {
fmt.Printf("%d+%d=%d",x,y,x+y)
time.Sleep(time.Second*2)
}
func mul(x,y int) {
fmt.Printf("%d*%d=%d",x,y,x*y)
time.Sleep(time.Second*3)
}
func main() {
var add = timer(add)
add(1,4)
var mul =timer(mul)
mul(2,5)
}
|
7.9、defer语句
defer语句是go语言提供的一种用于注册延迟调用的机制,是go语言中一种很有用的特性。
7.9.1、defer的用法
defer语句注册了一个函数调用,这个调用会延迟到defer语句所在的函数执行完毕后执行,所谓执行完毕是指该函数执行了return语句、函数体已执行完最后一条语句或函数所在协程发生了恐慌。
1
2
3
|
fmt.Println("test01")
defer fmt.Println("test02")
fmt.Println("test03")
|
编程经常会需要申请一些资源,比如数据库连接、打开文件句柄、申请锁、获取可用网络连接、申请内存空间等,这些资源都有一个共同点那就是在我们使用完之后都需要将其释放掉,否则会造成内存泄漏或死锁等其它问题。但操作完资源忘记关闭释放是正常的,而defer可以很好解决这个问题
1
2
3
4
5
6
7
8
9
10
|
// 打开文件
file_obj,err:=os.Open("满江红")
if err != nil {
fmt.Println("文件打开失败,错误原因:",err)
}
// 关闭文件
defer file_obj.Close()
// 操作文件
|
7.9.2、多个defer执行顺序
当一个函数中有多个defer语句时,会按defer定义的顺序逆序执行,也就是说最先注册的defer函数调用最后执行。
1
2
3
4
5
|
fmt.Println("test01")
defer fmt.Println("test02")
fmt.Println("test03")
defer fmt.Println("test04")
fmt.Println("test05")
|
7.9.3、defer的拷贝机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 案例1
foo := func() {
fmt.Println("I am function foo1")
}
defer foo()
foo = func() {
fmt.Println("I am function foo2")
}
// 案例2
x := 10
defer func(a int) {
fmt.Println(a)
}(x)
x++
// 闭包案例 (闭包里和外层引用了同一块内存空间)
x := 10
defer func() {
fmt.Println(x)
}()
x++
|
当执行defer语句时,函数调用不会马上发生,会先把defer注册的函数及变量拷贝到defer栈中保存,直到函数return前才执行defer中的函数调用。需要格外注意的是,这一拷贝拷贝的是那一刻函数的值和参数的值。注册之后再修改函数值或参数值时,不会生效。
7.9.4、defer执行时机
在Go语言的函数 return 语句不是原子操作,而是被拆成了两步
rval = xxx
ret
而 defer 语句就是在这两条语句之间执行,也就是
1
2
3
|
rval = xxx
defer_func
ret
|
案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
package main
import "fmt"
func f1() int {
i := 5
defer func() {
i++
}()
return i
}
func f2() *int {
i := 5
defer func() {
i++
fmt.Printf(":::%p",&i)
}()
fmt.Printf(":::%p",&i)
return &i
}
func f3() (result int) {
defer func() {
result++
}()
return 5 // ret (result = 5)
}
func f4() (result int) {
defer func() {
result++
}()
return result // ret result变量的值
}
func f5() (r int) {
t := 5
defer func() {
t = t + 1
}()
return t // ret r = 5 (拷贝t的值5赋值给r)
}
func f6() (r int) {
defer func(r int) {
r = r + 1
}(r)
return 5
}
func f7() (r int) {
defer func(x int) {
r = x + 1
}(r)
return 5
}
func main() {
// println(f1())
// println(*f2())
// println(f3())
// println(f4())
// println(f5())
// println(f6())
}
|
在命名返回方式中,最终函数返回的就是命名返回变量的值,因此,对该命名返回变量的修改会影响到最终的函数返回值!
7.10、递归函数
八、文件操作
8.1、读文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
package main
import (
"fmt"
"os"
)
func main() {
//打开文件
file, err := os.Open("./满江红")
if err != nil {
fmt.Println("err: ", err)
}
//关闭文件句柄
defer file.Close()
// *************************************************** 按字节读取数据 **************************************
/*var b = make([]byte, 2)
n, err :=file.Read(b)
fmt.Printf("读取字节数:%d\n",n)
fmt.Printf("切片值:%v\n",b)
fmt.Printf("读取内容:%v\n",string(b[:n]))*/
// *************************************************** 按行读取数据 **************************************
/*reader:=bufio.NewReader(file)
for {
// data,err:=reader.ReadString('\n')
data,_,err:=reader.ReadLine()
// fmt.Println(data)
fmt.Printf(string(data)+"\n")
if err == io.EOF { //io.EOF 表示文件的末尾
break
}
}*/
// *************************************************** 读取整个文件 **************************************
/*content, err := ioutil.ReadFile("满江红")
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Println(string(content))*/
}
|
8.2、写文件
8.2.1、Write
和WriteString
1
2
3
4
5
6
7
8
9
|
file, err := os.OpenFile("满江红1", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
str := "满江红\n"
file.Write([]byte(str)) //写入字节切片数据
file.WriteString("怒发冲冠,凭栏处、潇潇雨歇。") //直接写入字符串数据
|
8.2.2、bufio.NewWriter
1
2
3
4
5
6
7
8
9
10
11
|
file, err := os.OpenFile("满江红2", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
writer := bufio.NewWriter(file)
writer.WriteString("大浪淘沙\n") //将数据先写入缓存
writer.Flush() //将缓存中的内容写入文件
|
8.2.3、ioutil.WriteFile
1
2
3
4
5
6
|
str := "怒发冲冠,凭栏处、潇潇雨歇。"
err := ioutil.WriteFile("满江红3", []byte(str), 0666)
if err != nil {
fmt.Println("write file failed, err:", err)
return
}
|
九、结构体
Go语言结构体(Struct
)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、char、float 等基本类型组成的。你可以认为结构体是一种聚合类型。
在实际开发中,我们可以将一组类型不同的、但是用来描述同一件事物的变量放到结构体中。例如,在校学生有姓名、年龄、身高、成绩等属性,学了结构体后,我们就不需要再定义多个变量了,将它们都放到结构体中即可。
1
2
3
4
5
6
7
8
9
|
type Student struct {
sid int
name string
sex byte
age int
addr string
score int[]
}
|
Go 语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性。结构体成员,也可称之为成员变量,字段,属性。属性要满足唯一性。
在Go语言中,结构体承担着面向对象语言中类的作用。Go语言中,结构体本身仅用来定义属性。还可以通过接收器函数来定义方法,使用内嵌结构体来定义继承。这样使用结构体相关操作Go语言就可以实现OOP
面向对象编程了。
9.1、声明结构体
Go语言通过type
关键字声明结构体,格式如下:
1
2
3
4
5
|
type 类型名 struct { // 标识结构体的类型名,在同一个包内不能重复
字段1 字段1类型 // 字段名必须唯一
字段2 字段2类型
…
}
|
同类型的变量也可以写在一行,用逗号隔开
1
2
3
4
|
type Book struct {
title,author string
price int
}
|
9.2、结构体的实例化
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。
9.2.1、var实例化
结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
type Student struct {
sid int
name string
sex byte
age int
addr string
}
func main() {
var s Student
fmt.Println(reflect.TypeOf(s)) // main.Student
fmt.Printf("%p",&s) // 0xc0000e0000
fmt.Println(s.name) // 要访问结构体成员,需要使用点号 . 操作符
fmt.Println(s.age)
}
|
结构体属于值类型,即var声明后会像整形字符串一样创建内存空间。
9.2.2、new实例化
Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。 使用 new 的格式如下:其中:
其中:
- T 为类型,可以是结构体、整型、字符串等。
instance
:T 类型被实例化后保存到 instance
变量中,instance
的类型为 *T,属于指针。
Go语言让我们可以像访问普通结构体一样使用来访问结构体指针的成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package main
import (
"fmt"
"reflect"
)
type Student struct {
sid int
name string
sex byte
age int
addr string
}
func main() {
// 实例化
instance := new(Student)
fmt.Println(instance.name) // ""
fmt.Println(instance.sex) // 0
fmt.Println(reflect.TypeOf(instance)) // *main.Student 指针类型
// 初始化
instance.name = "yuan"
instance.name = "yuan"
instance.sex = 100
fmt.Println((*instance).name) // "yuan"
fmt.Println(instance.sex) // 100
}
|
在 C/C++ 语言中,使用 new 实例化类型后,访问其成员变量时必须使用->
操作符。
在Go语言中,访问结构体指针的成员变量时可以继续使用.
,这是因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将 instance.Name
形式转换为 (*instance).Name
。
9.2.3、取址实例化
在Go语言中可以通过取结构体的地址进行实例化:
- T 表示结构体类型。
- ins 为结构体的实例,类型为 *T,是指针类型。
1
2
3
4
5
6
7
8
9
10
|
// 结构体作为指针变量实例化
var s *Student = &Student{}
fmt.Println(s)
fmt.Println(s.sex)
s.sid = 8
s.name="yuan"
s.sex='1'
// s.id也行,但go会自动转为(*s).id
fmt.Println((*s).name)
fmt.Println(s.name)
|
结构体是值类型,不是引用类型。因此使用不同方式实例化的,在赋值时效果时不一样的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
package main
import (
"fmt"
)
type Student struct {
sid int
name string
sex byte
age int
addr string
}
func set_age(age int,s Student) {
s.age = age
}
func set_age2(age int,s *Student) {
s.age = age
}
func main() {
var s1 Student // s1:结构体对象
var s2 *Student = &Student{} // 结构体指针对象
set_age(100,s1)
fmt.Println("age修改后的值:",s1.age)
set_age2(100,s2)
fmt.Println("age修改后的值:",s2.age)
}
|
9.3、结构体的初始化
9.3.1、键值对初始化
结构体可以使用“键值对”(Key value pair)初始化字段,每个“键”(Key)对应结构体中的一个字段,键的“值”(Value)对应字段需要初始化的值。键值对的填充是可选的,不需要初始化的字段可以不填入初始化列表中。结构体实例化后字段的默认值是字段类型的默认值,例如 ,数值为 0、字符串为 “"(空字符串)、布尔为 false、指针为 nil 等。
键值对初始化的格式如下:
1
2
3
4
5
|
ins := 结构体类型名{
字段1: 字段1的值,
字段2: 字段2的值,
…
}
|
下面是对各个部分的说明:
- 结构体类型:定义结构体时的类型名称。
- 字段1、字段2:结构体成员的字段名,结构体类型名的字段初始化列表中,字段名只能出现一次。
- 字段1的值、字段2的值:结构体成员字段的初始值。
1
2
3
4
5
6
|
var s1 Student = Student{sid:1,name:"yuan",sex:'m',age:23,addr:"bj"}
// 指定某几个字段赋值,忽略的字段为 0 或 空
var s2 *Student = &Student{sid:2,name:"alvin"}
fmt.Println(s1.name)
fmt.Println(s2.sid)
|
9.3.2、多值初始化
Go语言可以在“键值对”初始化的基础上忽略“键”,也就是说,可以使用多个值的列表初始化结构体的字段。
多个值使用逗号分隔初始化结构体,例如:
1
2
3
4
5
|
ins := 结构体类型名{
字段1的值,
字段2的值,
…
}
|
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段。
- 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 键值对与值列表的初始化形式不能混用。
1
2
3
4
|
var s1 Student = Student{1,"yuan",'m',23,"bj"}
var s2 *Student = &Student{2,"alvin"} // error
fmt.Println(s1.name)
fmt.Println(s2.sid)
|
9.4、模拟构造函数
Go语言不支持没有构造函数,但是我们可以使用结构体初始化的过程来模拟实现构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func NewStudent(sid int,name string,sex byte,age int,addr string)*Student{
return &Student{
sid:sid,
name:name,
sex:sex,
age:age,
addr:addr,
}
}
func main() {
s:=NewStudent(1001,"yuan",'m',23,"beijing")
fmt.Println(s.name)
fmt.Println(s.age)
}
|
9.5、方法接收器
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。
方法的定义格式如下:
1
2
3
|
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
|
其中,
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为 p
,Connector
类型的接收者变量应该命名为c
等。
- 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
type Player struct{
Name string
HealthPoint int
Magic int
}
// Player结构类的构造函数
func NewPlayer(name string,hp int,mp int) *Player{
return &Player{
name,hp,mp,
}
}
// Player结构的方法
func (p Player) attack() {
fmt.Printf("%s发起攻击!\n",p.Name)
}
func (p Player) buy_prop() {
fmt.Printf("%s购买道具!\n",p.Name)
}
func main() {
player := NewPlayer("yuan",100,100)
player.attack()
player.buy_prop()
}
|
1、官方定义:Methods are not mixed with the data definition (the structs): they are orthogonal to types; representation(data) and behavior (methods) are independent
2、方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
9.6、匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
type Person struct {
string
int
}
func main() {
p1 := Person{
"yuan",
18,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"yuan", int:18}
fmt.Println(p1.string, p1.int) //北京 18
}
|
结构体也可以作为匿名字段使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
type Addr struct {
country string
province string
city string
}
type Person struct {
string
int
Addr
}
func main() {
p1 := Person{
"yuan",
18,
Addr{"中国","广东省","深圳"},
}
fmt.Printf("%#v\n", p1) //main.Person{string:"北京", int:18}
fmt.Println(p1.string, p1.int) // yuan 18
fmt.Println(p1.country) // 中国
fmt.Println(p1.city) // 深圳
}
|
当结构体中有和匿名字段相同的字段时,采用外层优先访问原则
9.7、结构体的"继承”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
package main
import "fmt"
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) eat() {
fmt.Printf("%s is eating!\n", a.name)
}
func (a *Animal) sleep() {
fmt.Printf("%s is sleeping!\n", a.name)
}
//Dog 狗
type Dog struct {
Kind string
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) bark () {
fmt.Printf("%s is barking ~\n", d.name)
}
func main() {
d1 := &Dog{
Kind: "金毛",
Animal: &Animal{ //注意嵌套的是结构体指针
name: "路易十六",
},
}
d1.bark()
d1.eat()
}
|
9.8、json操作
9.8.1、json
初识
JSON
:JavaScript 对象表示法(JavaScript Object Notation),是一种轻量级的数据交换格式。易于人阅读和编写。
go语言数据类型 | json支持的类型 |
整型、浮点型 |
整型、浮点型 |
字符串(在双引号中) |
true/false |
逻辑值(true 或 false ) |
逻辑值(true 或 false ) |
数组,切片 |
数组(在方括号中) |
map |
对象(在花括号中) |
nil |
null |
9.8.2、结构体的json
操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
package main
import (
"encoding/json"
"fmt"
)
type addr struct {
Province string
City string
}
type stu struct {
Name string `json:"name"`
Age int
Addr addr
}
func main(){
var stu01 = stu{Name:"yuan", Age:18, Addr:addr{Province:"Hebei",City:"beijing"}}
// 序列化
json_data, err := json.Marshal(stu01)
if err != nil{
fmt.Println("json解析失败,错误原因:",err)
return
}
fmt.Println(string(json_data))
// 反序列化
var data = `{"name":"yuan","Age":18,"Addr":{"Province":"Hebei","City":"beijing"}}`
var stu02 stu
json.Unmarshal([]byte(data),&stu02)
fmt.Println(stu02)
fmt.Println(stu02.Name)
fmt.Println(stu02.Addr.City)
}
|
十、包管理(go module)
10.1、package
Go语言是使用包来组织源代码的,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案。Go语言中为我们提供了很多内置包,如 fmt、os、io 等。任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是package pacakgeName
语句,通过该语句声明自己所在的包。
Go语言的包借助了目录树的组织形式,一般包的名称就是其源文件所在目录的名称,虽然Go语言没有强制要求包名必须和其所在的目录名同名,但还是建议包名和所在目录同名,这样结构更清晰。
10.1.1、包的基本使用
导入包的语法:
导入包路径规则:
Go 程序首先在 GOROOT/src
目录中寻找包目录,如果没有找到,则会去 GOPATH/src
目录中继续寻找。比如 fmt
包是位于 GOROOT/src
目录的 Go 语言标准库中的一部分,它将会从该目录中导入。
目录结构
1
2
3
4
5
6
7
|
├─mysite
├─api
│ http.go
│ rpc.go
│
└─main
main.go
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// http.go
package api
import "fmt"
func HttpRequest(){
fmt.Println("HttpRequest方法")
}
// rpc.go
package api
import "fmt"
func RpcRequest(){
fmt.Println("RpcRequest方法")
}
// main.go
package main
import "fmt" //标准库包
import "mysite/api"
func main() {
fmt.Println("hi,yuan!")
api.HttpRequest()
api.RpcRequest()
}
|
1、包名一般是小写的,见名知意。
2、包名一般要和所在的目录同名,也可以不同,包名中不能包含-
等特殊符号。
3、包名为 main 的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件。
4、一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下。
5、一个包下的不同文件不能含有同名函数。
6、如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。
7、将mysite
剪切到src
外的任何位置,都会导包失败。
当需要一个外部包时需要下载到本地,命令是
go get "远程包"
go get命令会依赖git clone拉取下载指定的包,并将下载的包进行编译,然后安装到$GOPATH/src
下。
比如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
import (
"fmt"
"mysite/api" // 内部包导入
)
import "github.com/jinzhu/now" // 外部包导入
func main() {
api.HttpRequest()
api.RpcRequest()
fmt.Println(now.BeginningOfMinute())
}
|
远程包介绍:
1
|
go get "github.com/jinzhu/now"
|
git-clone拉取下载存放在gopath/src目录:
然后编译器再执行import "github.com/jinzhu/now"
就可以从 $GOPATH/src 下搜索找到这个包了。
10.1.2、包的导入格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 一次导入多个包
import (
"fmt"
"mysite/api"
)
// 设置包的别名
import F "fmt"
// 省略引用格式
import . "mysite/api"
HttpRequest()
// 匿名导入 :在引用某个包时,如果只是希望执行包初始化的 init 函数,而不使用包内部的数据时,可以使用匿名引用格式
import _ "包名"
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
|
10.1.3、包的加载顺序
init()
函数会在每个包完成初始化后自动执行,并且执行优先级比main函数高。
注意:
1、一个包可以有多个 init 函数,包加载时会执行全部的 init 函数,但并不能保证执行顺序,所以不建议在一个包中放入多个 init 函数,将需要初始化的逻辑放到一个 init 函数里面。
2、包不能出现环形引用的情况,比如包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,则编译不能通过。
3、包的重复引用是允许的,比如包 a 引用了包 b 和包 c,包 b 和包 c 都引用了包 d。这种场景相当于重复引用了 d,这种情况是允许的,并且 Go 编译器保证包 d 的 init 函数只会执行一次。
4、init()
函数没有参数也没有返回值。 init()
函数在程序运行时自动被调用执行,不能在代码中主动调用它。
10.2、go module
module是一个相关Go包的集合,它是源代码更替和版本控制的单元。
10.2.1、Go mod命令
1
2
3
4
5
6
7
8
9
10
|
/*
download download modules to local cache (下载依赖的module到本地cache))
edit edit go.mod from tools or scripts (编辑go.mod文件)
graph print module requirement graph (打印模块依赖图))
init initialize new module in current directory (再当前文件夹下初始化一个新的module, 创建go.mod文件))
tidy add missing and remove unused modules (增加丢失的module,去掉未用的module)
vendor make vendored copy of dependencies (将依赖复制到vendor下)
verify verify dependencies have expected content (校验依赖)
why explain why packages or modules are needed (解释为什么需要依赖)*/
|
Mod Cache 路径默认在$GOPATH/pkg
下面:$GOPATH/pkg/mod
10.2.2、go mod流程
同样是mysite
项目,现在从src
中剪切到其它任何位置,会编译报错。用go module来解决这个问题。
-
(1) 首先将你的版本更新到最新的Go版本(>=1.11)
-
(2)通过go命令行,进入到你当前的工程目录下,在命令行设置临时环境变量set GO111MODULE=on
-
(3)go mod init
在当前目录下生成一个go.mod
文件,执行这条命令时,当前目录不能存在go.mod
文件(有删除)。
1
|
go mod init xxx // xxx即声明的模块名
|
该命令会在当前目录下生成go.mod
文件,文件中声明了模块名称。
以后相关依赖可以声明在这里,就像python的requirements
。
- (4)执行
go mod tidy
命令,它会添加缺失的模块以及移除不需要的模块
执行后会生成go.sum
文件(模块下载条目)。添加参数-v
,例如go mod tidy -v
可以将执行的信息,即删除和添加的包打印到命令行;
将拉取的依赖存放到GOPATH/pkg/mod
路径下。
- (5)执行命令
go mod verify
来检查当前模块的依赖是否全部下载下来,是否下载下来被修改过。如果所有的模块都没有被修改过,那么执行这条命令之后,会打印all modules verified
。
- (6)执行命令
go mod vendor
生成vendor文件夹,该文件夹下将会放置你go.mod
文件描述的依赖包,文件夹下同时还有一个文件modules.txt
,它是你整个工程的所有模块。在执行这条命令之前,如果你工程之前有vendor目录,应该先进行删除。同理go mod vendor -v
会将添加到vendor中的模块打印出来;
内部包调用从初始化模块名go.mod
所在的目录开始查找,所以导入api
包:import "xxx/api"
十一、接口(interface)
11.1、楔子
11.1.1 、多态的含义
在java里,多态是同一个行为具有不同表现形式或形态的能力,即对象多种表现形式的体现,就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
如下图所示:使用手机扫描二维码支付时,二维码并不知道客户是通过何种方式进行支付,只有通过二维码后才能判断是走哪种支付方式执行对应流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
// 支付抽象类或者接口
public class Pay {
public String pay() {
System.out.println("do nothing!")
return "success"
}
}
// 支付宝支付
public class AliPay extends Pay {
@Override
public String pay() {
System.out.println("支付宝pay");
return "success";
}
}
// 微信支付
public class WeixinPay extends Pay {
@Override
public String pay() {
System.out.println("微信Pay");
return "success";
}
}
// 银联支付
public class YinlianPay extends Pay {
@Override
public String pay() {
System.out.println("银联支付");
return "success";
}
}
// 测试支付
public static void main(String[] args) {
// 测试支付宝支付多态应用
Pay pay = new AliPay();
pay.pay();
// 测试微信支付多态应用
pay = new WeixinPay();
pay.pay();
// 测试银联支付多态应用
pay = new YinlianPay();
pay.pay();
}
// 输出结果如下:
支付宝pay
微信Pay
银联支付
|
多态存在的三个必要条件:
比如:
Pay pay = new AliPay();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
11.1.2、抽象类与接口
这样实现当然是可行的,但其实有一个小小的问题,就是Pay类当中的pay方法多余了。因为我们使用的只会是它的子类,并不会用到Pay这个父类。所以我们没必要实现父类Pay中的pay方法,做一个标记,表示有这么一个方法**,子类实现的时候需要实现它就可以了。
这就是抽象类和抽象方法的来源,我们可以把Pay做成一个抽象类,声明pay是一个抽象方法。抽象类是不能直接创建实例的,只能创建子类的实例,并且抽象方法也不用实现,只需要标记好参数和返回就行了。具体的实现都在子类当中进行。说白了抽象方法就是一个标记,告诉编译器凡是继承了这个类的子类必须要实现抽象方法,父类当中的方法不能调用。那抽象类就是含有抽象方法的类。
我们写出Pay变成抽象类之后的代码:
1
2
3
|
public abstract class Pay {
abstract public String pay();
}
|
很简单,因为我们只需要定义方法的参数就可以了,不需要实现方法的功能,方法的功能在子类当中实现。由于我们标记了pay这个方法是一个抽象方法,凡是继承了Pay的子类都必须要实现这个方法,否则一定会报错。
抽象类其实是一个擦边球,我们可以在抽象类中定义抽象的方法也就是只声明不实现,也可以在抽象类中实现具体的方法。在抽象类当中非抽象的方法,子类的实例是可以直接调用的,和子类调用父类的普通方法一样。但假如我们不需要父类实现方法,我们提出提取出来的父类中的所有方法都是抽象的呢?针对这一种情况,Java当中还有一个概念叫做接口,也就是interface,本质上来说interface就是抽象类,只不过是只有抽象方法的抽象类。
所以刚才的Pay通过接口实现如下:
1
2
3
|
interface Pay {
String pay();
}
|
把Pay变成了interface之后,子类的实现没什么太大的差别,只不过将extends关键字换成了implements。另外,子类只能继承一个抽象类,但是可以实现多个接口。早先的Java版本当中,interface只能够定义方法和常量,在Java8以后的版本当中,我们也可以在接口当中实现一些默认方法和静态方法。
接口的好处是很明显的,我们可以用接口的实例来调用所有实现了这个接口的类。也就是说接口和它的实现是一种要宽泛许多的继承关系,大大增加了灵活性。
以上虽然全是Java的内容,但是讲的其实是面向对象的内容,如果没有学过Java的小伙伴可能看起来稍稍有一点点吃力,但总体来说问题不大,没必要细扣当中的语法细节,get到核心精髓就可以了。
11.1.3、Go中的接口实现
Golang
当中也有接口,但是它的理念和使用方法和Java稍稍有所不同,它们的使用场景以及实现的目的是类似的,本质上都是为了抽象。通过接口提取出了一些方法,所有继承了这个接口的类都必然带有这些方法,那么我们通过接口获取这些类的实例就可以使用了,大大增加了灵活性。
但是Java当中的接口有一个很大的问题就是侵入性,Golang
当中的接口解决了这个问题,也就是说它完全拿掉了原本弱化的继承关系,只要接口中定义的方法能对应的上,那么就可以认为这个类实现了这个接口。
我们先来创建一个interface,当然也是通过type关键字:
1
2
3
|
type Pay interface {
pay() string
}
|
我们定义了一个Pay的接口,当中声明了一个pay函数。也就是说只要是拥有这个函数的结构体就可以用这个接口来接收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
type AliPay struct{}
type WeixinPay struct{}
type YinlianPay struct{}
func (a AliPay) pay() {
fmt.Println("支付宝pay")
}
func (w WeixinPay) pay() {
fmt.Println("微信pay")
}
func (y YinlianPay) pay() {
fmt.Println("银联pay")
}
|
之后,我们尝试使用这个接口来接收各种结构体的对象,然后调用它们的pay方法:
1
2
3
4
5
6
7
8
9
|
func main() {
var p Pay
p = AliPay{}
p.pay()
p = WeixinPay{}
p.pay()
p = YinlianPay{}
p.pay()
}
|
出来的结果是一样的!
golang中的接口设计非常出色,因为它解耦了接口和实现类之间的联系,使得进一步增加了我们编码的灵活度,解决了供需关系颠倒的问题。但是世上没有绝对的好坏,golang中的接口在方便了我们编码的同时也带来了一些问题,比如说由于没了接口和实现类的强绑定,其实也一定程度上增加了开发和维护的成本。
总体来说这是一个仁者见仁的改动,有些写惯了Java的同学可能会觉得没有必要,这是过度解绑,有些人之前深受其害的同学可能觉得这个进步非常关键。但不论你怎么看,这都不影响我们学习它,毕竟学习本身是不带立场的。
接口本身就是一种规范,能让大家在一个框架下开发,比如张三新进入部门,开发一个新的支付功能,在接口的限制下,开发就会会规范很多。就像USB接口一样,定义统一接口,无论外部实现的是音响还是硬盘,必须都按定义好的数据格式开发。
11.2、Go的接口语法
11.2.1、基本语法
在 Golang 中,interface 是一组 method 的集合,是 duck-type programming 的一种体现。不关心属性(数据),只关心行为(方法)。具体使用中你可以自定义自己的 struct,并提供特定的 interface 里面的 method 就可以把它当成 interface 来使用。
if something looks like a duck, swims like a duck and quacks like a duck then it’s probably a duck.
每个接口由数个方法组成,接口的定义格式如下:
1
2
3
4
5
|
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
|
其中:
- 接口名:使用
type
将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer
,有字符串功能的接口叫Stringer
等。接口名最好要能突出该接口的类型含义。
- 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
11.2.2、实现接口的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
我们来定义一个Sayer
接口:
1
2
3
4
|
// Sayer 接口
type Sayer interface {
say()
}
|
定义dog
和cat
两个结构体:
1
2
|
type dog struct {}
type cat struct {}
|
因为Sayer
接口里只有一个say
方法,所以我们只需要给dog
和cat
分别实现say
方法就可以实现Sayer
接口了。
1
2
3
4
5
6
7
8
9
|
// dog实现了Sayer接口
func (d dog) say() {
fmt.Println("汪汪汪")
}
// cat实现了Sayer接口
func (c cat) say() {
fmt.Println("喵喵喵")
}
|
接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。
11.2.3、接口类型变量
那实现了接口有什么用呢?
接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer
类型的变量能够存储dog
和cat
类型的变量。
1
2
3
4
5
6
7
8
9
|
func main() {
var x Sayer // 声明一个Sayer类型的变量x
a := cat{} // 实例化一个cat
b := dog{} // 实例化一个dog
x = a // 可以把cat实例直接赋值给x
x.say() // 喵喵喵
x = b // 可以把dog实例直接赋值给x
x.say() // 汪汪汪
}
|
11.2.4、值和指针实现接口的区别
使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。
我们有一个Mover
接口和一个dog
结构体。
1
2
3
4
5
|
type Run interface {
run()
}
type dog struct {}
|
1
2
3
|
func (d dog) run() {
fmt.Println("running...")
}
|
此时实现接口的是dog
类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
type Run interface {
run()
}
type dog struct {}
func main() {
var r Run
var huanhuan = dog{} // huanhuan是dog类型
r = huanhuan // r可以接收dog类型
var xiaohei = &dog{} // xiaohei是*dog类型
r = xiaohei // r可以接收*dog类型
r.run()
}
|
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针xiaohei
内部会自动求值xiaohei
。
同样的代码我们再来测试一下使用指针接收者有什么区别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
type Run interface {
run()
}
type dog struct {}
func (d *dog) run() {
fmt.Println("running...")
}
func main() {
var r Run
var huanhuan = dog{} // huanhuan是dog类型
r = huanhuan // r不可以接收dog类型
var xiaohei = &dog{} // xiaohei是*dog类型
r = xiaohei // r可以接收*dog类型
}
|
此时实现Run接口的是*dog
类型,所以不能给x
传入dog
类型的huanhuan,此时x只能存储*dog
类型的值。
11.2.5、类型与接口的关系
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover
接口。
1
2
3
4
5
6
7
8
9
|
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
|
dog既可以实现Sayer接口,也可以实现Mover接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
type dog struct {
name string
}
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}
func main() {
var x Sayer
var y Mover
var a = dog{name: "旺财"}
x = a
y = a
x.say()
y.move()
}
|
Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover
接口,它要求必须由一个move
方法。
1
2
3
4
|
// Mover 接口
type Mover interface {
move()
}
|
例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
type dog struct {
name string
}
type car struct {
brand string
}
// dog类型实现Mover接口
func (d dog) move() {
fmt.Printf("%s会跑\n", d.name)
}
// car类型实现Mover接口
func (c car) move() {
fmt.Printf("%s速度70迈\n", c.brand)
}
|
这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move
方法就可以了。
1
2
3
4
5
6
7
8
9
|
func main() {
var x Mover
var a = dog{name: "旺财"}
var b = car{brand: "保时捷"}
x = a
x.move()
x = b
x.move()
}
|
上面的代码执行结果如下:
并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
|
11.2.6、接口嵌套
接口与接口间可以通过嵌套创造出新的接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
|
嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
type cat struct {
name string
}
func (c cat) say() {
fmt.Println("喵喵喵")
}
func (c cat) move() {
fmt.Println("猫会动")
}
func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}
|
11.2.7、空接口
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func main() {
// 定义一个空接口x
var x interface{}
s := "Hello 沙河"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}
|
(1)空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
1
2
3
4
|
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
|
(2)空接口作为map的值
使用空接口实现可以保存任意值的字典。
1
2
3
4
5
6
|
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
|
(3)类型断言
一个接口的值(简称接口值)是由一个具体类型
和具体类型的值
两部分组成的。这两部分分别称为接口的动态类型
和动态值
。
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
其中:
- x:表示类型为
interface{}
的变量
- T:表示断言
x
可能是的类型。
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
举个例子:
1
2
3
4
5
6
7
8
9
10
|
func main() {
var x interface{}
x = "Hello Yuan!"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}
|
上面的示例中如果要断言多次就需要写多个if
判断,这个时候我们可以使用switch
语句来实现:
1
2
3
4
5
6
7
8
9
10
11
12
|
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
|
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
十二、反射
12.1.变量的内在机制
12.2. 反射的使用
12.3.空接口与反射
-
反射可以在运行时动态获取程序的各种详细信息
-
反射获取interface类型信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package main
import (
"fmt"
"reflect"
)
// 反射获取interface类型信息
func reflect_type(a interface{}) {
t := reflect.TypeOf(a)
fmt.Println("类型是:", t)
// kind()可以获取具体类型
k := t.Kind()
fmt.Println(k)
switch k {
case reflect.Float64:
fmt.Printf("a is float64\n")
case reflect.String:
fmt.Println("string")
}
}
func main() {
var x float64 = 3.4
reflect_type(x)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import (
"fmt"
"reflect"
)
// 反射获取interface值信息
func reflect_value(a interface{}) {
v := reflect.ValueOf(a)
fmt.Println(v)
k := v.Kind()
fmt.Println(k)
switch k {
case reflect.Float64:
fmt.Println("a是:", v.Float())
}
}
func main() {
var x float64 = 3.4
reflect_value(x)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package main
import (
"fmt"
"reflect"
)
// 反射修改值
func reflect_set_value(a interface{}) {
v := reflect.ValueOf(a)
k := v.Kind()
switch k {
case reflect.Float64:
// 反射修改值
v.SetFloat(6.9)
fmt.Println("a is ", v.Float())
case reflect.Ptr:
// Elem()获取地址指向的值
v.Elem().SetFloat(7.9)
fmt.Println("case:", v.Elem().Float())
// 地址
fmt.Println(v.Pointer())
}
}
func main() {
var x float64 = 3.4
// 反射认为下面是指针类型,不是float类型
reflect_set_value(&x)
fmt.Println("main:", x)
}
|
12.4.结构体与反射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
// 绑方法
func (u User) Hello() {
fmt.Println("Hello")
}
// 传入interface{}
func Poni(o interface{}) {
t := reflect.TypeOf(o)
fmt.Println("类型:", t)
fmt.Println("字符串类型:", t.Name())
// 获取值
v := reflect.ValueOf(o)
fmt.Println(v)
// 可以获取所有属性
// 获取结构体字段个数:t.NumField()
for i := 0; i < t.NumField(); i++ {
// 取每个字段
f := t.Field(i)
fmt.Printf("%s : %v", f.Name, f.Type)
// 获取字段的值信息
// Interface():获取字段对应的值
val := v.Field(i).Interface()
fmt.Println("val :", val)
}
fmt.Println("=================方法====================")
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Println(m.Name)
fmt.Println(m.Type)
}
}
func main() {
u := User{1, "zs", 20}
Poni(u)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
// 匿名字段
type Boy struct {
User
Addr string
}
func main() {
m := Boy{User{1,"zs",20},"bj"}
t := reflect.TypeOf(m)
fmt.Println(t)
// Anonymous:匿名
fmt.Printf("%#v\n",t.Field(0))
// 值信息
fmt.Printf("%#v\n",reflect.ValueOf(m).Field(0))
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
// 修改结构体值
func SetValue(o interface{}) {
v := reflect.ValueOf(o)
// 获取指针指向的元素
v = v.Elem()
// 取字段
f := v.FieldByName("Name")
if f.Kind() == reflect.String {
f.SetString("wangwu")
}
}
func main() {
u := User{1, "zs", 20}
SetValue(&u)
fmt.Println(u)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
func (u User) Hello(name string) {
fmt.Println("Hello:", name)
}
func main() {
u := User{1, "zs", 20}
v := reflect.ValueOf(u)
// 获取方法
m := v.MethodByName("Hello")
// 构建一些参数
args := []reflect.Value{reflect.ValueOf("6666")}
// 没参数的情况下:var args2 []reflect.Value
// 调用方法,需要传入方法的参数
m.Call(args)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string `json:"name1" db:"name2"`
}
func main() {
var s Student
v := reflect.ValueOf(&s)
// 类型
t := v.Type()
// 获取字段
f := t.Elem().Field(0)
fmt.Println(f.Tag.Get("json"))
fmt.Println(f.Tag.Get("db"))
}
|
12.5.反射练习
-
任务:解析如下配置文件
- 序列化:将结构体序列化为配置文件数据并保存到硬盘
- 反序列化:将配置文件内容反序列化到程序的结构体
-
配置文件有server和mysql相关配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#this is comment
;this a comment
;[]表示一个section
[server]
ip = 10.238.2.2
port = 8080
[mysql]
username = root
passwd = admin
database = test
host = 192.168.10.10
port = 8000
timeout = 1.2
|
十三、网络编程
13.1、计算机网络三要素
网络编程:使用编程语言实现多台计算机的通信。
网络编程三大要素。
(1)IP
地址:网络中每一台计算机的唯一标识,通过IP
地址找到指定的计算机。
(2)端口:用于标识进程的逻辑地址,通过端口找到指定进程。
(3)协议:定义通信规则,符合协议则可以通信,不符合不能通信。一般有TCP
协议和UDP
协议。
13.2、socket介绍
13.2.1、什么是 socket?
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。 我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具。
13.2.2、socket有哪些类型?
根据数据的传输方式,可以将 Internet 套接字分成两种类型。通过 socket() 创建连接时,必须告诉它使用哪种数据传输方式。
(1) 流格式套接字(SOCK_STREAM
)
(2) 数据报格式套接字(SOCK_DGRAM
)
13.3、OSI
模型
如果你读过计算机专业,或者学习过网络通信,那你一定听说过 OSI 模型,它曾无数次让你头大。OSI 是 Open System Interconnection 的缩写,译为“开放式系统互联”。 OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
这个网络模型究竟是干什么呢?简而言之就是进行数据封装的。
当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。
13.4、socket在go语言的实现
13.4.1、 基本语法
聊天案例服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
package main
import (
"fmt"
"net"
"strings"
)
func main() {
// 1.创建TCP服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()
// 2.服务端不断等待请求处理
for {
// 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
continue
}
go ClientConn(conn)
}
}
// 处理服务端逻辑
func ClientConn(conn net.Conn) {
defer conn.Close()
// 获取客户端地址
ipAddr := conn.RemoteAddr().String()
fmt.Println(ipAddr, "连接成功")
// 缓冲区
buf := make([]byte, 1024)
for {
// n是读取的长度
n, err := conn.Read(buf)
if err != nil {
fmt.Println(err)
return
}
// 切出有效数据
result := buf[:n]
fmt.Printf("接收到数据,来自[%s] [%d]:%s\n", ipAddr, n, string(result))
// 接收到exit,退出连接
if string(result) == "exit" {
fmt.Println(ipAddr, "退出连接")
return
}
// 回复客户端
conn.Write([]byte(strings.ToUpper(string(result))))
}
}
|
聊天案例客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package main
import (
"fmt"
"net"
)
func main() {
// 1.连接服务端
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
// 缓冲区
buf := make([]byte, 1024)
for {
fmt.Printf("请输入发送的内容:")
fmt.Scan(&buf)
fmt.Printf("发送的内容:%s\n", string(buf))
// 发送数据
conn.Write(buf)
// 接收服务端返回信息
n, err := conn.Read(buf)
if err != nil {
fmt.Println(err)
return
}
result := buf[:n]
fmt.Printf("接收到数据:%s\n", string(result))
}
}
|
13.4.2、 黏包
黏包案例之服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
package main
import (
"fmt"
"net"
"time"
)
func main() {
// 1.创建TCP服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()
for true {
// 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
return
}
go ClientConn(conn)
}
}
// 处理服务端逻辑
func ClientConn(conn net.Conn) {
defer conn.Close()
// 获取客户端地址
ipAddr := conn.RemoteAddr().String()
time.Sleep(time.Second*10)
fmt.Println(ipAddr, "连接成功")
// 缓冲区
buf := make([]byte, 1024)
// n是读取的长度
n, err := conn.Read(buf)
if err != nil {
fmt.Println(err)
return
}
// 切出有效数据
result := buf[:n]
fmt.Printf("接收到数据,来自[%s] [%d]:%s\n", ipAddr, n, string(result))
// 接收到exit,退出连接
if string(result) == "exit" {
fmt.Println(ipAddr, "退出连接")
return
}
// 回复客户端
conn.Write([]byte(string(result)))
}
|
黏包案例之客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
package main
import (
"fmt"
"net"
)
func main() {
// 1.连接服务端
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
// 缓冲区
buf := make([]byte, 1024)
fmt.Printf("请输入发送的内容:")
fmt.Scan(&buf)
fmt.Printf("发送的内容:%s\n", string(buf))
// 发送数据
conn.Write(buf)
conn.Write(buf)
conn.Write(buf)
// 接收服务端返回信息
data := make([]byte, 1024)
n, err := conn.Read(data)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("n",n)
result := data[:n]
fmt.Printf("接收到数据:%s\n", string(result))
}
|
13.4.3、 ssh案例
服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
package main
import (
"fmt"
"github.com/axgle/mahonia"
"net"
"os/exec"
)
func main() {
// 1.创建TCP服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()
for true {
// 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
return
}
go ClientConn(conn)
}
}
// 处理服务端逻辑
func ClientConn(conn net.Conn) {
defer conn.Close()
// 获取客户端地址
ipAddr := conn.RemoteAddr().String()
fmt.Println(ipAddr, "连接成功")
for true {
// 缓冲区
data := make([]byte, 1024)
// n是读取的长度
n, err := conn.Read(data)
fmt.Println("命令字节数",n)
if err != nil {
fmt.Println(err)
return
}
// 切出有效数据
data = data[:n]
fmt.Printf("接收到命令,来自[%s] [%d]:%s\n", ipAddr, n, string(data))
// 接收到exit,退出连接
if string(data) == "exit" {
fmt.Println(ipAddr, "退出连接")
return
}
// 回复客户端
cmd := exec.Command("cmd","/C",string(data))
// 执行命令,并返回结果
output,err := cmd.Output()
if err != nil {
panic(err)
}
fmt.Println("命令结果字节数:",len(output))
dec := mahonia.NewDecoder("gbk")
_, cdata, _ := dec.Translate(output, true)
result := string(cdata)
fmt.Println(result)
conn.Write([]byte(string(result)))
}
}
|
客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
// 1.连接服务端
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
// 缓冲区
for true {
reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
fmt.Println("输入执行命令>>>")
text, _ := reader.ReadString('\n') // 读到换行
text = strings.TrimSpace(text)
fmt.Println("text",text)
// 发送数据
conn.Write([]byte(text))
// 接收服务端返回信息
res := make([]byte, 100000)
n, err := conn.Read(res)
if err != nil {
fmt.Println("err:",err)
return
}
fmt.Println("n",n)
result := res[:n]
fmt.Printf("接收到数据:%s\n", string(result))
}
}
|
十四、并发编程
有人把Go语言比作 21 世纪的C语言,第一是因为Go语言设计简单,第二则是因为 21 世纪最重要的就是并发程序设计,而 Go 从语言层面就支持并发。同时实现了自动垃圾回收机制。Go语言的并发机制运用起来非常简便,在启动并发的方式上直接添加了语言级的关键字就可以实现,和其他编程语言相比更加轻量。
14.1、并发技术
14.1.1、操作系统的进程、线程发展
14.1.2、进程概念
我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。
进程一般由程序、数据集合和进程控制块三部分组成。
- 程序用于描述进程要完成的功能,是控制进程执行的指令集;
- 数据集合是程序在执行时所需要的数据和工作区;
- 程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。
进程具有的特征:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
- 并发性:任何进程都可以同其他进程一起并发执行;
- 独立性:进程是系统进行资源分配和调度的一个独立单位;
- 结构性:进程由程序、数据和进程控制块三部分组成。
14.1.3、线程的概念
在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
14.1.4、任务调度
线程是什么?要理解这个概念,需要先了解一下操作系统的一些相关概念。大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式。
在一个进程中,当一个线程任务执行几毫秒后,会由操作系统的内核(负责管理各个任务)进行调度,通过硬件的计数器中断处理器,让该线程强制暂停并将该线程的寄存器放入内存中,通过查看线程列表决定接下来执行哪一个线程,并从内存中恢复该线程的寄存器,最后恢复该线程的执行,从而去执行下一个任务。
上述过程中,任务执行的那一小段时间叫做时间片,任务正在执行时的状态叫运行状态,被暂停的线程任务状态叫做就绪状态,意为等待下一个属于它的时间片的到来。
这种方式保证了每个线程轮流执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。
14.1.5、进程与线程的区别
前面讲了进程与线程,但可能你还觉得迷糊,感觉他们很类似。的确,进程与线程有着千丝万缕的关系,下面就让我们一起来理一理:
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
14.1.6、线程的生命周期
当线程的数量小于处理器的数量时,线程的并发是真正的并发,不同的线程运行在不同的处理器上。但当线程的数量大于处理器的数量时,线程的并发会受到一些阻碍,此时并不是真正的并发,因为此时至少有一个处理器会运行多个线程。
在单个处理器运行多个线程时,并发是一种模拟出来的状态。操作系统采用时间片轮转的方式轮流执行每一个线程。现在,几乎所有的现代操作系统采用的都是时间片轮转的抢占式调度方式,如我们熟悉的Unix、Linux、Windows及macOS等流行的操作系统。
我们知道线程是程序执行的最小单位,也是任务执行的最小单位。在早期只有进程的操作系统中,进程有五种状态,创建、就绪、运行、阻塞(等待)、退出。早期的进程相当于现在的只有单个线程的进程,那么现在的多线程也有五种状态,现在的多线程的生命周期与早期进程的生命周期类似。
线程的生命周期
创建:一个新的线程被创建,等待该线程被调用执行;
就绪:时间片已用完,此线程被强制暂停,等待下一个属于它的时间片到来;
运行:此线程正在执行,正在占用时间片;
阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;
退出:一个线程完成任务或者其他终止条件发生,该线程终止进入退出状态,退出状态释放该线程所分配的资源。
14.1.7、协程(Coroutines)
协程,英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。因为是自主开辟的异步任务,所以很多人也更喜欢叫它们纤程(Fiber),或者绿色线程(GreenThread)。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
协程解决的是线程的切换开销和内存开销的问题
1
2
3
|
* 用户空间 首先是在用户空间, 避免内核态和用户态的切换导致的成本。
* 由语言或者框架层调度
* 更小的栈空间允许创建大量实例(百万级别)
|
14.1.8、三种线程模型
无论语言层面何种并发模型,到了操作系统层面,一定是以线程的形态存在的。而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间呢就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。我们现在的计算机语言,可以狭义的认为是一种“软件”,它们中所谓的“线程”,往往是用户态的线程,和操作系统本身内核态的线程(简称KSE),还是有区别的。
一、用户级线程模型(M : 1)
将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成。此模式中,用户级线程对操作系统不可见(即透明)。
优点: 这种模型的好处是线程上下文切换都发生在用户空间,避免的模态切换(mode switch),从而对于性能有积极的影响。
缺点:
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
二、内核级线程模型(1:1)
将每个用户级线程映射到一个内核级线程。
每个线程由内核调度器独立的调度,所以如果一个线程阻塞则不影响其他的线程。 优点:在多核处理器的硬件的支持下,内核空间线程模型支持了真正的并行,当一个线程被阻塞后,允许另一个线程继续执行,所以并发能力较强。
缺点:每创建一个用户级线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能。
三、两级线程模型(M:N)
内核线程和用户线程的数量比为 M : N,内核用户空间综合了前两种的优点。
一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程,自身的用户级线程需要本身程序去调度,内核级的线程交给操作系统内核去调度。这使得大部分的线程上下文切换都发生在用户空间,而多个内核线程又可以充分利用处理器资源。
Go语言的线程模型就是一种特殊的两级线程模型(GPM调度模型)。
14.2、goroutine的基本使用
14.2.1、goroutine的基本语法
goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package main
import (
"fmt"
"time"
)
func foo() {
fmt.Println("foo")
time.Sleep(time.Second)
fmt.Println("foo end")
}
func bar() {
fmt.Println("bar")
time.Sleep(time.Second*2)
fmt.Println("bar end")
}
func main() {
go foo()
bar()
}
|
14.2.2、sync.WaitGroup
Go语言中可以使用sync.WaitGroup
来实现并发任务的同步。 sync.WaitGroup
有以下几个方法:
方法名 | 功能 |
(wg * WaitGroup) Add(delta int) |
计数器+delta |
(wg *WaitGroup) Done() |
计数器-1 |
(wg *WaitGroup) Wait() |
阻塞直到计数器变为0 |
sync.WaitGroup
内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func foo() {
defer wg.Done()
fmt.Println("foo")
time.Sleep(time.Second)
fmt.Println("foo end")
}
func bar() {
defer wg.Done()
fmt.Println("bar")
time.Sleep(time.Second*2)
fmt.Println("bar end")
}
func main() {
start:=time.Now()
wg.Add(2)
go foo()
go bar()
wg.Wait()
fmt.Println("程序结束,运行时间为",time.Now().Sub(start))
}
|
14.2.3、GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS
参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()
函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
package main
import (
"fmt"
"runtime"
"sync"
)
var wg sync.WaitGroup
func foo() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
//time.Sleep(time.Millisecond*20)
}
wg.Done()
}
func bar() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
//time.Sleep(time.Millisecond*30)
}
wg.Done()
}
func main() {
wg.Add(2)
fmt.Println(runtime.NumCPU())
runtime.GOMAXPROCS(1)// 改为4
go foo()
go bar()
wg.Wait()
}
|
14.3、GPM调度器
GPM是GO语言运行时(runtime)层面得实现,是go语言自己实现得一套调度系统 区别于操作系统调度得OS线程
M
指的是Machine
,一个M
直接关联了一个内核线程。由操作系统管理。 P
指的是”processor”,代表了M
所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。 G
指的是Goroutine
,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。
P的数量由环境变量中的GOMAXPROCS
决定,通常来说它是和核心数对应,例如在4Core的服务器上回启动4个线程。G会有很多个,每个P会将Goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争,通常情况下每个P会负责一个队列。
Goroutine调度策略
每次调用go的时候,都会:
1
2
3
4
5
6
7
|
/*
A、创建一个G对象,加入到本地队列或者全局队列
B、如果有空闲的P,则创建一个M
C、M会启动一个底层线程,循环执行能找到的G任务
D、G任务的执行顺序是先从本地队列找,本地没有则从全局队列找(一次性转移(全局G个数/P个数)个,再去其它P中找(一次性转移一半)。
E、G任务执行是按照队列顺序(即调用go的顺序)执行的。
*/
|
创建一个M过程如下:
1
2
3
4
5
|
/*
A、先找到一个空闲的P,如果没有则直接返回。
B、调用系统API创建线程,不同的操作系统调用方法不一样。
C、 在创建的线程里循环执行G任务
*/
|
如果一个系统调用或者G任务执行太长,会一直占用内核空间线程,由于本地队列的G任务是顺序执行的,其它G任务就会阻塞。因此,Go程序启动的时候,会专门创建一个线程sysmon,用来监控和管理,sysmon内部是一个循环:
1
2
3
4
5
6
|
/*
A、记录所有P的G任务计数schedtick,schedtick会在每执行一个G任务后递增。
B、如果检查到 schedtick一直没有递增,说明P一直在执行同一个G任务,如果超过一定的时间(10ms),在G任务的栈信息里面加一个标记。
C、G任务在执行的时候,如果遇到非内联函数调用,就会检查一次标记,然后中断自己,把自己加到队列末尾,执行下一个G。
D、如果没有遇到非内联函数(有时候正常的小函数会被优化成内联函数)调用,会一直执行G任务,直到goroutine自己结束;如果goroutine是死循环,并且GOMAXPROCS=1,阻塞。
*/
|
(1) 局部优先调度
(2) steal working
(3) 阻塞调度
(4) 抢占式调度
14.4、数据安全与锁
14.4.1、互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine
可以访问共享资源。Go语言中使用sync
包的Mutex
类型来实现互斥锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var lock sync.Mutex
var x = 0
func add() {
//lock.Lock()
x++
//lock.Unlock()
wg.Done()
}
func main() {
wg.Add(1000)
for i:=0;i<1000 ;i++ {
go add()
}
wg.Wait()
fmt.Println(x)
}
|
使用互斥锁能够保证同一时间有且只有一个goroutine
进入临界区,其他的goroutine
则在等待锁;当互斥锁释放后,等待的goroutine
才可以获取锁进入临界区,多个goroutine
同时等待一个锁时,唤醒的策略是随机的。
14.4.2、读写锁
在读多写少的环境中,可以优先使用读写互斥锁(sync.RWMutex),它比互斥锁更加高效。sync 包中的 RWMutex 提供了读写互斥锁的封装。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
package main
import (
"fmt"
"sync"
"time"
)
// 效率对比
// 声明读写锁
var rwlock sync.RWMutex
var mutex sync.Mutex
var wg sync.WaitGroup
// 全局变量
var x int
// 写数据
func write() {
//mutex.Lock()
rwlock.Lock()
x += 1
fmt.Println("x",x)
time.Sleep(10 * time.Millisecond)
//mutex.Unlock()
rwlock.Unlock()
wg.Done()
}
func read(i int) {
//mutex.Lock()
rwlock.RLock()
time.Sleep(time.Millisecond)
fmt.Println(x)
//mutex.Unlock()
rwlock.RUnlock()
wg.Done()
}
// 互斥锁执行时间:18533117400
// 读写锁执行时间:1312065700
func main() {
start := time.Now()
wg.Add(1)
go write()
for i := 0; i < 1000; i++ {
wg.Add(1)
go read(i)
}
wg.Wait()
fmt.Println("运行时间:", time.Now().Sub(start))
}
|
14.4.3、map锁
Go语言中内置的map不是并发安全的.
并发读是安全的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package main
import (
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
m := make(map[int]int)
// 添一些假数据
for i := 0; i < 5; i++ {
m[i] = i*i
}
// 遍历打印
for i := 0; i < 5; i++ {
wg.Add(1)
go func(x int) {
fmt.Println(m[x], "\t")
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(m)
}
|
并发写则不安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package main
import (
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
//m := make(map[int]int)
var m = sync.Map{}
// 并发写
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
m.Store(i,i*i)
}(i)
}
wg.Wait()
fmt.Println(m.Load(1))
fmt.Println(m.Load(2))
}
|
14.4.4、原子性操作
AddXxx():加减操作
CompareAndSwapXxx():比较并交换
LoadXxx():读取操作
StoreXxx():写入操作
SwapXxx():交换操作
原子操作与互斥锁性能对比:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// 效率对比
// 原子操作需要接收int32或int64
var x int32
var wg sync.WaitGroup
var mutex sync.Mutex
// 互斥锁操作
func add1() {
for i := 0; i < 500; i++ {
mutex.Lock()
x += 1
mutex.Unlock()
}
wg.Done()
}
// 原子操作
func add2() {
for i := 0; i < 500; i++ {
atomic.AddInt32(&x, 1)
}
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10000; i++ {
wg.Add(1)
go add1()
//go add2()
}
wg.Wait()
fmt.Println("x:", x)
fmt.Println("执行时间:", time.Now().Sub(start))
}
|
14.5、channel(管道)
如果说 goroutine 是 Go语言程序的并发体的话,那么 channels 就是它们之间的通信机制。一个 channels 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。
在地铁站、食堂、洗手间等公共场所人很多的情况下,大家养成了排队的习惯,目的也是避免拥挤、插队导致的低效的资源使用和交换过程。代码与数据也是如此,多个 goroutine 为了争抢数据,势必造成执行的低效率,使用队列的方式是最高效的,channel 就是一种队列一样的结构。
Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
14.5.1、声明创建通道
声明通道类型
通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:
- 通道类型:通道内的数据类型。
- 通道变量:保存通道的变量。
chan 类型的空值是 nil,声明后需要配合 make 后才能使用。
创建通道
通道是引用类型,需要使用 make 进行创建,格式如下:
1
|
通道实例 := make(chan 数据类型)
|
- 数据类型:通道内传输的元素类型。
- 通道实例:通过make创建的通道句柄。
14.5.1、channel基本操作
1
2
3
4
5
6
|
通道实例 := make(chan 数据类型)
// 向通道发送数据
通道实例 <- 值
// 从通道接收数据
<- 通道实例
|
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// 创建一个空接口通道
ch := make(chan int,1)
// 将0放入通道中
ch <- 0
// 将1放入通道中
ch <- 1
// 取值
fmt.Println(<-ch)
fmt.Println(<-ch)
// fmt.Println(<-ch)
// 管道存放任意数据类型
ch := make(chan interface{},10)
ch<-10
ch<-"hi"
ch<-Stu{"alvin",18}
<-ch
<-ch
s1:=<-ch
s:=s1.(Stu)
fmt.Printf("%#v\n",s1)
fmt.Println(s.Name)
|
14.5.2、管道的循环与关闭
当向通道中发送完数据时,我们可以通过close
函数来关闭通道。
关闭 channel 非常简单,直接使用Go语言内置的 close() 函数即可,关闭后的通道只可读不可写。
在介绍了如何关闭 channel 之后,我们就多了一个问题:如何判断一个 channel 是否已经被关闭?我们可以在读取的时候使用多重返回值的方式:
这个用法与 map 中的按键获取 value 的过程比较类似,只需要看第二个 bool 返回值即可,如果返回值是 false 则表示 ch 已经被关闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
ch := make(chan interface{},10)
ch<-10
ch<-"hi"
ch<-Stu{"alvin",18}
// fmt.Println("第一个值:",<-ch)
close(ch)
// 遍历时必须管道已经关闭
for i:=range ch{
fmt.Println(i)
}
fmt.Println("range循环后:",<-ch)
// // 思考结果?
//for i:=0;i<len(ch) ;i++ {
// fmt.Println(<-ch)
//}
|
14.5.3、缓冲通道
- 无缓冲的通道是指在接收前没有能力保存任何值的通道
- 有缓冲的通道是一种在被接收前能存储一个或者多个值的通道
无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:
1
2
3
4
5
|
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
|
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
1
2
3
4
5
|
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../src/github.com/Q1mi/studygo/day06/channel02/main.go:8 +0x54
|
为什么会出现deadlock
错误呢?
因为我们使用ch := make(chan int)
创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
1
2
3
4
5
6
7
8
9
10
|
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
|
无缓冲通道上的发送操作会阻塞,直到另一个goroutine
在该通道上执行接收操作,这时值才能发送成功,两个goroutine
将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine
在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine
同步化。因此,无缓冲通道也被称为同步通道
。
14.5.4、单向通道
Go语言的类型系统提供了单方向的 channel 类型,顾名思义,单向 channel 就是只能用于写入或者只能用于读取数据。当然 channel 本身必然是同时支持读写的,否则根本没法用。
我们在将一个 channel 变量传递到一个函数时,可以通过将其指定为单向 channel 变量,从而限制该函数中可以对此 channel 的操作,比如只能往这个 channel 中写入数据,或者只能从这个 channel 读取数据。
单向 channel 变量的声明非常简单,只能写入数据的通道类型为chan<-
,只能读取数据的通道类型为<-chan
,格式如下:
1
2
|
var 通道实例 chan<- 元素类型 // 只能写入数据的通道
var 通道实例 <-chan 元素类型 // 只能读取数据的通道
|
- 元素类型:通道包含的元素类型。
- 通道实例:声明的通道变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
package main
import (
"fmt"
)
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
|
14.5.5、select语句
golang中的select语句格式如下
1
2
3
4
5
6
7
8
9
|
select {
case <-ch1:
// 如果从 ch1 信道成功接收数据,则执行该分支代码
case ch2 <- 1:
// 如果成功向 ch2 信道成功发送数据,则执行该分支代码
default:
// 如果上面都没有成功,则进入 default 分支处理流程
}
|
可以看到select的语法结构有点类似于switch,但又有些不同。select里的case后面并不带判断条件,而是一个信道的操作,不同于switch里的case,对于从其它语言转过来的开发者来说有些需要特别注意的地方。golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作。
注:Go 语言的 select
语句借鉴自 Unix 的 select()
函数,在 Unix 中,可以通过调用 select()
函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了 IO 动作,该 select()
调用就会被返回(C 语言中就是这么做的),后来该机制也被用于实现高并发的 Socket 服务器程序。Go 语言直接在语言级别支持 select
关键字,用于处理并发编程中通道之间异步 IO 通信问题。
注意:如果 ch1
或者 ch2
信道都阻塞的话,就会立即进入 default
分支,并不会阻塞。但是如果没有 default
语句,则会阻塞直到某个信道操作成功为止。
- select语句只能用于信道的读写操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package main
import "fmt"
func main() {
size := 10
ch := make(chan int, size)
for i := 0; i < size; i++ {
ch <- 1
}
ch2 := make(chan int, size)
for i := 0; i < size; i++ {
ch2 <- 2
}
select {
case ch<-3:
fmt.Print("插值OK")
case b := <-ch2:
fmt.Print(b)
default:
fmt.Println("none")
}
}
|
- select中的case语句是随机执行的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package main
import "fmt"
func main() {
size := 10
ch := make(chan int, size)
for i := 0; i < size; i++ {
ch <- 1
}
ch2 := make(chan int, size)
for i := 0; i < size; i++ {
ch2 <- 2
}
ch3 := make(chan int, 1)
select {
case v := <-ch:
fmt.Print(v)
case b := <-ch2:
fmt.Print(b)
case ch3 <- 10:
fmt.Print("write")
default:
fmt.Println("none")
}
}
|
3.超时用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func(c chan int) {
// 修改时间后,再查看执行结果
time.Sleep(time.Second * 3)
ch <- 1
}(ch)
select {
case v := <-ch:
fmt.Print(v)
case <-time.After(2 * time.Second): // 等待 2s
fmt.Println("no case ok")
}
}
|
4.空select
空select
指的是内部不包含任何case
,例如:
空的 select
语句会直接阻塞当前的goroutine
,使得该goroutine
进入无法被唤醒的永久休眠状态。
14.6、并发案例
14.6.1、聊天室案例
服务器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
package main
import (
"fmt"
"net"
"strings"
)
//保存用户信息的结构体
type Client struct {
C chan string //传递用户数据
Name string //用户名(默认与IP地址相同)
Addr string //客户端的IP
}
var onlineClients = make(map[string]Client) //保存所有用户 {"ip":{"name":"","c":c}}
var message = make(chan string) //传递用户消息
//监听Message通道中的数据
func Manager() {
for {
msg := <-message //读取message通道中的数据,如果通道中没有数据,就会一直等待。
fmt.Println("msg",msg)
for _, client := range onlineClients {
fmt.Println("监听:",)
client.C <- msg
}
}
}
func read(conn net.Conn){
for true{
data:=make([]byte,1024)
n, _ :=conn.Read(data)
fmt.Println(string(data[:n]))
s:=strings.TrimSpace(string(data[:n]))
message<-s
}
}
//处理客户端的连接
func HandleConnect(conn net.Conn) {
//把客户端的用户信息保存在map对象
addr := conn.RemoteAddr().String() //获取客户端的IP
fmt.Println("来自客户端的连接")
//把用户信息封装成Client
client := Client{make(chan string), addr, addr}
onlineClients[addr] = client
//向所有用户广播消息
msg := "[" + client.Addr + "]" + client.Name + ": login"
message <- msg
//启动WriteMsgToClient的Go程
go read(conn)
go WriteMsgToClient(conn, client)
}
//负责向指定的用户发送消息
func WriteMsgToClient(conn net.Conn, client Client) {
for {
msg := <- client.C //读取C通道中的数据,如果没有数据,就会一直等待
conn.Write([]byte(msg + "\n")) //把数据输出到客户端
}
}
//主协程
func main() {
fmt.Println("聊天室服务端启动了...")
//创建一个监听器
listener, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("net.Listen err: ", err)
return //结束主协程
}
//负责监听Message通道中的数据
go Manager()
for {
conn, err := listener.Accept() //阻塞方法,监听客户端的连接
if err != nil {
fmt.Println("listener.Accept err: ", err)
continue //结束当次循环
}
go HandleConnect(conn)
}
}
|
十五、程序调试
15.1、单元测试
15.2、压力测试
十六、web开发
16.1、http协议
16.1.1 、简介
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于万维网(WWW:World Wide Web )服务器与本地浏览器之间传输超文本的传送协议。HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。
16.1.2、 http协议特性
(1) 基于TCP/IP协议
http协议是基于TCP/IP协议之上的应用层协议。
(2) 基于请求-响应模式
HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并 返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有 接收到请求之前不会发送响应
(3) 无状态保存
HTTP是一种不保存状态,即无状态(stateless)协议。HTTP协议 自身不对请求和响应之间的通信状态进行保存。也就是说在HTTP这个 级别,协议对于发送过的请求或响应都不做持久化处理。
使用HTTP协议,每当有新的请求发送时,就会有对应的新响应产 生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把HTTP协议设计成 如此简单的。
可是,随着Web的不断发展,因无状态而导致业务处理变得棘手 的情况增多了。比如,用户登录到一家购物网站,即使他跳转到该站的 其他页面后,也需要能继续保持登录状态。针对这个实例,网站为了能 够掌握是谁送出的请求,需要保存用户的状态。HTTP/1.1虽然是无状态协议,但为了实现期望的保持状态功能, 于是引入了Cookie技术。有了Cookie再用HTTP协议通信,就可以管 理状态了。有关Cookie的详细内容稍后讲解。
(4) 无连接
无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
16.1.3 、http请求协议与响应协议
http协议包含由浏览器发送数据到服务器需要遵循的请求协议与服务器发送数据到浏览器需要遵循的请求协议。用于HTTP协议交互的信被为HTTP报文。请求端(客户端)的HTTP报文 做请求报文,响应端(服务器端)的 做响应报文。HTTP报文本身是由多行数据构成的字文本。
(1) 请求协议
请求方式: get与post请求
- GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditBook?name=test1&id=123456. POST方法是把提交的数据放在HTTP包的请求体中.
- GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制
(2) 响应协议
响应状态码:状态码的职 是当客户端向服务器端发送请求时, 返回的请求 结果。借助状态码,用户可以知道服务器端是正常 理了请求,还是出 现了 。状态码如200 OK,以3位数字和原因 成。数字中的 一位指定了响应 别,后两位无分 。响应 别有以5种。
图片来自于《图解HTTP》
16.2、基于net库的web应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package main
import (
"fmt"
"net"
)
func main() {
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()
// 2.服务端不断等待请求处理
for {
// 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
continue
}
buf := make([]byte, 1024)
n, err := conn.Read(buf)
fmt.Println("n",n)
conn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n<h1>Welcome to Web World!</h1>"))
}
}
|
16.3、基于http库的web应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import (
"fmt"
"net/http"
)
func foo (w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello Yuan")
}
func main() {
http.HandleFunc("/hi", foo)
http.ListenAndServe(":8090", nil)
}
|