Go语言开篇介绍
Go语言 是Google公司 在2007开发一种静态强类型、编译型语言,并在 2009 年正式对外发布。
Go语言以其近C的执行性能和近解析型语言的开发效率,以及近乎于完美的编译速度,已经风靡全球。很多人将Go语言称为21世纪的C语言,因为Go不仅拥有C的简洁和性能,而且针对多处理器系统应用程序的编程进行了优化,很好的提供了21世纪互联网环境下服务端开发的各种实用特性。
Go语言的诞生
事件起源于2007年9月,当时 C++委员会正在 Google 对 C++ 语言新增加的35个新的特性进行一场分享演讲。
Google 的技术大神们也在认真听讲座,其中就有Go语言的三个创作者,分别是: Robert Griesemer(罗伯特.格利茨默),Rob Pike(罗伯.派克),Ken Thompson(肯.汤普森)。
-
Rob Pike(罗伯.派克)
罗布·派克是Unix的先驱,是贝尔实验室最早和Ken Thompson以及 Dennis M. Ritche(C语言之父) 开发Unix的猛人,UTF-8的设计人。让人佩服不已的是,罗伯伯还是1980年奥运会射箭的银牌得主。
-
Ken Thompson(肯.汤普森)
Ken Thompson,C语言前身B语言的作者,与Dennis Ritchie是Unix的原创者。1983年图灵奖得主以及1998年美国国家技术奖(National Medal of Technology)得主。
-
Robert Griesemer(罗伯特.格利茨默)
参与制作了Java的HotSpot编译器以及Chrome浏览器的javascript的搜索引擎V8。
随着会议的中场休息,大家开始了对这些 C++ 语言新特性是否带来更多的价值进行吐槽。Rob Pike他们认为:简化语言的成就远大于添加功能。于是,一门新的语言,Go,在这个思路下应运而生。
2007 年 9 月 25 号,Rob Pike在回家的路上得到关于新语言名字的灵感,于是给另外两人发了邮件:
邮件正文大意为:
在开车回家的路上我得到了些灵感。
1.给这门编程语言取名为“go”,它很简短,易书写。工具类可以命名为:goc、 gol、goa。
交互式的调试工具也可以直接命名为“go”。语言文件后缀名为 .go 等等
这就是 Go 语言名字的来源,自此之后 Robert、Rob 和 Ken 三个人开始在 Google 内部进行了研发,一直到了 2009 年,Go 正式开源了,Go 项目团队将 2009 年 11 月 10 日,即该语言正式对外开源的日字作为其官方生日。源代码最初托管在 http://code.google.com 上,之后几年才逐步的迁移到 GitHub 上。
Go语言的版本
Go 1.0 — 2012 年 3 月:Go 的第一个版本,带着一份兼容性说明文档来保证与未来发布版本的兼容性,进而不会破坏已有的程序。
Go 1.1 — 2013 年 5 月:这个 Go 版本专注于优化语言(编译器,gc,map,go 调度器)和提升它的性能。
Go 1.3 — 2014 年 6 月:这个版本对栈管理做了重要的改进。栈可以申请[连续的内存片段,提高了分配的效率
Go 1.4 — 2014 年 12 月:此版本带来了官方对 Android 的支持,让我们可以只用 Go 代码就能写出简单的 Android 程序。
Go 1.7 — 2016 年 8 月: 这个版本发布了context 包,为用户提供了处理超时和任务取消的方法。
Go 1.11 — 2018 年 8 月: Go 1.11 带来了一个重要的新功能:Go modules。
Go语言的优势
Go语言的应用领域
使用Go的大型互联网公司
Go语言的强项在于它适合用来开发网络并发方面的服务,比如消息推送、监控、容器等,所以在高并发的项目上大多数公司会优先选择 Golang 作为开发语言。另外一个应用就是对一些python,php或者java项目进行重构。
介绍完Go语言,那么接下来我们就可以正式进入Golang的学习了。
一、计算机基础
计算机(computer)俗称电脑,是现代一种用于高速计算的电子计算机器,可以进行数值计算,又可以进行逻辑计算,还具有存储记忆功能。是能够按照程序运行,自动、高速处理海量数据的现代化智能电子设备。
计算机是20世纪最先进的科学技术发明之一,对人类的生产活动和社会活动产生了极其重要的影响。它的应用领域从最初的军事科研应用扩展到社会的各个领域,已形成了规模巨大的计算机产业,带动了全球范围的技术进步,由此引发了深刻的社会变革。
1.1、计算机硬件组成
其中,CPU包括运算器和控制器,相当于计算机的大脑,是计算机的运算核心和控制核心。
(1) 运算器是用来进行数据运算加工的。
(2) 控制器是是计算机的指挥中心,负责决定执行程序的顺序,给出执行指令时机器各部件所需要的操作控制命令,用于协调和控制计算机的运行。
储存器可分为内储存器和外储存器两部分:内存属于内储存器,内存是CPU与硬盘之间的桥梁,只负责在CPU与硬盘之间做数据预存加速的功能。断电后即会被清除。输入设备的数据是从设备接口进去到端口缓冲器的,再经主板的输入输出总线(I/O总线)直接到CPU的,不经过内存。
外储存器是指除计算机内存及CPU缓存以外的储存器,此类储存器一般断电后仍然能保存数据。常见的外存储器有硬盘、软盘、光盘、U盘等。
输入设备就是键盘、鼠标、麦克风、扫描仪等等,向电脑输入信息。输入设备则相反,电脑向外部输出信息,如显示器、打印、音像、写入外存等。
1.2、冯-诺伊曼计算机
提到计算机,就不得不提及在计算机的发展史上做出杰出贡献的著名应用数学家冯诺依曼(Von Neumann,是他带领专家提出了一个全新的存储程序的通用电子计算机方案。这个方案规定了新机器由5个部分组成:运算器、逻辑控制装置、存储器、输入和输出。并描述了这5个部分的职能和相互关系。
早期的ENIAC有一个致命的缺点就是程序与计算两分离。在埃历阿克ENIAC尚未投入运行前,冯·诺依曼就已开始着手起草一份新的设计报告,要对这台电子计算机进行脱胎换骨的改造。他把新机器的方案命名为“离散变量自动电子计算机”,英文缩写译音是“埃德瓦克”(EDVAC)。1945年6月,冯·诺依曼与戈德斯坦、勃克斯等人,为埃德瓦克方案联名发表了一篇长达101页纸洋洋万言的报告,即计算机史上著名的“101页报告”。这份报告奠定了现代电脑体系结构坚实的根基,直到今天,仍然被认为是现代电脑科学发展里程碑式的文献。报告明确规定出计算机的五大部件**(输入系统、输出系统、存储器、运算器、控制器),并用二进制替代十进制运算**,大大方便了机器的电路设计。埃德瓦克方案的革命意义在于**“存储程序”**──程序也被当作数据存进了机器内部,以便电脑能自动依次执行指令,再也不必去接通什么线路。
人们后来把根据这一方案思想设计的机器统称为“诺依曼机”。自冯·诺依曼设计的埃德瓦克始,直到今天我们用“奔腾”芯片制作的多媒体计算机为止,电脑一代又一代的“传人”,大大小小千千万万台计算机,都没能够跳出诺依曼机的掌心。在这个意义上,冯·诺依曼是当之无愧的“计算机之父”。而
总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束, 按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。总线是一种内部结构,它是cpu
、内存、输入、输出设备传递信息的公用通道,主机的各个部件通过总线相连接,外部设备通过相应的接口电路再与总线相连接,从而形成了计算机硬件系统。
二、编程语言介绍
2.1、什么是编程语言
编程语言是用来控制计算机的一系列指令(Instruction),它有固定的格式和词汇(不同编程语言的格式和词汇不一样)。就像我们中国人之间沟通需要汉语,英国人沟通需要英语一样,人与计算机之间进行沟通需要一门语言作为介质,即编程语言。
编程语言的发展经历了机器语言(指令系统)=>汇编语言=>高级语言(C、java、Go
等)。
010010101001-》ADD
- 计算机在设计中规定了一组指令(二级制代码),这组指令的集和就是所谓的机器指令系统,用机器指令形式编写的程序称为机器语言。
- 但由于机器语言的千上万条指令难以记忆,并且维护性和移植性都很差,所以在机器语言的基础上,人们提出了采用字符和十进制数代替二进制代码,于是产生了将机器语言符号化的汇编语言。
- 虽然汇编语言相较于机器语言简单了很多,但是汇编语言是机器指令的符号化,与机器指令存在着直接的对应关系,无论是学习还是开发,难度依然很大。所以更加接近人类语言,也更容易理解和修改的高级语言就应运而生了,高级语言的一条语法往往可以代替几条、几十条甚至几百条汇编语言的指令。因此,高级语言易学易用,通用性强,应用广泛。
2.2、编译型语言与解释性语言
计算机是不能理解高级语言的,更不能直接执行高级语言,它只能直接理解机器语言,所以使用任何高级语言编写的程序若想被计算机运行,都必须将其转换成计算机语言,也就是机器码。而这种转换的方式分为编译和解释两种。由此高级语言也分为编译型语言和解释型语言。
使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。
编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe
格式的文件,以后要再运行时,直接使用编译结果即可,如直接运行exe
文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。
1、一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高;
2、与特定平台相关,一般无法移植到其他平台;
使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。
1.解释型语言每次运行都需要将源代码解释称机器码并执行,执行效率低;
2.只要平台提供相应的解释器,就可以运行源代码,所以可以方便源程序移植;
三、Go环境安装
3.1、Go编译器的下载
1
2
|
-- 官网:https://golang.google.cn/
-- go中文网:https://studygolang.com/dl
|
3.2、安装 for Mac
3.2.1、 mac系统下安装SDK
安装完成以后可以使用终端软件(例如iTerm)中输入go version查看Go编译器的版本信息
mac系统下会默认安装到GOROOT="/usr/local/go"
中,通过go env可以查看
3.2.2、 mac系统下配置GOPATH
开发包安装完成后,我们还需要配置一下GOPATH 环境变量,之后才可以使用Go语言进行开发。GOPATH是开发人员编写Go程序的工作空间路径,也就是存放Go代码的地方
在终端中运行 vi ~/.bash_profile
添加下面这行代码
1
|
export GOPATH=$HOME/goWork
|
保存然后退出你的编辑器。然后在终端中运行下面命令
提示:$HOME 是每个电脑下的用户主目录,每个电脑可能不同,可以在终端运行 echo $HOME 获取
然后保存并退出编辑器,运行 source ~/.bash_profile 命令即可。
3.3 安装 for Window
3.3.1、window系统下安装SDK
双击我们下载好的Go语言开发包即可启动安装程序,如下图所示,这是Go语言的用户许可协议,无需管它,直接勾选“I accept …”然后点击“Next”即可。
在 Windows 系统下Go语言开发包会默认安装到 C 盘的 Go 目录下,推荐在这个目录下安装,使用起来较为方便。当然,你也可以选择其他的安装目录,确认无误后点击“Next”,如下图所示:
Go语言开发包的安装没有其他需要设置的选项,点击“Install”即可开始安装,如下图所示:
等待程序完成安装,然后点击“Finish”退出安装程序。
安装完成后,在我们所设置的安装目录下将生成一些目录和文件,如下图所示:
在默认情况下,win系统下Go 将会被安装在目录 c:\go 下,但如果你在安装过程中修改安装目录,则需要手动修改所有的环境变量的值。
通过go env命令可以查看环境变量的所有情况。值得一提的是,GOROOT 表示 Go 开发包的安装目录。
国内Go语言库镜像:https://github.com/goproxy/goproxy.cn 在终端输入:go env -w GOPROXY=https://goproxy.cn,direct
对代理进行修改。
GOPROXY https://proxy.golang.org,direct
阿里云: export GOPROXY=https://mirrors.aliyun.com/goproxy/
七牛云: export GOPROXY= https://goproxy.cn
go env -w “GO111MODULE=off” // 关闭go mod
3.3.2、window系统下配置GOPATH
GOPATH 是 Go语言中使用的一个环境变量,它使用绝对路径提供项目的工作目录(workspace)。
GOPATH下创建src文件夹,即存放Go项目代码的位置。
开发包安装完成后,我们还需要配置一下GOPATH 环境变量,之后才可以使用Go语言进行开发。GOPATH是开发人员编写Go程序的工作空间路径,也就是存放Go代码的地方。
在桌面或者资源管理器右键“此电脑”(或者“我的电脑”)→“属性”→“高级系统设置”→“环境变量”,如下图所示。
在弹出的菜单里找到 GOPATH 对应的选项点击编辑之后就可以修改了,没有的话可以选择新建,并将变量名填写为 GOPATH,变量值设置为任意目录均可(尽量选择空目录),例如 F:\GoWork。
GOPATH对应创建的文件夹中里面,手动创建如下3个目录
1
2
3
|
src 存储go的源代码(需要我们自己手动创建)
pkg 存储编译后生成的包文件 (自动生成)
bin 存储生成的可执行文件(自动生成)
|
3.4、第一个Go程序
1
2
3
4
5
6
7
8
|
package main
import "fmt"
func main() {
fmt.Println("Hello Yuan!")
}
|
- 程序没有涉及到自定义包调用时可以放在电脑任何位置
- 为了以后方便管理,我们暂时统一放在gopath的src下
- 后面会学习go mod更好进行包管理
3.4.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.4.2、程序编译执行
我们上面给大家介绍过,Go语言是像C语言一样的编译型的静态语言,所以在运行Go语言程序之前,先要将其编译成二进制的可执行文件。
可以通过Go语言提供的go build
或者go run
命令对Go语言程序进行编译:
(1) go build
命令可以将Go语言程序代码编译成二进制的可执行文件,但是需要我们手动运行该二进制文件;
1、如果是普通包,当你执行go build之后,它不会产生任何文件。【非main包】
2、如果是main包,当你执行go build之后,它就会在当前目录下生成一个可执行文件,比如win系统的exe
文件
3、你也可以指定编译输出的文件名。我们可以指定go build -o 可执行文件.exe
(2)除了使用go build
命令外,Go语言还为我们提供了go run
命令,go run
命令将编译和执行指令合二为一,会在编译之后立即执行Go语言程序,但是不会生成可执行文件。
1
|
go run go文件名称 // go文件名称不能为空
|
3.5、IDE的安装与使用
3.5.1、安装Goland
GoLand是Jetbrains公司推出专为Go开发人员构建的跨平台IDE,可以运行在Windows,Linux,macOS系统之上,
下载地址:https://www.jetbrains.com/go/download/#section=windows
下载完成之后便可以进行安装了
因为GoLand是收费的IDE,同时也提供了30天免费试用的方式。如果经济能力允许的话,可以从指定渠道购买正版GoLand.
GoLand提供了Jetbrains Account,Activition Code和License Server三种激活方式,使用前必须激活或者选择免费试用
当激活或者选择免费试用之后便会启动GoLand。
免费试用需要点击log in,进行账户注册(在PC端完成),然后登陆,即可试用30天
此时可以选择New Project在指定的路径创建新的项目目录或者选择Open打开已经存在的项目目录,进行编辑。
3.5.2、GoLand下编写Go程序
当GoLand启动后,便可以使用它来编写Go程序了。首先选择New Project创建一个项目。然后设置项目路径和GOROOT
然后点击create创建。
创建文件和文件夹:
goland安装好后没有编译器的单独配置go编译器路径:
3.5.3、IDE的快捷键
快捷键 |
作用 |
Ctrl + / |
单行注释 |
Ctrl + Shift + / |
多行注释 |
Ctrl + D |
复制当前光标所在行 |
Ctrl + X |
删除当前光标所在行 |
Ctrl + Alt + L |
格式化代码 |
Ctrl + Shift + 方向键上或下 |
将光标所在的行进行上下移动(也可以使用 Alt+Shift+方向键上或下) |
Ctrl + Alt + left/right |
返回至上次浏览的位置 |
Ctrl + R |
替换 |
Ctrl + F |
查找文本 |
Ctrl + Shift + F |
全局查找 |
3.5.4、控制台折叠多余信息
四、基础语法
4.1、注释
注释就是对代码的解释和说明,其目的是让人们能够更加轻松地了解代码。注释是开发人员一个非常重要的习惯,也是专业的一种表现。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾。
注释只是为了提高可读性,不会被计算机编译。
4.2、变量
在计算机编程中,我们用变量来保存并管理很多数据,并用变量名来区分、识别和处理这些数据。
变量本质上是一种对内存地址的引用,让你能够把程序中准备使用的每一段数据都赋给一个简短、易于记忆的名字进行操作。
4.2.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 (
name string
age int
isMarried bool
)
|
4.2.2、变量赋值
变量赋值的3种方法
(1)变量名=值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 先声明再赋值
var x int
x = 10 // 不要 重复声明 : var x = 10
fmt.Println(x)
x = 20. // 重新赋值
// 直接声明赋值
// 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 name,age = "yuan",22
|
(2)变量名=变量名
1
2
3
4
5
|
var a = 100
var b = a // 变量之间的赋值是值拷贝
fmt.Println(a, b)
a = 200
fmt.Println(b)
|
(3)变量名=值 + 值 (变量名)
1
2
3
4
5
|
var a, b = 10, 20
var c = a + b
fmt.Println(c)
var d = c + 100
fmt.Println(d)
|
练习题:
- 将x,y两个变量的值进行交换
4.2.3、匿名变量
匿名变量即没有命名的变量,在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_
表示。
1
2
3
4
5
|
a,b,c :=4,5,6
fmt.Println(a,b,c)
// 如果只想接受第个变量,可以对前两个变量匿名
_,_,x := 4,5,6
fmt.Println(x)
|
匿名变量不占用命名空间,不会分配内存
让代码非常清晰,基本上屏蔽掉了可能混淆代码阅读者视线的内容,从而大幅降低沟通的复杂度和代码维护的难度。
4.2.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.3、语句分隔符
就像我们写作文一样,一定要有像逗号或者句号这样的语句分隔符,否则无法断句根本不能理解,编程语言也一样,需要给解释器或者编译器一个语句分割,让它知道哪里到哪里是一个语句,才能再去解析语句。
在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分(不建议这样写)。
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.4、基本数据类型
基本数据类型包含整型和浮点型,布尔类型以及字符串,这几种数据类型在几乎所有编程语言中都支持。
4.4.1、整形
字节了解:
字节(Byte):计算机中数据储存的单位。
位(bit):也叫作“比特”,计算机中数据储存的最小单位,因为在计算机中是以二进制的形式数据储存,所以每个位以“0”或“1”表示。
位和字节的关系是:8个位组成一个字节。
字节与位的关系:1Byte=8bit。
具体类型 |
取值范围 |
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 标准。
单精度浮点数占用4个字节(32位)存储空间来存储一个浮点数。而双精度浮点数使用 8个字节(64位)存储空间来存储一个浮点数。
单精度浮点数最多有7位十进制有效数字,如果某个数的有效数字位数超过7位,当把它定义为单精度变量时,超出的部分会自动四舍五入。双精度浮点数可以表示十进制的15或16位有效数字,超出的部分也会自动四舍五入。
浮点类型默认声明为float64。
1
2
3
4
5
6
7
8
9
10
|
var f1 float32 // float32: 单精度浮点型
f1 = 3.1234567890123456789
fmt.Println(f1, reflect.TypeOf(f1))
var f2 float64 // 双精度浮点型
f2 = 3.1234567890123456789
fmt.Println(f2, reflect.TypeOf(f2))
var f3 = 3.1234567890123456789
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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"fmt"
"reflect"
)
func main() {
var b bool // 声明b是一个布尔类型
b = true
b = false // 该类型只有true和false两个值,分别代表真假两种状态
fmt.Println(b, reflect.TypeOf(b))
fmt.Println(1 == 1) // 比较运算符的结果是一个布尔值
// fmt.Println(1 == "1") // 报错,mismatched types不能比较
fmt.Println(3 > 1)
var name = "yuan"
var b2 = name == "rain"
fmt.Println(b2)
}
|
4.4.4、字符串
字符串是最基本也是最常用的数据类型,是通过双引号将多个字符按串联起来的一种数据,用于展示文本。
1
2
|
var s = "hello yuan"
fmt.Println(s)
|
单引号只能标识字符
字符串的基本操作
字符串在内存中是一段连续存储空间
注意:
- 索引从零开始计数
- go语言不支持负索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
var s = "hello yuan"
fmt.Println(s)
// (1)索引取值 slice[index]
a:= s[2]
fmt.Println(string(a))
// (2)切片取值slice[start:end], 取出的元素数量为:结束位置 - 开始位置;
b1:=s[2:5] //
fmt.Println(b1)
b2:=s[0:] // 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
fmt.Println(b2)
b3:=s[:8] // 当缺省开始位置时,表示从连续区域开头到结束位置;
fmt.Println(b3)
// (3)字符串拼接
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
8
9
10
11
12
|
package main
import "fmt"
func main() {
var s1 = "hi yuan\nhi,alvin"
fmt.Println(s1)
var s2 = "his name is \"rain\""
fmt.Println(s2)
var s3 = "D:\\next\\go.exe"
fmt.Println(s3)
}
|
多行字符串
Go语言中要定义一个多行字符串时,就必须使用反引号
字符:
1
2
3
4
5
|
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)
|
反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
字符串的常用方法
方法 |
介绍 |
len(str) |
求长度 |
strings.ToUpper ,strings.ToLower |
生成一个新的全部大写的字符串,生成一个新的全部小写的字符串 |
strings.ReplaceAll |
生成一个新的原字符串被指定替换后的字符串 |
strings.Contains |
判断是否包含 |
strings.HasPrefix,strings.HasSuffix |
前缀/后缀判断 |
strings.Trim 、 |
去除字符串两端匹配的内容 |
strings.Index(),strings.LastIndex() |
子串出现的位置 |
strings.Split |
分割,将字符串按指定的内容分割成数组 |
strings.Join(a[]string, sep string) |
join操作,将数组按指定的内容拼接成字符串 |
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"
"reflect"
"strings"
)
func main() {
s := "hello world"
// fmt.Println(len(s))
// strings.ToUpper 和 strings.ToLower
s1 := strings.ToUpper("Yuan")
s2 := strings.ToLower("Rain")
fmt.Println(s1, s2)
// strings.Trim
user := " yuan "
fmt.Println(len(user))
fmt.Println(strings.TrimLeft(user, " "))
fmt.Println(strings.TrimSpace(user))
fmt.Println(strings.Trim(user, " "))
s := "alvin,yuan,eric"
// strings.Index,strings.LastIndex
var index = strings.Index(s, "yuan!")
fmt.Println(index) // 未找到返回-1
var index2 = strings.LastIndex(s, "l")
fmt.Println(index2)
// strings.HasPrefix,strings.HasSuffix,strings.Contains(实现的依赖的就是strings.Index)
fmt.Println(strings.HasPrefix(s, "alv"))
fmt.Println(strings.HasSuffix(s, "eric"))
fmt.Println(strings.Contains(s, "eric"))
// strings.Split: 将字符串分割成数组
var ret2 = strings.Split(s, ",")
fmt.Println(ret2, reflect.TypeOf(ret2))
// strings.Join:将数组拼接成字符串
var ret3 = strings.Join(ret2, "-")
fmt.Println(ret3, reflect.TypeOf(ret3))
}
|
练习:
- 基于字符串操作获取用户名和密码* s := “mysql … -u root -p 123”
4.4.6、类型转换
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
|
// (1)整型之间的转换
var a int8
a = 99
fmt.Println(int64(a), reflect.TypeOf(int64(a)))
fmt.Println(float64(a), reflect.TypeOf(float64(a)))
// (2)string与int类型的转换
x := strconv.Itoa(98)
fmt.Println(x, reflect.TypeOf(x))
y, _ := strconv.Atoi("97")
fmt.Println(y, reflect.TypeOf(y))
// (3) Parse系列函数
// ParseInt
// 输入:1.数字的字符串形式 2.base,数字字符串的进制,比如:2进制、10进制。
// 3.bitSize的含义是⼤⼩限制,如果字符串转化的整形数据类型超过bitSize的最大值,那么输出的int64为bitSize的最大值,err就会显⽰数据超出范围。
i1, _ := strconv.ParseInt("1000", 10, 8)
println(i1)
i2, _ := strconv.ParseInt("1000", 10, 64)
println(i2)
f2, _ := strconv.ParseFloat("3.1415926", 64)
fmt.Println(f2, reflect.TypeOf(f2))
f1, _ := strconv.ParseFloat("3.1415926", 32)
fmt.Println(f1, reflect.TypeOf(f1))
// 返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。
b1, _ := strconv.ParseBool("true")
fmt.Println(b1, reflect.TypeOf(b1))
b2, _ := strconv.ParseBool("T")
fmt.Println(b2, reflect.TypeOf(b2))
b3, _ := strconv.ParseBool("1")
fmt.Println(b3, reflect.TypeOf(b3))
b4, _ := strconv.ParseBool("100")
fmt.Println(b4, reflect.TypeOf(b4))
|
4.5、运算符
一个程序的最小单位是一条语句,一条语句最少包含一条指令,而指令就是对数据做运算,我们已经学完基本数据类型了,知道如何构建和使用一些最简单的数据,那么我们能对这些数据做什么运算呢,比如fmt.Println(1+1)
这条语句包含两个指令,首先是计算1+1的指令,1就是数据,+就是算术运算符中的相加,这样计算机就可以帮我们执行这个指令计算出结果,然后执行第二个指令,即将计算结果2打印在终端,最终完成这条语句。
4.5.1、算数运算符
运算符 |
描述 |
+ |
相加 |
- |
相减 |
* |
相乘 |
/ |
相除 |
% |
求余 |
如何判断一个整型数字是奇数还是偶数?
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。 |
1
2
3
4
5
6
|
fmt.Println(2.1 > 1 || 3 == 2)
// 用户名为root或者年龄大于18岁
username := "root"
age := 16
ret := username == "root" || !(age < 18)
fmt.Println(ret)
|
4.5.4、赋值运算符
运算符 |
描述 |
= |
简单的赋值运算符,将一个表达式的值赋给一个左值 |
+= |
相加后再赋值 |
-= |
相减后再赋值 |
*= |
相乘后再赋值 |
/= |
相除后再赋值 |
%= |
求余后再赋值 |
«= |
左移后赋值 |
»= |
右移后赋值 |
&= |
按位与后赋值 |
^= |
按位异或后赋值 |
|= |
按位或后赋值 |
1
2
3
4
5
6
7
8
9
10
11
12
|
var a = 10
// 使a自加1
ret := a + 1
a = ret
// 使a自加1
a = a + 1
// 使a自加1
a += 1 // 赋值元算符
// 使a自加1
a++ // 注意:不能写成 ++ a 或 -- a 必须放在右边使用
// b := a++ // 此处为错误的用法,不能写在一行,要单独作为语句使用
fmt.Println(a)
|
4.5.5、运算符优先级
1
2
3
4
5
6
7
8
9
10
11
12
|
// 案例1
var a, b, c, d = 8, 6, 4, 2
ret := a + b*c/d
fmt.Println(ret)
// 案例2
x := 10
y := 1
x += 5*(1+2) + y
fmt.Println(x)
z := 1+2 > 3 || 1 == 1*5
fmt.Println(z)
|
为什么x = 1+1, 为什么先计算后赋值:运算符的优先级
4.6、输入输出函数
4.6.1、输出函数
fmt.Print
有几个变种:
Print: 输出到控制台,不接受任何格式化操作
Println: 输出到控制台并换行
Printf : 只可以打印出格式化的字符串,只可以直接输出字符串类型的变量(不可以输出别的类型)
Sprintf:格式化并返回一个字符串而不带任何输出
(1)Print 和Println
Print和Println()函数可以打印出字符串或变量的值。
1
2
3
4
5
6
7
8
9
10
|
name := "yuan"
age := 24
fmt.Print(name, age)
fmt.Println("hello world")
fmt.Println(name)
fmt.Println(age)
fmt.Println(name, age)
fmt.Println("姓名:", name, "年龄:", age)
|
(2)格式化输出(Printf)
Printf 根据格式说明符格式化并写入标准输出。Printf 只打印字符串
比如上面打印一个人的基本信息格式:
1
2
3
4
5
6
7
|
name := "yuan"
age := 24
isMarried := false
salary := 3000.549
fmt.Printf("姓名:%s 年龄:%d 婚否:%t 薪资:%.2f\n", name, age, isMarried, salary)
fmt.Printf("姓名:%v 年龄:%v 婚否:%v 薪资:%v\n", name, age, isMarried, salary)
fmt.Printf("姓名:%#v 年龄:%#v 婚否:%#v 薪资:%#v\n", name, age, isMarried, salary)
|
|
%v :以默认的方式打印变量的值 |
%#v :相应值的Go语法表示 |
%T :打印对应值的类型 |
%+d :带符号的整型,%d 不带符号的整型 |
%o :不带零的八进制,%#o 带零的八进制 |
%x :小写的十六进制,%X 大写的十六进制,%#x 带0x的十六进制 |
%b :打印整型的二进制 |
%t :打印true 或 false |
%s :输出字符串表示,%-5s 最小宽度为5(左对齐) |
%f 小数点而无指数,默认精度为6 |
%e 科学计数法,如-1234.456e+78 |
%p 带0x的指针,%#p 不带0x的指针 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 整形和浮点型
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("%.3f\n", 3.1415) // 3.142
fmt.Printf("%e\n", 1000.25) // 科学计数法: 1.000250e+03,默认精度为6
// 设置宽度
fmt.Printf("学号:%s 姓名:%-20s 平均成绩:%-4d\n", "1001", "alvin", 100)
fmt.Printf("学号:%s 姓名:%-20s 平均成绩:%-4d\n", "1002", "zuibangdeyuanlaoshi", 98)
fmt.Printf("学号:%s 姓名:%-20s 平均成绩:%-4d\n", "1003", "x", 78)
|
(3)Sprintf
Printf和Sprintf都是替换字符串,Printf是直接输出到终端,Sprintf是不直接输出到终端,而是返回该字符串
1
2
3
4
5
6
|
name := "yuan"
age := 24
isMarried := false
salary := 3000.549
info := fmt.Sprintf("姓名:%s 年龄:%d 婚否:%t 薪资:%.2f\n", name, age, isMarried, salary)
fmt.Println(info)
|
4.6.2、输入函数
go语言fmt包下有三个函数,可以在程序运行过程中从标准输入获取用户的输入:
(1)fmt.Scan
语法:
func Scan(a ...interface{}) (n int, err error)
- Scan 从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。
- 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package main
import "fmt"
func main() {
var (
name string
age int
isMarried bool
)
fmt.Scan(&name, &age, &isMarried) // 输入类型不一致,按默认值
fmt.Printf("扫描结果 name:%s age:%d married:%t\t", name, age, isMarried)
}
|
将上述代码在终端运行,在终端依次输入 yuan 、26、false使用空格分隔。
go run main.go
yuan 26 false
扫描结果 name:yuan age:26 married:false
fmt.Scan从标准输入中扫描用户输入的数据,将以空白符分隔的数据分别存入指定的参数中。
(2)fmt.Scanf
语法
func Scanf(format string, a ...interface{})(n int, err error)
- Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
- 本函数返回成功扫描的数据个数和遇到的任何错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 案例1
var (
name string
age int
isMarried bool
)
fmt.Scanf("1:%s 2:%d 3:%t", &name,&age,&isMarried)
fmt.Printf("扫描结果 姓名:%s 年龄:%d 婚否:%t", name,age,isMarried)
// 案例2
var a, b int
fmt.Scanf("%d+%d", &a, &b)
fmt.Println(a + b)
|
(3)fmt.Scanln
语法
func Scanln(a ...interface{}) (n int, err error)
- Scanln类似于Scan,它遇到换行立即停止扫描。
- 本函数返回成功扫描的数据个数和遇到的任何错误。
Scanln和Scan的区别就是Scanln遇到换行立即结束输入,而Scan则会将换行符作为一个空白符继续下一个输入
4.7、常量与itoa
4.7.1 常量
常量是⼀个简单值的标识符,在程序运⾏时,不会被修改的量。
在Python、Java编程规范中,常量⼀般都是全⼤写字母,但是在Golang中,⼤⼩写是具有⼀定特殊含义的,所以不⼀定所有常量都得全⼤写。
声明赋值方式与变量接近,通过const
实现
const 常量名[数据类型] = value
数据类型可以忽略不写,Golang编译器会⾃动推断出数据类型。
在使⽤时,要注意以下⼏点:
- 数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
- 满⾜多重赋值
- 常量只定义不使⽤,编译不会报错
- 常量可以作为枚举,常量组
- 常量组中如不指定类型和初始化值,则与上⼀⾏⾮空常量右值相同
- 显⽰指定类型的时候,必须确保常量左右值类型⼀致,需要时可做显⽰类型转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// (1)声明常量
const pai = 3.1415926
const e float64 = 2.7182818
fmt.Println(pai * pai)
// (2)常量也可以和变量一样一组一起声明
// const monday, tuesday, wednesday = 1, 2, 3
// 更推荐下面这种方式
const (
monday = 1
tuesday = 2
wednesday = 3
thursday = 4
friday = 5
saturday = 6
sunday = 7
)
const (
female = 0
male = 1
)
// ⼀组常量中,如果某个常量没有初始值,默认和上⼀⾏⼀致
const (
a int = 1
b
c = 2
d
)
fmt.Println(a, b, c, d)
|
4.7.2 iota计数器
iota
是go语言的常量计数器,只能在常量的表达式中使用。 使用iota
时只需要记住以下两点
1.iota
在const
关键字出现时将被重置为0。
2.const
中每新增一行常量声明将使iota
计数一次(iota可理解为const
语句块中的行索引)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
const (
food = iota
cloth
bed
electric
)
fmt.Println(food, cloth, bed, electric)
const (
a = 1
b = iota
c = 6
d
e = iota
f
)
fmt.Println(a, b, c, d, e, f)
|
1
2
3
4
5
6
7
8
9
10
|
const (
b = 1 << (iota * 10)
kb
mb
gb
tb
pb
)
fmt.Println(b, kb, mb, gb, tb, pb)
|
思考题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const (
n1 = iota
n2
_
n4
)
const (
a = iota
b
_
c, d = iota + 1, iota + 2
e = iota
)
fmt.Println(a, b, c, d, e)
|
五、流程控制语句
程序是由语句构成,而流程控制语句 是用来控制程序中每条语句执行顺序的语句。可以通过控制语句实现更丰富的逻辑以及更强大的功能。几乎所有编程语言都有流程控制语句,功能也都基本相似。
其流程控制方式有
这里最简单最常用的就是顺序结构,即语句从上至下一一执行。
5.1、分支语句
顺序结构的程序虽然能解决计算、输出等问题,但不能做判断再选择。对于要先做判断再选择的问题就要使用分支结构。
5.1.1、单分支语句
语法:
1
2
3
|
if 布尔表达式 { // 注意左花括号必须与表达式同行
/* 在布尔表达式为 true 时执行 */
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var username,password string
fmt.Print("请输入用户名:")
fmt.Scanln(&username)
fmt.Println("username",username)
fmt.Print("请输入密码:")
fmt.Scanln(&password)
fmt.Println("password",password)
if username == "yuan" && password=="123"{
fmt.Println("登录成功!")
}
|
5.1.2、双分支语句
双分支语句顾名思义,二条分支二选一执行!
1
2
3
4
5
6
7
8
|
var age int
fmt.Println("请输入你的年龄:")
fmt.Scanln(&age)
if age >= 18 {
fmt.Println("恭喜,你已经成年,可以观看该影片!")
} else {
fmt.Println("抱歉,你还未成年,不宜观看该影片!")
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var username,password string
fmt.Print("请输入用户名:")
fmt.Scanln(&username)
fmt.Println("username",username)
fmt.Print("请输入密码:")
fmt.Scanln(&password)
fmt.Println("password",password)
if username == "yuan" && password=="123"{
fmt.Println("登录成功!")
}else {
fmt.Println("用户名或者密码错误!")
}
|
5.1.3、if多分支语句
多分支即从比双分支更多的分支选择一支执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package main
import "fmt"
func main() {
var score int
fmt.Scanln(&score)
if score > 100 && score < 0 {
fmt.Println("输入数字应该在1-100之间")
} else if score > 90 {
fmt.Println("成绩优秀!")
} else if score > 80 {
fmt.Println("成绩良好!")
} else if score > 60 {
fmt.Println("成绩及格!")
} else {
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
var month, day int
fmt.Print("请输入生日的月和日:")
fmt.Scan(&month, &day)
var xingZuo string
if day < 1 || day > 31 {
fmt.Println("输入的日有问题")
os.Exit(0)
}
switch month {
case 1:
// 日判断
if day >= 1 && day <= 19 {
xingZuo = "摩羯座"
} else {
xingZuo = "水瓶座"
}
case 2:
// 日判断
if day >= 1 && day <= 18 {
xingZuo = "水瓶座"
} else {
xingZuo = "双鱼座"
}
case 3:
// 日判断
if day >= 1 && day <= 20 {
xingZuo = "双鱼座"
} else {
xingZuo = "白羊座"
}
case 4:
// 日判断
if day >= 1 && day <= 19 {
xingZuo = "白羊座"
} else {
xingZuo = "金牛座"
}
case 5:
// 日判断
if day >= 1 && day <= 20 {
xingZuo = "金牛座"
} else {
xingZuo = "双子座"
}
case 6:
// 日判断
if day >= 1 && day <= 21 {
xingZuo = "双子座"
} else {
xingZuo = "巨蟹座"
}
case 7:
// 日判断
if day >= 1 && day <= 22 {
xingZuo = "巨蟹座"
} else {
xingZuo = "狮子座"
}
case 8:
// 日判断
if day >= 1 && day <= 22 {
xingZuo = "狮子座"
} else {
xingZuo = "处女座"
}
case 9:
// 日判断
if day >= 1 && day <= 22 {
xingZuo = "处女座"
} else {
xingZuo = "天秤座"
}
case 10:
// 日判断
if day >= 1 && day <= 23 {
xingZuo = "天秤座"
} else {
xingZuo = "天蝎座"
}
case 11:
// 日判断
if day >= 1 && day <= 22 {
xingZuo = "天蝎座"
} else {
xingZuo = "射手座"
}
case 12:
// 日判断
if day >= 1 && day <= 21 {
xingZuo = "射手座"
} else {
xingZuo = "摩羯座"
}
default:
fmt.Println("输入的月份有问题")
}
fmt.Println("您的星座是:", xingZuo)
|
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:
}
|
strconv.ParseBool() 源码查看
5.1.5、分支嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// 分支嵌套
var user string
var pwd int
fmt.Printf("请输入用户名:")
fmt.Scanln(&user)
fmt.Printf("请输入密码:")
fmt.Scanln(&pwd)
if user == "yuan" && pwd==123{
var score int
fmt.Printf("请输入成绩:")
fmt.Scanln(&score)
if score >= 90 && score<=100 {
fmt.Println("成绩优秀!")
} else if score >= 80 {
fmt.Println("成绩良好!")
} else if score >= 60 {
fmt.Println("成绩及格")
} else {
fmt.Println("不及格!")
}
}else {
fmt.Println("用户名或者密码错误!")
}
|
5.2、循环语句
在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。一组被重复执行的语句称之为循环体,能否继续重复,决定循环的终止条件。
与其它主流编程语言不同的的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构。
5.2.1、for循环
原始for循环
通过关系表达式或逻辑表达式控制循环
简单循环案例:
1
2
3
4
5
6
|
count := 0 // 初始化语句
for count < 10 { // 条件判断
fmt.Println("hello yuan!")
count++ // 步进语句
}
fmt.Println("end")
|
1
2
3
4
5
6
|
count := 10 // 初始化语句
for count > 0 { // 条件判断
fmt.Println(count)
count-- // 步进语句
}
fmt.Println("end")
|
练习:如何打印1-100
三要素for循环(核心)
将初始化语句、条件判断以及步进语句格式固定化的循环方式,本质上和上面的for循环没有区别。
1
2
3
|
for init;condition;post {
// 循环体语句
}
|
-
init
: 初始化语句,一般为赋值表达式,给控制变量赋初值;
-
condition
:条件判断,一般是关系表达式或逻辑表达式,循环控制条件;
-
post
: 步进语句,一般为赋值表达式,给控制变量增量或减量。
1
2
3
|
for i := 1; i < 10; i++ {
fmt.Println(i)
}
|
执行流程(关键):
(1)初始语句
(2)条件判断,布尔值为真则执行一次循环体,为假则退出循环
(3)执行一次循环体语句结束后,再执行步进语句,然后回到步骤(2),依次循环
案例1:计算1-100的和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/* var s = 0
s += 1
s += 2
s += 3
s += 4
s += 5
s += 6
s += 7
s += 8
s += 9
s += 10
fmt.Println(s)*/
// 如果是1+2+3+....100呢?如何借助循环语法实现
var s = 0
for i := 1; i <= 100; i++ {
s += i
}
fmt.Println(s)
|
5.2.2、分支与循环的嵌套使用
循环与和分支语句是可以相互嵌套使用的,即分支语句中使用循环语句,循环语句中使用分支语句。
打印1-100中所有的偶数
1
2
3
4
5
|
for i := 1; i <= 100; i++ {
if i%2 == 0 {
fmt.Println(i)
}
}
|
无限循环案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
/*
无限循环
for true{}
*/
// 循环中嵌入分支语句
func main() {
fmt.Printf(`
1、普通攻击
2、超级攻击
3、使用道具
4、逃跑
`)
for true {
var choice int
fmt.Printf("请输入选择:")
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("输入有误!")
}
}
}
|
输入正整数num,大于100,则打印1-num,小于100,则打印num-1?
1
2
3
4
5
6
7
8
9
10
11
12
|
var num int
fmt.Scanln(&num)
if num > 100 {
for i := 1; i <= num; i++ {
fmt.Println(i)
}
} else {
for i := num; i > 0; i-- {
fmt.Println(i)
}
}
|
5.2.3、退出循环
如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用break或continue关键字。
- break 用来跳出整个循环语句,也就是跳出所有的循环次数;
- continue 用来跳出当次循环,也就是跳过当前的一次循环。
break语句
当 break 关键字用于 for 循环时,会终止循环而执行整个循环语句后面的代码。break 关键字通常和 if 语句一起使用,即满足某个条件时便跳出循环,继续执行循环语句下面的代码。
1
2
3
4
5
6
|
for i := 0; i < 10; i++ {
if i==8{
break
}
fmt.Println(":",i)
}
|
continue语句
break 语句使得循环语句还没有完全执行完就提前结束,与之相反,continue 语句并不终止当前的循环语句的执行,仅仅是终止当前循环变量 i 所控制的这一次循环,而继续执行该循环语句。continue 语句的实际含义是“忽略 continue 之后的所有循环体语句,回到循环的顶部并开始下一次循环”
1
2
3
4
5
6
|
for i := 0; i < 10; i++ {
if i==8{
continue
}
fmt.Println(":",i)
}
|
案例: 计算 1 - 2 + 3 - 4 + … + 99 中除了88以外所有数的总和?
5.2.4、循环嵌套
在一个循环体语句中又包含另一个循环语句,称为循环嵌套
独立嵌套
在控制台上打印一个五行五列的矩形,如下图所示
1
2
3
4
5
|
*****
*****
*****
*****
*****
|
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()
}
|
六、重要数据类型
6.1、指针类型(核心类型)
6.1.1、指针的基本使用
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。
我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
Go语言中使用对于指针存在两种操作: 取址
和取值
。
符号 |
名称 |
作用 |
&变量 |
取址符 |
返回变量所在的地址 |
*指针变量 |
取值符 |
返回指针指地址存储的值 |
1
2
3
4
5
6
7
8
|
var x = 100
// 取址符:& 取值符:*
fmt.Println("x的地址:", &x)
// 将地址值赋值给的变量称为指针变量
var p *int
p = &x
fmt.Println("p的值:", p)
fmt.Println("p地址对应的值", *p)
|
关于地址的格式化打印
1
2
3
4
5
|
var x = 10
fmt.Printf("%p\n", &x)
x = 100
fmt.Printf("%p\n", &x)
fmt.Println(*&x)
|
关于指针的应用:
1
2
3
4
5
6
7
8
9
|
// 当使用等号将一个变量的值赋给另一个变量时,如 x = y ,实际上是在内存中将 i 的值进行了拷贝
var x = 10
var y = x
var z = &x
x = 20
fmt.Println(y)
fmt.Println(*z)
*z = 30
fmt.Println(x)
|
练习1
1
2
3
4
5
6
7
|
var x = 10
var y = &x
var z = *y
x = 20
fmt.Println(x)
fmt.Println(*y)
fmt.Println(z)
|
练习2
1
2
3
4
5
|
var a = 100
var b = &a
var c = &b
**c = 200
fmt.Println(a)
|
- Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移和运算,从而避免了非法修改关键性数据的问题。
6.1.2、new函数
new 和 make 是 Go 语言中用于内存分配的原语。简单来说,new 只分配内存,make 用于初始化 slice、map 和 channel。
之前我们学习的基本数据类型声明之后是有一个默认零值的,但是指针类型呢?
1
2
3
4
|
var p *int
// fmt.Println(p) // <nil>
// fmt.Println(*p) // 报错,并没有开辟空间地址
*p = 10. // 报错
|
我们可以看到初始化⼀个指针变量,其值为nil,nil的值是不能直接赋值的。通过内建的new函数返回⼀个指向新分配的类型为int的指针,指针值为0xc00004c088,这个指针指向的内容的值为零(zero value)。
1
2
3
4
5
|
var p *int = new(int)
fmt.Println(p) // 0x14000122008
fmt.Println(*p) // 0
*p = 10
fmt.Println(*p) // 10
|
make返回的还是引⽤类型本⾝;⽽new返回的是指向类型的指针。后面再详细介绍
6.2、数组
我们之前学习过变量,当存储一个学生名字时可以name="yuan"
,但是如果班级有三十人,每个人的名字都想存储到内存中怎么办呢?总不能用三十个变量分别存储吧,这时数组就可以发挥作用了。
数组其实是和字符串一样的序列类型,不同于字符串在内存中连续存储字符,数组用[]
的语法将同一类型的多个值存储在一块连续内存中。
6.2.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
3
4
|
var x [3]int
var y [5]int
// x y的数据类型相同吗?
|
6.2.2、数组初始化
初始化方式1:先声明再赋值
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]
|
初始化方式2:声明并赋值
1
2
3
4
|
var names = [3]string{"张三","李四","王五"}
var ages = [3]int{23,24,25}
fmt.Println(names) // [张三 李四 王五]
fmt.Println(ages) // [23 24 25]
|
初始化方式3: […]不限长度
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
|
初始化方式4:索引设置
1
2
|
var names = [...]string{0:"张三",2:"王五"}
fmt.Println(names) // [张三 王五]
|
6.2.3、基于索引访问和修改数组元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
var names = [...]string{"张三","李四","王五","赵六","孙七"}
// 索引取值
fmt.Println(names[2])
// 修改元素值
names[0] = "zhangsan"
fmt.Println(names)
// 切片取值
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{ // range 表达式是副本参与循环
fmt.Println(k,v)
}
|
6.3、切片(slice)
切片是一个动态数组,因为数组的长度是固定的,所以操作起来很不方便,比如一个names数组,我想增加一个学生姓名都没有办法,十分不灵活。所以在开发中数组并不常用,切片类型才是大量使用的。
6.3.1、切片基本操作
切片的创建有两种方式:
- 从数组或者切片上切取获得
- 直接声明切片 :
var name []Type
// 不同于数组, []没有数字
切片语法:
1
|
arr [start : end] 或者 slice [start : end] // start: 开始索引 end:结束索引
|
切片特点:
- 左闭右开 [ )
- 取出的元素数量为:结束位置 - 开始位置;
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用
slice[len(slice)]
获取;
- 当缺省开始位置时,表示从连续区域开头到结束位置;当缺省结束位置时,表示从开始位置到整个连续区域末尾;两者同时缺省时,与切片本身等效;
1
2
3
4
5
6
|
var arr = [5]int{10, 11, 12, 13, 14}
var s1 = arr[1:4]
fmt.Println(s1, reflect.TypeOf(s1)) // [11 12 13] []int
var s2 = arr[2:5]
fmt.Println(s2, reflect.TypeOf(s2)) // [12 13 14]
var s3 = s2[0:2] // [12 13]
|
思考:
1
2
|
s3[0] = 1000
fmt.Println(":::", s1, s2, s3)
|
6.3.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
|
// 整型赋值
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)
|
(2) 引用类型
指针、slice,map,chan,interface
等都是引用类型。
特点:变量通过存储一个地址来存储最终的值。内存通常在堆上分配,通过GC回收。
引用类型必须申请内存才可以使用,new()和make()是给引用类型申请内存空间。
6.3.3、切片原理
切片的构造根本是对一个具体数组通过切片起始指针,切片长度以及最大容量三个参数确定下来的
1
2
3
4
5
|
type Slice struct {
Data uintptr // 指针,指向底层数组中切片指定的开始位置
Len int // 长度,即切片的长度
Cap int // 最大长度(容量),也就是切片开始位置到数组的最后位置的长度
}
|
举例:
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
|
var arr = [5]int{10, 11, 12, 13, 14}
s1 := arr[0:3] // 对数组切片
s2 := arr[2:5]
s3 := s2[0:2] // 对切片切片
fmt.Println(s1) // [10, 11, 12]
fmt.Println(s2) // [12, 13, 14]
fmt.Println(s3) // [12, 13]
// 地址是连续的
fmt.Printf("%p\n", &arr)
fmt.Printf("%p\n", &arr[0]) // 相差8个字节
fmt.Printf("%p\n", &arr[1])
fmt.Printf("%p\n", &arr[2])
fmt.Printf("%p\n", &arr[3])
fmt.Printf("%p\n", &arr[4])
// 每一个切片都有一块自己的空间地址,分别存储了对于数组的引用地址,长度和容量
fmt.Printf("%p\n", &s1) // s1自己的地址
fmt.Printf("%p\n", &s1[0])
fmt.Println(len(s1), cap(s1))
fmt.Printf("%p\n", &s2) // s2自己的地址
fmt.Printf("%p\n", &s2[0])
fmt.Println(len(s2), cap(s2))
fmt.Printf("%p\n", &s3) // s3自己的地址
fmt.Printf("%p\n", &s3[0])
fmt.Println(len(s3), cap(s3))
|
练习题:
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))
|
除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:
1
|
var name []Type // []Type是切片类型的标识
|
其中 name 表示切片的变量名,Type 表示切片对应的元素类型。
1
2
|
var names = []string{"张三","李四","王五"}
fmt.Println(names,reflect.TypeOf(names)) // [张三 李四 王五 赵六 孙七] []string
|
直接声明切片,会针对切片构建底层数组,然后切片形成对数组的引用
练习1
1
2
3
4
|
s1 := []int{1, 2, 3}
s2 := s1[1:]
s2[1] = 4
fmt.Println(s1)
|
练习2
1
2
3
4
|
var a = []int{1, 2, 3}
b := a
a[0] = 100
fmt.Println(b)
|
6.3.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.3.5、append(重点)
上面我们已经讲过,切片作为一个动态数组是可以添加元素的,添加方式为内建方法append。
(1)append的基本用法
1
2
3
4
5
6
7
8
9
10
11
12
|
var emps = make([]string, 3, 5)
emps[0] = "张三"
emps[1] = "李四"
emps[2] = "王五"
fmt.Println(emps)
emps2 := append(emps, "rain")
fmt.Println(emps2)
emps3 := append(emps2, "eric")
fmt.Println(emps3)
// 容量不够时发生二倍扩容
emps4 := append(emps3, "yuan")
fmt.Println(emps4) // 此时底层数组已经发生变化
|
扩容机制
1、每次 append 操作都会检查 slice 是否有足够的容量,如果足够会直接在原始数组上追加元素并返回一个新的 slice,底层数组不变,但是这种情况非常危险,极度容易产生 bug!而若容量不够,会创建一个新的容量足够的底层数组,先将之前数组的元素复制过来,再将新元素追加到后面,然后返回新的 slice,底层数组改变而这里对新数组的进行扩容
2、扩容策略:如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。
经典面试题
1
2
3
4
5
6
7
8
9
|
arr := [4]int{10, 20, 30, 40}
s1 := arr[0:2] // [10, 20]
s2 := s1 // // [10, 20]
s3 := append(append(append(s1, 1), 2), 3)
s1[0] = 1000
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
fmt.Println(arr)
|
(2)append的扩展用法
1
2
3
4
5
6
7
|
var a []int
a = append(a, 1) // 追加1个元素
fmt.Println(a)
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
fmt.Println(a)
a = append(a, []int{1, 2, 3}...) // 追加一个切片, 切片需要解包
fmt.Println(a)
|
a = append(a, 1)返回切片又重新赋值a的目的是丢弃老数组
经典练习:
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
|
// 案例1
a := []int{11, 22, 33}
fmt.Println(len(a), cap(a))
c := append(a, 44)
a[0] = 100
fmt.Println(a)
fmt.Println(c)
// 案例2
a := make([]int, 3, 10)
fmt.Println(a)
b := append(a, 11, 22)
fmt.Println(a) // 小心a等于多少?
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)
|
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] 中。
思考这样写可以不:
1
2
3
4
|
var a = []int{1,2,3,4}
s1:=a[:2]
s2:=a[2:]
fmt.Println(append(append(s1,100,),s2...))
|
删除元素
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
6
7
8
9
10
11
12
13
14
15
16
17
|
a:=[]int{10,2,3,100}
sort.Ints(a)
fmt.Println(a) // [2 3 10 100]
b:=[]string{"melon","banana","caomei","apple"}
sort.Strings(b)
fmt.Println(b) // [apple banana caomei melon]
c:=[]float64{3.14,5.25,1.12,4,78}
sort.Float64s(c)
fmt.Println(c) // [1.12 3.14 4 5.25 78]
// 注意:如果是一个数组,需要先转成切片再排序 [:]
sort.Sort(sort.Reverse(sort.IntSlice(a)))
sort.Sort(sort.Reverse(sort.Float64Slice(c)))
fmt.Println(a,c)
|
6.2.8、切片拷贝
1
2
3
4
5
6
7
8
9
|
var s1 = []int{1, 2, 3, 4, 5}
var s2 = make([]int, len(s1))
copy(s2, s1)
fmt.Println(s2)
s3 := []int{4, 5}
s4 := []int{6, 7, 8, 9}
copy(s4, s3)
fmt.Println(s4) //[4 5 3]
|
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类型。
- Map是一种通过key来获取value的一个数据结构,其底层存储方式为数组,在存储时key不能重复,当key重复时,value进行覆盖,我们通过key进行hash运算(可以简单理解为把key转化为一个整形数字)然后对数组的长度取余,得到key存储在数组的哪个下标位置,最后将key和value组装为一个结构体,放入数组下标处
- slice查询是遍历方式,时间复杂度是O(n), map查询是hash映射 ;当数据量小的时候切片查询比map快,但是数据量大的时候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[]
|
(1) 先声明再赋值
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的键是无序的
- map的键不能重复
(2) 直接声明赋值
1
2
|
info := map[string]string{"name": "yuan", "age": "23","gender":"male"}
fmt.Println(info) // map[age:18 gender:male name:yuan]
|
6.3.2、map的增删改查
(1) 查
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)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
noSortMap := map[int]int{
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
}
for k, v := range noSortMap { // for range顺序随机
fmt.Println(k, v)
}
|
(2)添加和更新
1
2
3
4
|
info := map[string]string{"name": "yuan", "age": "18","gender":"male"}
info["height"] = "180cm" // 键不存在,则是添加键值对
info["age"] = "22" // 键存在,则是更新键的值
fmt.Println(info) // map[age:22 gender:male height:180cm name:yuan]
|
(3)删除键值对
一个内置函数 delete(),用于删除容器内的元素
1
2
3
|
info := map[string]string{"name": "yuan", "age": "18","gender":"male"}
delete(info,"gender")
fmt.Println(info)
|
如果想清空一个map,最优方式即创建一个新的map!
6.3.3、map 容量
和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:
1
|
make(map[keytype]valuetype, cap)
|
例如:
1
|
m := make(map[string]float, 100)
|
当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
6.3.4、map的灵活运用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 案例1
data := map[string][]string{"hebei": []string{"廊坊市", "石家庄", "邯郸"}, "beijing": []string{"朝阳", "丰台", "海淀"}}
// 打印河北的第二个城市
// 循环打印每个省份的名字和城市数量
// 添加一个新的省份和城市的key-value
// 删除北京的key-value
// 案例2
info := map[int]map[string]string{1001: {"name": "yuan", "age": "23"}, 1002: {"name": "alvin", "age": "33"}}
// 打印学号为1002的学生的年龄
// 循环打印每个学员的学号,姓名,年龄
// 添加一个新的学员
// 删除1001的学生
// 案例3
stus := []map[string]string{{"name": "yuan", "age": "23"}, {"name": "rain", "age": "22"}, {"age": "32", "name": "eric"}}
// 打印第二个学生的姓名
// 循环打印每一个学生的姓名和年龄
// 添加一个新的学生map
// 删除一个学生map
// 将姓名为rain的学生的年龄自加一岁
|
6.3.5、练习
1
2
3
|
// 根据age的大小重新排序
stus := []map[string]int{map[string]int{"age": 23}, map[string]int{"age": 33}, map[string]int{"age": 18}}
fmt.Println(stus)
|
6.3.6、map的底层原理
(1)摘要算法
“消息摘要”(Message Digest)是一种能产生特殊输出格式的算法,这种加密算法的特点是无论用户输入什么长度的原始数据,经过计算后输出的密文都是固定长度的,这种算法的原理是根据一定的运算规则对原数据进行某种形式的提取,这种提取就是“摘要”,被“摘要”的数据内容与原数据有密切联系,只要原数据稍有改变,输出的“摘要”便完全不同,因此基于这种原理的算法便能对数据完整性提供较为健全的保障。但是,由于输出的密文是提取原数据经过处理的定长值,所以它已经不能还原为原数据,即消息摘要算法是**“不可逆”**的,理论上无法通过反向运算取得原数据内容,因此它通常只能被用来做数据完整性验证,而不能作为原数据内容的加密方案使用,否则谁也无法还原。
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 (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"fmt"
"os"
)
func main() {
//输⼊字符串测试开始.
input := "k4"
//MD5算法.
hash := md5.New()
_, err := hash.Write([]byte(input))
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
result := hash.Sum(nil)
//或者result := hash.Sum([]byte(""))
fmt.Printf("md5 hash算法长度为%d,结果:%x\n", len(result), result)
//SHA1算法.
hash = sha1.New()
_, err = hash.Write([]byte(input))
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
result = hash.Sum(nil)
//或者result = hash.Sum([]byte(""))
fmt.Printf("sha1 hash算法长度为%d,结果:%x\n", len(result), result)
//SHA256算法.
hash = sha256.New()
_, err = hash.Write([]byte(input))
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
result = hash.Sum(nil)
//或者result = hash.Sum([]byte(""))
fmt.Printf("sha256 hash算法长度为%d,结果:%x\n", len(result), result)
}
|
(2)map底层存储
哈希表属于编程中比较常见的数据结构之一,基本上所有的语言都会实现数组和哈希表这两种结构。
slice查询是遍历⽅式,时间复杂度是O(n)
map查询是hash映射,时间复杂度是O(1)
在go的map实现中,它的底层结构体是hmap,hmap⾥维护着若⼲个bucket数组 (即桶数组)。
Bucket数组中每个元素都是bmap结构,也即每个bucket(桶)都是bmap结构,【ps:后⽂为了语义⼀致,和⽅便理解,就不再提bmap 了,统⼀叫作桶】 每个桶中保存了8个kv对,如果8个满了,⼜来了⼀个key落在了这个桶⾥,会使⽤overflow连接下⼀个桶(溢出桶)。
map 的源码位于 src/runtime/map.go 文件中,结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
type hmap struct {
count int // 当前 map 中元素数量
flags uint8
B uint8 // 当前 buckets 数量,2^B 等于 buckets 个数
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // 哈希种子
buckets unsafe.Pointer // buckets 数组指针
oldbuckets unsafe.Pointer // 扩容时保存之前 buckets 数据。
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
|
count |
键值对的数量 |
B |
2^B=len(buckets) |
hash0 |
hash因子 |
buckets |
指向一个数组(连续内存空间),数组的类型为[]bmap,bmap类型就是存在键值对的结构下面会详细介绍,这个字段我们可以称之为正常桶。如下图所示 |
oldbuckets |
扩容时,存放之前的buckets(Map扩容相关字段) |
extra |
溢出桶结构,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对 |
noverflow |
溢出桶里bmap大致的数量 |
nevacuate |
分流次数,成倍扩容分流操作计数的字段(Map扩容相关字段) |
flags |
状态标识,比如正在被写、buckets和oldbuckets在被遍历、等量扩容(Map扩容相关字段) |
1
2
3
4
5
6
7
8
9
10
11
12
|
// 每一个 bucket 的结构,即 hmap 中 buckets 指向的数据。
type bmap struct {
tophash [bucketCnt]uint8
}
// 编译期间重构此结构
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
|
topbits |
长度为8的数组,[]uint8,元素为:key获取的hash的高8位,遍历时对比使用,提高性能。如下图所示 |
keys |
长度为8的数组,[]keytype,元素为:具体的key值。每个bucket可以存储8个键值对 |
elems |
长度为8的数组,[]elemtype,元素为:键值对的key对应的值。 |
overflow |
指向的hmap.extra.overflow 溢出桶里的bmap ,上面的字段topbits 、keys 、elems 长度为8,最多存8组键值对,存满了就往指向的这个bmap 里存 |
pad |
对齐内存使用的,不是每个bmap都有会这个字段,需要满足一定条件 |
(1)插入key-value
map的赋值流程可总结位如下几步:
map的赋值流程可总结位如下⼏步:
<1> 通过key的hash值后“B”位确定是哪⼀个桶,图中⽰例为5号桶。
<2> 遍历当前桶,通过key的tophash和hash值,防⽌key重复。如果key已存在则直接更新值。如果没找到将key,将key插入到第⼀个可以插⼊的位置,即空位置处存储数据。
<3> 如果当前桶元素已满,会通过overflow链接创建⼀个新的桶,来存储数据。
(2)查询key-value
参考上图,k4的get流程可以归纳为如下⼏步:
<1> 计算k4的hash值。[由于当前主流机都是64位操作系统,所以计算结果有64个⽐特位]
<2> 通过最后的“B”位来确定在哪号桶,此时B为4,所以取k4对应哈希值的后4位,也就是0101,0101⽤⼗进制表⽰为5,所以在5号桶)
<3> 根据k4对应的hash值前8位快速确定是在这个桶的哪个位置(额外说明⼀下,在bmap中存放了每个key对应的tophash,是key的哈希值前8位),⼀旦发现前8位⼀致,则会执⾏下⼀步
<4> 对⽐key完整的hash是否匹配,如果匹配则获取对应value
<5> 如果都没有找到,就去连接的下⼀个溢出桶中找
有很多同学会问这⾥为什么要多维护⼀个tophash,即hash前8位?
这是因为tophash可以快速确定key是否正确,也可以把它理解成⼀种缓存措施,如果前8位都不对了,后⾯就没有必要⽐较了。
七、函数
设计一个程序:
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
|
/*
打印两个六层菱形
期待结果:
*
***
*****
*******
*********
***********
*********
*******
*****
***
*
*
***
*****
*******
*********
***********
*********
*******
*****
***
*
*/
|
如果没有函数,我们的实现方式:
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
|
// 打印菱形
// 层数
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()
}
// 再次打印菱形
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
|
package main
import "fmt"
func printLing(){
// 打印菱形
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 main() {
// 打印菱形
printLing()
// 打印菱形
printLing()
}
|
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)
}
|
声明一个函数并不会执行函数内代码,只是完成一个一个包裹的作用。真正运行函数内的代码还需要对声明的函数进行调用,一个函数可以在任意位置多次调用。调用一次,即执行一次该函数内的代码。
调用语法:
案例:
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、什么是参数
什么是参数,函数为什么需要参数呢?将上面的打印的两个菱形换乘打印一个6行的和一个8行的,如何实现呢?这就涉及到了函数参数。
再比如上面我们将计算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
|
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
|
// 函数声明 两个形参: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
|
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、值传递
7.6.1、赋值操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 案例1
var x = 10
fmt.Printf("x的地址%p\n", &x)
y := x
fmt.Printf("y的地址%p\n", &y)
x = 100
fmt.Println(y)
// 案例2
var a = []int{1, 2, 3}
b := a
a[0] = 100
fmt.Println(b)
// 案例3
var m1 = map[string]string{"name":"yuan"}
var m2 = m1
m2["age"] = "22"
fmt.Println(m1)
|
7.6.2、函数传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import "fmt"
func swap(a int, b int) {
c := a
a = b
b = c
fmt.Println("a", a)
fmt.Println("b", b)
}
func main() {
var x = 10
var y = 20
swap(x, y)
fmt.Println("x", x)
fmt.Println("y", y)
}
|
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"
func func01(x int) {
x = 100
}
func func02(s []int) {
fmt.Printf("func02的s的地址:%p\n",&s)
s[0] = 100
// s = append(s, 1000)
}
func func03(p *int) {
*p = 100
}
func main() {
// 案例1
var x = 10
func01(x)
fmt.Println(x)
// 案例2
var s = []int{1, 2, 3}
fmt.Printf("main的s的地址:%p\n",&s)
func02(s)
fmt.Println(s)
//案例3
var a = 10
var p *int = &a
func03(p)
fmt.Println(a)
}
|
思考之前的scan函数为什么一定传参&
Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。
1
2
|
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
}
|
make
函数创建的map
就是一个指针类型,工作原理类似于案例3,所以map数据和切片数据一样虽然值拷贝但依然可以修改原值。
7.7、匿名函数
匿名函数,顾名思义,没有函数名的函数。
匿名函数的定义格式如下:
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.8、高阶函数
一个高阶函数应该具备下面至少一个特点:
- 将一个或者多个函数作为形参
- 返回一个函数作为其结果
首先明确一件事情:函数名亦是一个变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package main
import (
"fmt"
"reflect"
)
func addCal(x int, y int)int{
return x + y
}
func main() {
var a = addCal
a(2, 3)
fmt.Println(a)
fmt.Println(addCal)
fmt.Println(reflect.TypeOf(addCal)) // func(int, int) int
}
|
结论:函数参数是一个变量,所以,函数名当然可以作为一个参数传入函数体,也可以作为一个返回值。
7.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
|
package main
import (
"fmt"
"time"
)
func timer(f func()){
timeBefore := time.Now().Unix()
f()
timeAfter := time.Now().Unix()
fmt.Println("运行时间:", timeAfter - timeBefore)
}
func foo() {
fmt.Println("foo function... start")
time.Sleep(time.Second * 2)
fmt.Println("foo function... end")
}
func bar() {
fmt.Println("bar function... start")
time.Sleep(time.Second * 3)
fmt.Println("bar function... end")
}
func main() {
timer(foo)
timer(bar)
}
|
注意如果函数参数也有参数该怎么写呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package main
import "fmt"
func add(x,y int ) int {
return x+y
}
func mul(x,y int ) int {
return x*y
}
// 双值计算器
func cal(f func(x,y int) int,x,y int,) int{
return f(x,y)
}
func main() {
ret1 := cal(add,12,3,)
fmt.Println(ret1)
ret2 := cal(mul,12,3,)
fmt.Println(ret2)
}
|
7.8.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.9、闭包
复习函数作用域
7.9.1、闭包函数
闭包并不只是一个go中的概念,在函数式编程语言中应用较为广泛。
首先看一下维基上对闭包的解释:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量(外部非全局)的函数。
简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包函数。
- 闭包就是当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之 外执行。
- 需要注意的是,自由变量不一定是在局部环境中定义的,也有可能是以参数的形式传进局部环境;另外在 Go 中,函数也可以作为参数传递,因此函数也可能是自由变量。
- 闭包中,自由变量的生命周期等同于闭包函数的生命周期,和局部环境的周期无关。
- 闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
实现一个计数器函数,不考虑闭包的情况下,最简单的方式就是声明全局变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package main
import "fmt"
var i = 0
func counter() {
i++
fmt.Println(i)
}
func main() {
counter()
counter()
counter()
}
|
这种方法的一个缺点是全局变量容易被修改,安全性较差。闭包可以解决这个问题,从而实现数据隔离
。
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 getCounter() func() {
var i = 0
return func() {
i++
fmt.Println(i)
}
}
func main() {
counter := getCounter()
counter()
counter()
counter()
counter2 := getCounter()
counter2()
counter2()
counter2()
}
|
getCounter完成了对变量i以及counter函数的封装,然后重新赋值给counter变量,counter函数和上面案例的counter函数的区别就是将需要操作的自由变量转化为闭包环境。
7.9.2、闭包函数应用案例
在go语言中可以使用闭包函数来实现装饰器
案例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
|
package main
import (
"fmt"
"reflect"
"runtime"
)
// 函数计数器
func getCounter(f func()) func() {
calledNum := 0 // 数据隔离
return func() {
f()
calledNum += 1
// 获取函数名
fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
fmt.Printf("函数%s第%d次被调用\n", fn, calledNum)
}
}
// 测试的调用函数
func foo() {
fmt.Println("foo function执行")
}
func bar() {
fmt.Println("bar function执行")
}
func main() {
/*fooAndCounter := getCounter(foo) // 针对foo的计数器
fooAndCounter()
fooAndCounter()
fooAndCounter()
barAndCounter := getCounter(bar)
barAndCounter()
barAndCounter()
barAndCounter()*/
foo := getCounter(foo) // 开放原则
foo()
foo()
foo()
bar := getCounter(bar)
bar()
bar()
}
|
案例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
|
package main
import (
"fmt"
"time"
)
func GetTimer(f func(t time.Duration)) func(duration time.Duration) {
return func(t time.Duration) {
t1 := time.Now().Unix()
f(t)
t2 := time.Now().Unix()
fmt.Println("运行时间:", t2-t1)
}
}
func foo(t time.Duration) {
fmt.Println("foo功能开始")
time.Sleep(time.Second * t)
fmt.Println("foo功能结束")
}
func bar(t time.Duration) {
fmt.Println("bar功能开始")
time.Sleep(time.Second * t)
fmt.Println("bar功能结束")
}
func main() {
var foo = GetTimer(foo)
foo(3)
var bar = GetTimer(bar)
bar(2)
}
|
关键点:将一个功能函数作为自由变量与一个装饰函数封装成一个整体
作为返回值,赋值给一个新的函数变量,这个新的函数变量在调用的时候,既可以完成原本的功能函数又可以实现装饰功能。
7.10、defer语句
defer语句是go语言提供的一种用于注册延迟调用的机制,是go语言中一种很有用的特性。
7.10.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
|
// 打开文件
file_obj,err:=os.Open("满江红")
if err != nil {
fmt.Println("文件打开失败,错误原因:",err)
}
// 关闭文件
defer file_obj.Close()
// 操作文件
|
7.10.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.10.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++
// 案例3
x := 10
defer func() {
fmt.Println(x) // 保留x的地址
}()
x++
|
当执行defer语句时,函数调用不会马上发生,会先把defer注册的函数及变量拷贝到defer栈中保存,直到函数return前才执行defer中的函数调用。需要格外注意的是,这一拷贝拷贝的是那一刻函数的值和参数的值。注册之后再修改函数值或参数值时,不会生效。
7.10.4、defer执行时机
在Go语言的函数 return 语句不是原子操作,而是被拆成了两步
rval = xxx
ret
而 defer 语句就是在这两条语句之间执行,也就是
1
2
3
4
5
6
7
8
|
rval = xxx
defer_func
ret rval
defer x = 100
x := 10
return x // rval=10. x = 100, ret rval
|
经典面试题:
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"
func f1() int {
i := 5
defer func() {
i++
}()
return i
}
func f2() *int {
i := 5
defer func() {
i++
fmt.Printf(":::%p\n", &i)
}()
fmt.Printf(":::%p\n", &i)
return &i
}
func f3() (result int) {
defer func() {
result++
}()
return 5 // result = 5;ret result(result替换了rval)
}
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) {
fmt.Println(&r)
defer func(r int) {
r = r + 1
fmt.Println(&r)
}(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())
// println(f7())
}
|
在命名返回方式中,最终函数返回的就是命名返回变量的值,因此,对该命名返回变量的修改会影响到最终的函数返回值!
7.11、递归函数
一种计算过程,如果其中每一步都要用到前一步或前几步的结果,称为递归的。用递归过程定义的函数,称为递归函数,例如连加、连乘及阶乘等。
递归特性:
- 调用自身函数
- 必须有一个明确的结束条件
- 在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返 回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
案例1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
import "fmt"
func factorial(n int)int{
if n == 0{
return 1
}
return n * factorial(n-1)
}
func main() {
// 计算n的阶乘,即 n!
var ret = factorial(4)
fmt.Println(ret)
}
|
案例2
这个数列生成规则很简单,每一项都是前两项的和,举例
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package main
import "fmt"
func fib(n int) int {
if n == 2 || n == 1 {
return 1
}
return fib(n-1) + fib(n-2)
}
func main() {
// 计算n的阶乘,即 n!
ret:=fib(6)
fmt.Println(ret)
}
|
7.12、章节练习
添加客户:
查看客户和删除客户:
修改客户:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
// 构建数据存储结构
var customers []map[string]interface{}
var customersId int
func findById(id int) int {
index := -1
//遍历this.customers切⽚
for i := 0; i < len(customers); i++ {
if customers[i]["cid"] == id {
index = i
}
}
return index
}
func isBack() bool {
// 引导用户选择继续还是返回
fmt.Print("请问是否返回上一层【Y/N】:")
var backChoice string
fmt.Scan(&backChoice)
if strings.ToUpper(backChoice) == "Y" {
return true
} else {
return false
}
}
func inputInfo() (string, string, int8, string) {
var name string
fmt.Print("请输入客户姓名:")
fmt.Scan(&name)
var gender string
fmt.Print("请输入客户性别:")
fmt.Scan(&gender)
var age int8
fmt.Print("请输入客户年龄:")
fmt.Scan(&age)
var email string
fmt.Print("请输入客户邮箱:")
fmt.Scan(&email)
return name, gender, age, email
}
func addCustomer() {
for true {
// 引导用户输入学号和姓名
fmt.Printf("\033[1;35;40m%s\033[0m\n", "---------------------------添加客户开始-----------------------------")
name, gender, age, email := inputInfo()
// 创建客户的map对象
customersId++ // 客户编号不需要输入,系统自增即可
newCustomer := map[string]interface{}{
"cid": customersId,
"name": name,
"gender": gender,
"age": age,
"email": email,
}
// 添加客户map对象添加到客户切片中
customers = append(customers, newCustomer)
fmt.Printf("\033[1;35;40m%s\033[0m\n", "---------------------------添加客户完成-----------------------------")
b := isBack()
if b {
break
}
}
}
func listCustomer() {
for true {
fmt.Printf("\033[1;32;40m%s\033[0m\n", "----------------------------------客户列表开始-----------------------------------")
for _, customer := range customers {
fmt.Printf("编号:%-8d 姓名:%-8s 性别:%-8s 年龄:%-8d 邮箱:%-8s \n",
customer["cid"], customer["name"], customer["gender"], customer["age"], customer["email"])
}
fmt.Printf("\033[1;32;40m%s\033[0m\n", "----------------------------------客户列表完成-----------------------------------")
b := isBack()
if b {
break
}
}
}
func updateCustomer() {
fmt.Printf("\033[1;36;40m%s\033[0m\n", "---------------------------客户修改开始----------------------------")
for true {
var updateCid int
fmt.Print("请输入更新客户编号:")
fmt.Scan(&updateCid)
updateIndex := findById(updateCid)
if updateIndex == -1 {
fmt.Println("删除失败,输入的编号ID不存在")
continue
}
fmt.Println("请输入修改客户的信息")
name, gender, age, email := inputInfo()
customers[updateIndex]["name"] = name
customers[updateIndex]["gender"] = gender
customers[updateIndex]["age"] = age
customers[updateIndex]["email"] = email
fmt.Printf("\033[1;36;40m%s\033[0m\n", "---------------------------客户修改完成----------------------------")
b := isBack()
if b {
break
}
}
}
func deleteCustomer() {
fmt.Printf("\033[1;31;40m%s\033[0m\n", "---------------------------删除客户开始----------------------------")
var delCid int
fmt.Print("请输入删除客户编号:")
fmt.Scan(&delCid)
delIndex := findById(delCid)
if delIndex == -1 {
fmt.Println("删除失败,输入的编号ID不存在")
return
}
customers = append(customers[:delIndex], customers[delIndex+1:]...)
fmt.Printf("\033[1;31;40m%s\033[0m\n", "---------------------------删除客户完成----------------------")
}
var data = make(map[string]map[string]string)
func main() {
for true {
fmt.Printf("\033[1;33;40m%s\033[0m\n", `
----------------客户信息管理系统--------------
1、添加客户
2、查看客户
3、更新客户
4、删除客户
5、退出
-------------------------------------------
`)
var choice int
fmt.Printf("\033[1;38;40m%s\033[0m", "请输入选择【1-5】:")
stdin := bufio.NewReader(os.Stdin)
fmt.Fscan(stdin, &choice)
switch choice {
case 1:
addCustomer()
case 2:
listCustomer()
case 3:
updateCustomer()
case 4:
deleteCustomer()
default:
fmt.Println("非法输入!")
os.Exit(0)
}
}
}
|
八、文件操作
8.1、编码
众所周知,计算机起源于美国,英文只有26个字符,算上其他所有特殊符号也不会超过128个。字节是计算机的基本储存单位,一个字节(bytes)包括八个比特位(bit),能够表示出256个二进制数字,所以美国人在这里只是用到了一个字节的前七位即127个数字来对应了127个具体字符,而这张对应表就是ASCII码字符编码表,简称ASCII表。后来为了能够让计算机识别拉丁文,就将一个字节的最高位也应用了,这样就多扩展出128个二进制数字来对应新的符号。这张对应表因为是在ASCII表的基础上扩展的最高位,因此称为扩展ASCII表。到此位置,一个字节能表示的256个二进制数字都有了特殊的符号对应。
但是,当计算机发展到东亚国家后,问题又出现了,像中文,韩文,日文等符号也需要在计算机上显示。可是一个字节已经被西方国家占满了。于是,我中华民族自己重写一张对应表,直接生猛地将扩展的第八位对应拉丁文全部删掉,规定一个小于127的字符的意义与原来相同,即支持ASCII码表,但两个大于127的字符连在一起时,就表示一个汉字,这样就可以将几千个汉字对应一个个二进制数了。而这种编码方式就是GB2312,也称为中文扩展ASCII码表。再后来,我们为了对应更多的汉字规定只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。这样能多出几万个二进制数字,就算甲骨文也能够用了。而这次扩展的编码方式称为GBK标准。当然,GBK标准下,一个像”苑”这样的中文符号,必须占两个字节才能存储显示。
与此同时,其它国家也都开发出一套编码方式,即本国文字符号和二进制数字的对应表。而国家彼此间的编码方式是互不支持的,这会导致很多问题。于是ISO国际化标准组织为了统一编码,统计了世界上所有国家的字符,开发出了一张万国码字符表,用两个字节即六万多个二进制数字来对应。这就是Unicode编码方式。这样,每个国家都使用这套编码方式就再也不会有计算机的编码问题了。Unicode的编码特点是对于任意一个字符,都需要两个字节来存储。这对于美国人而言无异于吃上了世界的大锅饭,也就是说,如果用ASCII码表,明明一个字节就可以存储的字符现在为了兼容其他语言而需要两个字节了,比如字母I,本可以用01001001来存储,现在要用Unicode只能是00000000 01001001存储,而这将导致大量的空间被浪费掉。基于此,美国人创建了utf8编码,而utf8编码是一种针对Unicode的可变长字符编码方式,根据具体不同的字符计算出需要的字节,对于ASCII码范围的字符,就用一个字节,而且符号与数字的对应也是一致的,所以说utf8是兼容ASCII码表的。但是对于中文,一般是用三个字节存储的。
8.2、Go的字符与字节
byte就是字节的意思,一个字节就是8个二进制位。uint8,无符号整形,占8位,正好也是2的8次方。所以byte
和 uint8
类型本质上没有区别,它表示的是 ACSII 表中的一个字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// byte类型
var b1 byte
b1 = 'A' // 必须是单引号
// b1 = 98 // 必须是单引号
fmt.Println(reflect.TypeOf(b1)) // 65 uint8
fmt.Printf("%c\n",b1)
fmt.Printf("%d\n",b1) // ASCII数字
fmt.Println(b1) // ASCII数字
// uint8类型
var b2 uint8
b2 = 65
// b2 = 'c'
fmt.Printf("%c\n",b2)
fmt.Printf("%d\n",b2)
fmt.Println(b2) // ASCII数字
// var b3 byte
var b3 rune
b3 = '苑'
// rune,占用4个字节,共32位比特位,所以它和 int32 本质上也没有区别。它表示的是一个 Unicode字符
fmt.Println(b3,string(b3),reflect.TypeOf(b3))
|
8.3、字符串
go语⾔的string是⼀种数据类型,这个数据类型占⽤16字节空间,前8字节是⼀个指针,指向字符串值的地址,后⼋个字节是⼀个整数,标识字 符串的长度;
(1)字符串的存储原理
string 数据结构:源码包src/runtime/string.go:stringStruct
定义了string的数据结构:
1
2
3
4
|
type stringStruct struct {
str unsafe.Pointer
len int
}
|
其数据结构很简单:
stringStruct.str:字符串的首地址;
stringStruct.len:字符串的长度;
string数据结构跟切片有些类似,只不过切片还有一个表示容量的成员,事实上string和切片,准确的说是byte切片经常发生转换。这个后面再详细介绍。
1
2
3
4
5
6
|
s1 := "hello"
s2 := s1[:]
s3 := s1[1:]
fmt.Println(&s1, (*reflect.StringHeader)(unsafe.Pointer(&s1)))
fmt.Println(&s2, (*reflect.StringHeader)(unsafe.Pointer(&s2)))
fmt.Println(&s3, (*reflect.StringHeader)(unsafe.Pointer(&s3)))
|
1
2
3
4
5
|
/*
A string type represents the set of string values. A string value is a (possibly empty) sequence of bytes. Strings are immutable: once created, it is impossible to change the contents of a string. The predeclared string type is string.
The length of a string s (its size in bytes) can be discovered using the built-in function len. The length is a compile-time constant if the string is a constant. A string's bytes can be accessed by integer indices 0 through len(s)-1. It is illegal to take the address of such an element; if s[i] is the i'th byte of a string, &s[i] is invalid.
*/
|
字符串类型表示字符串值的集合。字符串值是一个字节序列(可能为空)。字符串是不可变的:一旦创建,就不可能改变字符串的内容。预先声明的字符串类型是string。
字符串s的长度(以字节为单位的大小)可以使用内置函数len来发现。如果字符串是常量,则长度为编译时常量。字符串的字节可以通过索引0到len(s)-1的整数来访问。取这样一个元素的地址是非法的;如果s[i]是字符串的第i个字节,&s[i]是无效的。
go语⾔指针和C/C++指针的唯⼀差别就是:go语⾔不允许对指针做算术运算(+、-、++、–)。
但是,Go提供了⼀套底层库reflect和unsafe,它们可以把任意⼀个go指针转成uintptr类型的值,然后再像C/C++⼀样对指针做算术运算,最后再还原成go类型。所以从这个⾓度上看,go指针也是可以和C/C++指针⼀样使⽤的,只是会⽐较绕,这同时也要求使⽤者⾃⼰明⽩,如果真要把指针这么⽤,那么请记得后果⾃负。
(2)字符串的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 本质上,unicode是一个编码集,和ascii码相同,而utf8是编码规则
var a = '苑'
fmt.Printf("字符'苑'unicode的十进制:%d\n", a)
fmt.Printf("字符'苑'unicode的十六进制:%x\n", a)
fmt.Printf("字符'苑'unicode的二进制:%b\n", a)
var b = 0b111010001000101110010001
fmt.Printf("字符'苑'的utf8:%x\n", b)
var c = "苑abc"
fmt.Println(c) // 苑abc
for i := 0; i < len(c); i++ {
fmt.Printf("%d\n", c[i]) // 存储的字节的十进制数
}
for _, v := range c {
fmt.Printf("%d,%c\n", v, v) // 通过存储的utf8解析到unicode值和对应的符号
}
|
UTF-8的编码规则:
(1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
(2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
举例说明:
已知’苑’的unicode是82d1(1000001011010001),‘苑’的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从’苑’的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,‘苑’的UTF-8编码是 “111010001 00010111 0010001”,转换成十六进制就是e88b91。
(3)字符串与字节串的转换
字节数组,就是一个数组,里面每一个元素都是字符,字符又跟字节划等号。所以字符串和字节数组之间可以相互转化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// (1) 字符串类型(string) 转为字节串类型([]byte)
var s = "苑昊"
fmt.Println(s,reflect.TypeOf(s)) // 苑昊 string
var b = []byte(s) // 默认用uft-8进行编码
fmt.Println(b,reflect.TypeOf(b)) // [232 139 145 230 152 138] []uint8
// 可以通过代码 len([]rune(s)) 来获得字符串中字符的数量, 但使用 utf8.RuneCountInString(s) 效率会更高一点.
s := "Hello,世界"
r1 := []byte(s)
r2 := []rune(s)
fmt.Println(r1) // 输出:[72 101 108 108 111 44 32 228 184 150 231 149 140]
fmt.Println(r2) // 输出:[72 101 108 108 111 44 32 19990 30028]
// 统计字节个数
fmt.Println(len(r1))
// 统计字符个数
fmt.Println(len(r2))
fmt.Println(utf8.RuneCountInString(s))
// (2) byte转为string
fmt.Println(string(b))
var data = []byte{121,117,97,110}
fmt.Println(string(data)) // yuan
|
这里的转化不是将string结构体中指向的byte切片直接做赋值操作,而是通过copy实现的,在数据量比较大时,这里的转化会比较耗费内存空间。
(4)练习
将字符串 “hello” 转换为 “cello”
1
2
3
4
|
s := "hello"
c := []byte(s)
c[0] = 'c'
s2 := string(c) //s2 == "cello"
|
将字符串 “hello” 反转
1
2
3
4
5
6
7
8
9
|
func reverseString(s []byte) []byte {
var i, j = 0, len(s) - 1
for i < j {
s[i], s[j] = s[j], s[i]
i++
j--
}
return s
}
|
8.3、读写文件
8.3.1、打开文件
os.Open()
函数能够打开一个文件,返回一个*File
和一个err
。
1
2
3
4
5
6
7
|
//打开文件
file, err := os.Open("./满江红")
if err != nil {
fmt.Println("err: ", err)
}
//关闭文件句柄
defer file.Close()
|
8.3.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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
)
func readBytes(file *os.File) {
var b = make([]byte, 3)
n, err := file.Read(b)
if err != nil {
fmt.Println("err:", err)
return
}
fmt.Printf("读取字节数:%d\n", n)
fmt.Printf("切片值:%v\n", b)
fmt.Printf("读取内容:%v\n", string(b[:n]))
}
func readLines(file *os.File) {
reader := bufio.NewReader(file)
for {
// (1) 按行都字符串
strs, err := reader.ReadString('\n') // 读取到换行符为止,读取内容包括换行符
fmt.Print(err, strs)
// (2) 按行都字节串
// bytes, err := reader.ReadBytes('\n')
// fmt.Print(bytes)
// fmt.Print(string(bytes))
if err == io.EOF { //io.EOF 读取到了文件的末尾
// fmt.Println("读取到文件末尾!")
break
}
}
}
func readFile() {
content, err := ioutil.ReadFile("满江红") //包含了打开文件和读取整个文件,适用于较小文件
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Print(string(content))
}
func main() {
//打开文件
file, err := os.Open("满江红") // 相对路径或者绝对路径
if err != nil {
fmt.Println("err: ", err)
}
//关闭文件句柄
defer file.Close()
// (1) 按字节读取数据
// readBytes(file)
// (2) 按行读取文件
// readLines(file)
// (3) 读取整个文件
// readFile()
}
|
8.3.3、写文件
OpenFile是一个更一般性的文件打开函数,大多数调用者都应用Open或Create代替本函数。它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于I/O。如果出错,错误底层类型是*PathError。
1
2
3
4
5
6
7
8
9
10
11
12
|
func OpenFile(name string, flag int, perm FileMode) (file *File, err error) // ⽂件路径、打开模式、⽂件权限
/*
os.O_RDONLY: 只读模式(read-only)
os.O_WRONLY: 只写模式(write-only)
os.O_RDWR : 读写模式(read-write)
os.O_APPEND: 追加模式(append)
os.O_CREATE: ⽂件不存在就创建(create a new file if none exists.)
os.O_TRUNC: 打开并清空⽂件(必须有写权限)
os.O_EXCL: 如与 O_CREATE ⼀起⽤,构成⼀个新建⽂件的功能,它要求⽂件必须不存在(used with O_CREATE, file must not exist)
os.O_SYNC:同步⽅式打开,即不使⽤缓存,直接写⼊硬盘
*/
|
(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
|
package main
import (
"bufio"
"fmt"
"io/ioutil"
"os"
)
func writeBytesOrStr(file *os.File) {
str := "满江红666\n"
//写入字节切片数据
file.Write([]byte(str))
//直接写入字符串数据
file.WriteString("怒发冲冠,凭栏处、潇潇雨歇。")
}
func writeByBufio(file *os.File) {
writer := bufio.NewWriter(file)
//将数据先写入缓存,并不会到文件中
writer.WriteString("大浪淘沙\n")
// 必须flush将缓存中的内容写入文件
// writer.Flush()
}
func writeFile() {
str := "怒发冲冠,凭栏处、潇潇雨歇。"
err := ioutil.WriteFile("满江红", []byte(str), 0666)
if err != nil {
fmt.Println("write file failed, err:", err)
return
}
}
func main() {
file, err := os.OpenFile("满江红.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
// 写字节或者字符串
writeBytesOrStr(file)
// flush写
writeByBufio(file)
// 写文件
writeFile()
}
|
0777:-rwxrwxrwx,创建了一个普通文件,所有人拥有所有的读、写、执行权限
0666:-rw-rw-rw-,创建了一个普通文件,所有人拥有对该文件的读、写权限,但是都不可执行
0644:-rw-r–r–,创建了一个普通文件,文件所有者对该文件有读写权限,用户组和其他人只有读权限,没有执行权限
(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
|
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
func main() {
file, err := os.OpenFile("读写满江红", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
reader := bufio.NewReader(file)
writer := bufio.NewWriter(file)
for true {
// (1) 按行都字符串
strs, err := reader.ReadString('\n') // 读取到换行符为止,读取内容包括换行符
content := strings.Trim(strs, "\n")
s := fmt.Sprintf("\n该行长度为%d,内容为:%s", len([]rune(content)), content)
// (2) 将行数记录追加进入文件
writer.WriteString(s)
writer.Flush()
if err == io.EOF {
break
}
}
}
|
8.4、其它文件操作
(1) 删除文件
os.Remove(fname)
(2) 创建目录
dname :=“rain”
os.Mkdir(dname,os.ModeDir|os.ModePerm)
(3)获取文件信息
通过os.Stat
方法,我们可以获取文件的信息,比如文件大小、名字等。
1
2
3
4
5
6
7
8
9
10
|
func main() {
f,err:=os.Stat("满江红")
if err ==nil {
fmt.Println("name:",f.Name())
fmt.Println("size:",f.Size())
fmt.Println("is dir:",f.IsDir())
fmt.Println("mode::",f.Mode())
fmt.Println("modTime:",f.ModTime())
}
}
|
以上就是可以获取到的文件信息,还包括判断是否是目录,权限模式和修改时间。所以我们对于文件的信息获取要使用os.Stat
函数,它可以在不打开文件的情况下,高效获取文件信息。
os.Stat
函数有两个返回值,一个是文件信息,一个是err
,通过err
我们可以判断文件是否存在。首先,err==nil
的时候,文件肯定是存在的;其次err!=nil
的时候也不代表不存在,通过err是否等于os.IsNotExist
来判断一个文件不存在。
九、结构体
在实际开发中,我们可以将一组类型不同的、但是用来描述同一件事物的变量放到结构体中。例如,在校学生有姓名、年龄、身高、成绩等属性,学了结构体后,我们就不需要再定义多个变量了,将它们都放到结构体中即可。
在Go语言中,结构体承担着面向对象语言中类的作用。Go语言中,结构体本身仅用来定义属性。还可以通过接收器函数来定义方法,使用内嵌结构体来定义继承。这样使用结构体相关操作Go语言就可以实现OOP
面向对象编程了。
9.1、声明结构体
Go语言通过type
和struct
关键字声明结构体,格式如下:
1
2
3
4
5
|
type 类型名 struct { // 标识结构体的类型名,在同一个包内不能重复
字段1 字段1类型 // 字段名必须唯一
字段2 字段2类型
…
}
|
Go语言结构体(Struct
)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、char、float 等基本类型组成的。你可以认为结构体是一种聚合类型
。
1
2
3
4
5
6
7
|
type Student struct {
sid int
name string
age int8
course []string // 选秀课程
}
|
Go 语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性。结构体成员,也可称之为成员变量,字段,属性。属性要满足唯一性。
同类型的变量也可以写在一行,用逗号隔开
1
2
3
4
|
type Book struct {
title,author string
price int
}
|
9.2、结构体的实例化
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。
实例化方式包括如下几种。
9.2.1、声明结构体变量再赋值
结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package main
import "fmt"
type Student struct {
sid int
name string
age int8
course []string // 选秀课程
}
func main() {
// 声明一个结构体对象 ,值类型,默认开辟空间,字段赋予零值
var s Student
fmt.Println("s:", s)
// 要访问结构体成员,需要使用点号 . 操作符
fmt.Println(s.name)
// 更改成员变量的值
s.name = "yuan"
fmt.Println(s.name)
// s.course[0] = "chinese" // 结果,如何调整
}
|
1、结构体属于值类型,即var声明后会像整形字符串一样创建内存空间。
2、创建结构体对象如果没有给字段赋值,则默认零值(字符串默认 “",数值默认0,布尔默认false,切片和map默认nil对象)
结构体的内存存储:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package main
import "fmt"
type Student struct {
sid int
name string
age int8
course []string // 选秀课程
}
func main() {
// 声明一个结构体对象 ,值类型,默认开辟空间,字段赋予零值
var s Student
fmt.Println("s:", s)
s.sid = 1001
s.name = "yuan"
s.age = 23
s.course = []string{"chinese", "math", "english"}
fmt.Printf("%p\n", &s)
fmt.Printf("%p\n", &(s.sid))
fmt.Printf("%p\n", &(s.name))
fmt.Printf("%p\n", &(s.age))
fmt.Printf("%p\n", &(s.course)) // 切片24个字节
}
|
之前我们学习过值类型和引用类型,知道值类型是变量对应的地址直接存储值,而引用类型是变量对应地址存储的是地址。因为结构体因为是值类型,所以p的地址与存储的第一个值的地址是相同的,而后面每一个成员变量的地址是连续的。
9.2.2、实例化之 结构体
1
2
3
4
5
6
7
8
9
10
|
// (1) 方式1
s1 := Student{}
s1.sid = 1001
s1.name = "yuan"
// (2) 方式2:键值对赋值
s2 := Student{sid: 1002, name: "rain", course: []string{"chinese", "math", "english"}}
fmt.Println(s2)
// (3) 方式3:多值赋值
s3 := Student{1003, "alvin", 22, []string{"chinese", "math", "english"}}
fmt.Println(s3)
|
1、结构体可以使用“键值对”(Key value pair)初始化字段,每个“键”(Key)对应结构体中的一个字段,键的“值”(Value)对应字段需要初始化的值。键值对的填充是可选的,不需要初始化的字段可以不填入初始化列表中,走默认值。
2、多值初始化方式必须初始化结构体的所有字段且每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3、键值对与值列表的初始化形式不能混用。
9.2.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
package main
import "fmt"
type Student struct {
sid int
name string
age int8
course []string // 选秀课程
}
func CourseInit(stu Student) {
stu.course = []string{"chinese", "math", "english"}
fmt.Println(stu)
}
func CourseInit2(stu *Student) {
(*stu).course = []string{"chinese", "math", "english"}
}
func main() {
// 案例1
s1 := Student{sid: 1001, name: "alvin", age: 32}
s2 := s1 // 值拷贝
fmt.Println(s2)
s1.age = 100
fmt.Println(s2.name)
// 如果希望s3的值跟随s2保持一致怎么实现
s3 := &s1 // var s4 *Student = &s2
s1.age = 100
fmt.Println((*s3).age)
fmt.Println(s3.age)
// 案例2
var s4 = Student{sid: 1001, name: "alvin", age: 32}
CourseInit(s4)
fmt.Println("s报的课程:", s4.course)
// 怎么能初始化成功呢?
var s5 = &Student{sid: 1001, name: "alvin", age: 32}
CourseInit2(s5)
fmt.Println("s报的课程:", (*s5).course) // *s.course的写法是错误的
fmt.Println("s报的课程:", s5.course)
}
|
在Go语言中,结构体指针的变量可以继续使用.
,这是因为Go语言为了方便开发者访问结构体指针的成员变量可以像访问结构体的成员变量一样简单,使用了语法糖(Syntactic sugar)技术,将 instance.Name
形式转换为 (*instance).Name
。
9.2.4、实例化之 new(结构体)
Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。使用 new 的格式如下:其中:
其中:
- T 为类型,可以是结构体、整型、字符串等。
instance
:T 类型被实例化后保存到 instance
变量中,instance
的类型为 *T,属于指针。
1
2
3
4
5
6
|
s := new(Student) // &Student{}
fmt.Println(reflect.TypeOf(s)) // *Student
fmt.Println(s) // *Student
s.name = "yuan"
fmt.Println((*s).name)
fmt.Println(s.name)
|
9.4、模拟构造函数
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
|
package main
import "fmt"
type Student struct {
sid int
name string
age int8
course []string // 选秀课程
}
func NewStudent(sid int, name string, age int8, course []string) *Student {
return &Student{
sid: sid,
name: name,
age: age,
course: course,
}
}
func main() {
s := NewStudent(1001, "yuan", 32, nil)
fmt.Println(s)
}
|
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package main
import "fmt"
type Player struct {
Name string
HealthPoint int
Level int
NowPosition []int
Prop []string
}
func NewPlayer(name string, hp int, level int, np []int, prop []string) *Player {
return &Player{
name,
hp,
level,
np,
prop,
}
}
func (p Player) attack() {
fmt.Printf("%s发起攻击!\n", p.Name)
}
func (p *Player) attacked() {
fmt.Printf("%s被攻击!\n", p.Name)
p.HealthPoint -= 10
fmt.Println(p.HealthPoint)
}
func (p *Player) buyProp(prop string) {
p.Prop = append(p.Prop, prop)
fmt.Printf("%s购买道具!\n", p.Name)
}
func main() {
player := NewPlayer("yuan", 100, 100, nil, nil)
player.attack()
player.attacked()
fmt.Println(player.HealthPoint)
player.buyProp("魔法石")
fmt.Println(player.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
14
15
16
17
|
package main
import "fmt"
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
25
26
27
28
|
package main
import "fmt"
type Addr struct {
country string
province string
city string
}
type Person struct {
name string
age int
Addr
}
func main() {
p1 := Person{
"yuan",
18,
Addr{"中国", "广东省", "深圳"},
}
fmt.Printf("%#v\n", p1) //main.Person{string:"北京", int:18}
fmt.Println(p1.name, p1.age) // yuan 18
fmt.Println(p1.Addr)
fmt.Println(p1.Addr.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
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)
}
// Cat 类型
type Cat struct {
*Animal
}
func (c *Cat) climbTree() {
fmt.Printf("%s is climb tree ~\n", c.name)
}
func main() {
d1 := &Dog{
Kind: "金毛",
Animal: &Animal{ //注意嵌套的是结构体指针
name: "旺财",
},
}
d1.eat()
d1.bark()
c1 := &Cat{
Animal: &Animal{
name: "喵喵",
},
}
c1.sleep()
c1.climbTree()
}
|
9.8、序列化
序列化: 通过某种方式把数据结构或对象写入到磁盘文件中或通过网络传到其他节点的过程。
反序列化:把磁盘中对象或者把网络节点中传输的数据恢复为python的数据对象的过程。
9.8.1、json初识
序列化最重要的就是json序列化。
JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。
go语言数据类型 |
json支持的类型 |
整型、浮点型 |
整型、浮点型 |
字符串(在双引号中) |
字符串(双引号) |
逻辑值(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
38
39
40
41
42
43
44
45
46
47
48
|
package main
import (
"encoding/json"
"fmt"
)
type Addr struct {
Province string
City string
}
type Stu struct {
Name string `json:"name"` // 结构体的标签
Age int `json:"-"` // 表示不参与序列化
Addr Addr
}
func main() {
var stuMap = map[string]interface{}{"name": "yuan", "age": 32, "addr": "beijing"}
var stuStruct = Stu{Name: "yuan", Age: 18, Addr: Addr{Province: "Hebei", City: "langFang"}}
// 序列化
jsonStuMap, _ := json.Marshal(stuMap)
jsonStuStruct, _ := json.Marshal(stuStruct)
fmt.Println(string(jsonStuMap))
fmt.Println(string(jsonStuStruct))
// 反序列化
// var x = make(map[int]string)
var StuMap map[string]interface{}
err := json.Unmarshal(jsonStuMap, &StuMap)
if err != nil {
return
}
fmt.Println("StuMap", StuMap, StuMap["name"])
var StuStruct Stu
err := json.Unmarshal(jsonStuStruct, &StuStruct)
if err != nil {
return
}
fmt.Println(StuStruct)
fmt.Println(StuStruct.Name)
fmt.Println(StuStruct.Addr.City)
}
|
9.9、章节作业
将客户关系管理系统改为结构体版本
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
|