🗒️Golang基础学习笔记
00 分钟
2023-9-14
2023-12-9
type
status
date
slug
summary
tags
category
icon
password

Golang基础

输出语句

GO中要打印一个值需要引入fmt包
fmt包里面给我们提供了一些常见的打印数据的方法,比如:Print、Println、Printf,在我 们实际开发中Println、Printf用的非常多。

Print和Println区别

一次输入多个值的时候Println中间有空格Print没有
Println会自动换行,Print不会

Println和Printf区别

Printf是格式化输出,在很多场景下比Println更方便,举个例子:
%d是占位符,表示数字的十进制表示。Printf中的占位符与后面的数字变量一一对应。

Golang中常见的变量定义方法

1、var定义变量
2、类型推导方式定义变量
a在函数内部,可以使用更简略的:=方式声明并初始化变量。
注意:短变量只能用于声明局部变量,不能用于全局变量的声明
3、G0语言中的注释
win下面ctrl+/可以快速的注释一样,mac下面command+/也可以快速的注释一样
格式化输出
查看输出语句中的所有占位符及其用法

GO变量、常量、变量命名规则

GO语言中的变量

GO语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。GO语言中关键字和保留字都不能用作变量名。
GO语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。并且GO语言的变量声明后必须使用。
1、变量的来历
程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。
2、变量类型
变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个 多世纪的发展,编程语言己经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。
GO语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。
var声明变量
短变量声明法
在函数内部,可以使用更简略的:=方式声明并初始化变量。
注意:短变量只能用于声明局部变量,不能用于全局变量的声明
多个变量可以一起声明

GO语言中的常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。
1、使用const定义常量
声明了pi和e这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。
多个常量可以一起声明
const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。例如:
2、const常量结合iota的使用(了解)
iota是golang语言的常量计数器,只能在常量的表达式中使用。
iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
1、iota只能在常量的表达式中使用。
2、每次const出现时,都会让iota初始化为0.【自增长】

GO语言变量、常量命名规则

1、变量名称必须由数字、字母、下划线组成。
2、标识符开头不能是数字
3、标识符不能是保留字和关键字。
4、变量的名字是区分大小写的如:age和Age是不同的变量。在实际的运用中,也建议,不要用一个单词大小写区分两个变量。
5、标识符(变量名称)一定要见名思意:变量名称建议用名词,方法名称建议用动词
6、变量命名一般采用驼峰式,当遇到特有名词(缩写或简称,如DNS)的时候,特有名词根据是否私有全部大写或小写。

GO语言代码风格

1、代码每一行结束后不用写分号(;)
2、运算符左右建议各加一个空格
3、GO语言程序员推荐使用驼峰式命名
当名字有几个单词组成的时优先使用大小写分隔
4、强制的代码风格
左括号必须紧接着语句不换行,这个特性刚开始会使开发者不习惯,但随着对G0语言的 不断熟悉,就会发现风格统一让大家在阅读代码时把注意力集中到了解决问题上,而不是代码风格上
5、go fmt主要用于格式化文档,让所有人的代码风格保持一致

数据类型

Golang数据类型介绍

G0语言中数据类型分为:基本数据类型和复合数据类型
基本数据类型有:
  • 整型、浮点型、布尔型、字符串
复合数据类型有:
  • 数组、切片、结构体、函数、map、通道(channel)、接口等。

整型(int)

整型分为以下两个大类:
有符号整形按长度分为:int8、int16、int32、int64
对应的无符号整型:uint8、uint16、uint32、uint64
notion image
关于字节:
字节也叫Byte,是计算机数据的基本存储单位。8bit(位)=1Byte(字节) 1024Byte(字节)=1KB 1024KB=1MB 1024M=1GB 1024GB=1TB。在电脑里一个中文字是占两个字节的。
特殊整型
notion image
unsafe.Sizeof
unsafe.Sizeof(n1)是unsafe包的一个函数,可以返回n1变量占用的字节数
无法查看string类型数据所占用的储存空间
int不同长度直接的转换
注意:较大的数据类型转为小数据类型若超出小数据类型范围,则转换为错误结果
数字字面量语法(Number literals syntax)(了解)
G01.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,例如:
v:=0b00101101,代表二进制的101101,相当于十进制的45。

浮点型(float64,float32)

Go语言支持两种浮点型数:float32和float64。这两种浮点型数据格式遵循1EEE754标准:float32的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32。floate64的浮点数的最大范围约为1.8e308,可以使用一个常量定义:math.MaxFloate64。
打印浮点数时,可以使用fmt包配合动词%f,代码如下:
Go语言中浮点数默认是float64
Golang中float精度丢失问题
几乎所有的编程语言都有精度丢失这个问题,这是典型的二进制浮点数精度损失问题,在定长条件下,二进制小数和十进制小数互转可能有精度丢失。
使用第三方包来解决精度损失问题:
用法:加法Add 减法Sub 乘法MuI 除法Div 用法均与上述类似
Golang科学计数法表示浮点类型
整型和浮点型相互转换

布尔类型(bool)

Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)和false(假)两个值。 注意:
1.布尔类型变量的默认值为false
2.G0语言中不允许将整型强制转换为布尔型
3.布尔型无法参与数值运算,也无法与其他类型进行转换

字符串(string)

GO语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64等)一样。GO语言里的字符串的内部实现使用UTF-8编码。字符串的值为双引号(")中的内容,可以在GO语言的源码中直接添加非ASCII码字符,例如:
定义方式
字符串转义符
Go语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。
转义符
含义
\r
回车符
\n
换行符(直接跳到下一行的同列位置)
\\t
制表符
\\'
单引号
\\"
双引号
\\\
反斜杠
举个例子,我们要打印一个Windows平台下的一个文件路径:
字符串的常用操作
notion image
字符串拼接

byte和rune类型

组成每个字符串的元素叫做“字符”,可以通过遍历字符串元素获得字符。字符用单引号(')包裹起来。
Go语言的字符有以下两种:
1.uint8类型,或者叫byte型,代表了ASCII码的一个字符。
2.rune类型,代表一个UTF-8字符。
当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。
Go使用了特殊的rune类型来处理Unicode,让基于Unicode的文本处理更为方便,也可以使用byte型进行默认字符串处理,性能和扩展性都有照顾。
字节(byte):是计算机中数据处理的基本单位,习惯上用大写B来表示,1B(byte,字节) =8bit(位)
字符:是指计算机中使用的字母、数字、字和符号
一个汉字占用3个字节 一个字母占用一个字节
遍历字符串
遍历字符串中的字符时,若字符串含有中文,则需要使用range遍历,否则会将1个中文拆成3个字节遍历
修改字符串
要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

数值类型之间的相互转换

Go 语言中只有强制类型转换,没有隐式类型转换。
数值类型包括:整形和浮点型
转换的时候建议从低位转换成高位,高位转换成低位的时候如果转换不成功就会溢出,和我
们想的结果不一样。
比如:
比如计算直角三角形的斜边长时使用 math 包的 Sqrt()函数,该函数接收的是 float64 类型的
参数,而变量 a 和 b 都是 int 类型的,这个时候就需要将 a 和 b 强制类型转换为 float64 类型。
其他类型转换成String类型
sprintf把其他类型转换成string类型
  • *注意:**sprintf 使用中需要注意转换的格式 int 为%d float 为%f bool 为%t byte 为%c
输出:
使用strconv包里面的几种转换方法进行转换
String类型转换成数值类型
string类型转换成int类型
string 类型转换成 float 类型
string类型转换成bool类型(意义不大)
string转字符
数值类型没法和bool类型进行转换
注意:在 go 语言中数值类型没法直接转换成 bool 类型 bool 类型也没法直接转换成数值类型

Golang内置的运算符

算术运算符

notion image
注意: ++(自增)和--(自减)在 Go 语言中是单独的语句,并不是运算符。
  • *注意:**在 golang 中,++ 和 -- 只能独立使用 错误写法如下:
  • *注意:**在 golang 中没有前++ 错误写法如下:
++ -- 正确写法:

关系运算符

notion image

逻辑运算符

notion image
逻辑运算符短路演示

赋值运算符

notion image

位运算符

位运算符对整数在内存中的二进制位进行操作。
notion image

Golang中的流程控制

流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的“经脉”。
Go 语言中最常用的流程控制有 if 和 for,而 switch 和 goto 主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制。

if else(分支结构)

if条件判断基本写法

Go 语言中 if 条件判断的格式如下:
当表达式 1 的结果为 true 时,执行分支 1,否则判断表达式 2,如果满足则执行分支 2,都不满足时,则执行分支 3。 if 判断中的 else if 和 else 都是可选的,可以根据实际需要进行选择。
  • *注意:**Go 语言规定与 if 匹配的左括号{必须与 if 和表达式放在同一行,{放在其他位置会触发编译错误。 同理,与 else 匹配的{也必须与 else 写在同一行,else 也必须与上一个 if 或 else if 右边的大括号在同一行。
举个例子:

if条件判断特殊写法

if 条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值
进行判断,举个例子:
思考题: 上下两种写法的区别在哪里?
  • *练习:**求两个数的最大值 (注意 go 语言中没有三目运算)

for(循环结构)

Go 语言中的所有循环类型均可以使用 for 关键字来完成。
for 循环的基本格式如下:
条件表达式返回 true 时循环体不停地进行循环,直到条件表达式返回 false 时自动退出循环。
for 循环的初始语句可以被忽略,但是初始语句后的分号必须要写,例如:
for 循环的初始语句和结束语句都可以省略,例如:
这种写法类似于其他编程语言中的 while,在 while 后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。
注意:Go 语言中是没有 while 语句的,我们可以通过 for 代替

for无限循环

for 循环可以通过 break、goto、return、panic 语句强制退出循环。

for range(键值循环)

Go 语言中可以使用 for range 遍历数组、切片、字符串、map 及通道(channel)。 通过 for range 遍历的返回值有以下规律:
  1. 数组、切片、字符串返回索引和值。
  1. map 返回键和值。
  1. 通道(channel)只返回通道内的值。

switch case

使用 switch 语句可方便的对大量的值进行条件判断。
  • *练习:**判断文件类型,如果后缀名是.html 输入 text/html, 如果后缀名.css 输出 text/css ,如果后缀名是.js 输出 text/javascript
Go 语言规定每个 switch 只能有一个 default 分支。
Go 语言中每个 case 语句中可以不写 break,不加 break 也不会出现穿透的现象 如下例子:
一个分支可以有多个值,多个 case 值中间使用英文逗号分隔。
另一种写法:
注意: 上面两种写法的作用域
分支还可以使用表达式,这时候 switch 语句后面不需要再跟判断变量。例如:

switch的穿透fallthrought

fallthrough语法可以执行满足条件的 case 的下一个 case,是为了兼容 C 语言中的 case 设计
的。
输出:
输出:

break(跳出循环)

Go 语言中 break 语句用于以下几个方面:
  • 用于循环语句中跳出循环,并开始执行循环之后的语句。
  • break 在 switch(开关语句)中在执行一条 case 后跳出语句的作用。
  • 在多重循环中,可以用标号 label 标出想 break 的循环。
switch(开关语句)中在执行一条case后跳出语句的作用。
for 循环中默认 break 只能跳出一层循环
在多重循环中,可以用标号label标出想break的循环。
continue(继续下次循环)
continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用。
输出:
continue 语句后添加标签时,表示开始标签对应的循环。例如:

goto(跳转到指定标签)

goto 语句通过标签进行代码间的无条件跳转。goto 语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go 语言中使用 goto 语句能简化一些代码的实现过程。
输出结果:
使用 goto 语句能简化代码:
输出结果:

Golang中的数组

Array(数组)的介绍

数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(element),这种类型可以是任意的原始类型,比如 int、string 等,也可以是用户自定义的类型。一个数组包含的元素个数被称为数组的长度。在 Golang 中数组是一个长度固定的数据类型,数组的长度是类型的一部分,也就是说 [5]int 和 [10]int 是两个不同的类型。Golang中数组的另一个特点是占用内存的连续性,也就是说数组中的元素是被分配到连续的内存地址中的,因而索引数组元素的速度非常快。和数组对应的类型是 Slice(切片),Slice 是可以增长和收缩的动态序列,功能也更灵活,但是想要理解 slice 工作原理的话需要先理解数组,所以本节主要为大家讲解数组的使用。
notion image
数组基本语法:

数组定义

比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。 [5]int 和[4]int 是不同的类型。
数组可以通过下标进行访问,下标是从 0 开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会 panic。

数组的初始化

数组的初始化也有很多方式。
方法一
初始化数组时可以使用初始化列表来设置数组元素的值。
方法二
按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:
方法三
我们还可以使用指定索引值的方式来初始化数组,例如:

数组的遍历

遍历数组 a 有以下两种方法:

数组是值类型

数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
注意:
  1. 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
  1. [n]T 表示指针数组,[n]T 表示数组指针、

多维数组

Go 语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。
notion image
二维数组的定义
二维数组的遍历
输出:
注意: 多维数组只有第一层可以使用...来让编译器推导数组长度。例如:

Golang 中的切片

为什么要使用切片

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。 例如:
这个求和函数只能接受[4]int 类型,其他的都不支持。所以传入长度为 5 的数组的时候就会报错。

切片的定义

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址长度容量
声明切片类型的基本语法如下:
其中:
  1. name:表示变量名
  1. T:表示切片中的元素类型
举个例子:

关于 nil 的认识

当你声明了一个变量 , 但却还并没有赋值时 , golang 中会自动给你的变量赋值一个默认零值。这是每种类型对应的零值。

切片的循环遍历

切片的循环遍历和数组的循环遍历是一样的

基于数组定义切片

由于切片的底层就是一个数组,所以我们可以基于数组定义切片。
还支持如下方式:

切片再切片

除了基于数组得到切片,我们还可以通过切片来得到切片。
输出:
注意: 对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误。

关于切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的 len() 函数求长度,使用内置的 cap() 函数求切片的容量
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。
输出:
1、第一个输出为[2,3,5,7,11,13],长度为6,容量为6
notion image
2、c :=s[:2]后输出:[2 3], 左指针s[0],右指针s[2] ,所以长度为2,容量为6
notion image
3、d := s[1:3]后输出:[3 5], 左指针s[1],右指针s[3] ,所以长度为2,容量为5
notion image

切片的本质

切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
举个例子,现在有一个数组 a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片 s1 := a[:5],相应示意图如下。
notion image
切片 s2 := a[3:6],相应示意图如下:
notion image

使用make()函数构造切片

我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的 make()函数,格式如下:
其中:
  1. T:切片的元素类型
  1. size:切片中元素的数量
  1. cap:切片的容量
举个例子:
上面代码中 a 的内部存储空间已经分配了 10 个,但实际上只用了 2 个。 容量并不会影响当前元素的个数,所以 len(a)返回 2,cap(a)则返回该切片的容量。

切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较操作是和 nil 比较。 一个 nil 值的切片并没有底层数组,一个 nil 值的切片的长度和容量都是 0。但是我们不能说一个长度和容量都是 0 的切片一定是 nil,例如下面的示例:
所以要判断一个切片是否是空的,要是用 len(s) == 0 来判断,不应该使用 s == nil 来判断。

切片是引用数据类型--注意切片的赋值拷贝

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

append()方法为切片添加元素

Go 语言的内建函数 append()可以为切片动态添加元素,每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收 append 函数的返回值。
给切片追加元素的错误写法:
输出:
从上面的结果可以看出:
  1. append()函数将元素追加到切片的最后并返回该切片。
  1. 切片 numSlice 的容量按照 1,2,4,8,16 这样的规则自动进行扩容,每次扩容后都是扩容前的 2 倍。
append()函数还支持一次性追加多个元素。 例如:
切片的追加切片

切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go 源码,其中扩容相关代码如下:
从上面的代码可以看出以下内容:
1、首先判断,如果新申请容量(cap)大于 2 倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
2、否则判断,如果旧切片的长度小于 1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
3、否则判断,如果旧切片长度大于等于 1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
4、如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如 int 和 string类型的处理方式就不一样。

使用copy()函数复制切片

首先我们来看一个问题:
由于切片是引用类型,所以 a 和 b 其实都指向了同一块内存地址。修改 b 的同时 a 的值也会发生变化。
Go 语言内建的 copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:
其中:
  • srcSlice: 数据来源切片
  • destSlice: 目标切片
举个例子:

从切片中删除元素

Go 语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
总结一下就是:要从切片 a 中删除索引为 index 的元素,操作方法是 a = append(a[:index], a[index+1:]...)

Golang 切片排序算法以及 sort 包

选择排序

选择排序:进行从小到大排序
概念: 通过比较,首先选出最小的数放在第一个位置上,然后在其余的数中选出次小数放在
第二个位置上,依此类推,直到所有的数成为有序序列。

冒泡排序

概念:从头到尾,比较相邻的两个元素的大小,如果符合交换条件,交换两个元素的位置。
特点:每一轮比较中,都会选出一个最大的数,放在正确的位置。var numSlice = []int{9, 8, 7, 6, 5, 4}

Golang 内置 Sort 包对切片进行排序

sort 包的文档:
sort 升序排序
对于 int 、 float64 和 string 数组或是切片的排序, go 分别提供了 sort.Ints() 、sort.Float64s() 和 sort.Strings() 函数, 默认都是从小到大排序。
输出:
sort 降序排序
Golang 的 sort 包可以使用 sort.Reverse(slice) 来调换 slice.Interface.Less ,也就是比较函数,所以, int 、 float64 和 string的逆序排序函数可以这么写。

Golang map 详解

map 的介绍

map 是一种无序的基于 key-value 的数据结构,Go 语言中的 map 是引用类型,必须初始化才能使用。
Go 语言中 map 的定义语法如下:
其中:
  • KeyType:表示键的类型。
  • ValueType:表示键对应的值的类型。
map 类型的变量默认初始值为 nil,需要使用 make()函数来分配内存。语法为:
make: 用于 slice,map,和 channel 的初始化
其中 cap 表示 map 的容量,该参数虽然不是必须的。
注意:获取 map 的容量不能使用 cap, cap 返回的是数组切片分配的空间大小, 根本不能用于map。要获取 map 的容量,可以用 len 函数。

map 基本使用

map 中的数据都是成对出现的,map 的基本使用示例代码如下:
输出:
map 也支持在声明的时候填充元素,例如:

判断某个键是否存在

Go 语言中有个判断 map 中键是否存在的特殊写法,格式如下:
举个例子:

map 的遍历

Go 语言中使用 for range 遍历 map。
但我们只想遍历 key 的时候,可以按下面的写法:
注意: 遍历 map 时的元素顺序与添加键值对的顺序无关。

使用 delete()函数删除键值对

使用 delete()内建函数从 map 中删除一组键值对,delete()函数的格式如下:
其中,
  • map 对象:表示要删除键值对的 map 对象
  • key:表示要删除的键值对的键
示例代码如下:

【案例】按照指定顺序遍历 map

元素为 map 类型的切片

下面的代码演示了切片中的元素为 map 类型时的操作:

值为切片类型的 map

下面的代码演示了 map 中值为切片类型的操作:

练习题

写一个程序,统计一个字符串中每个单词出现的次数。比如:”how do you do”中 how=1 do=2 you=1

Golang 函数详解

函数定义

函数是组织好的、可重复使用的、用于执行指定任务的代码块。本文介绍了 Go 语言中函数的相关内容。
Go 语言中支持:函数、匿名函数和闭包
Go 语言中定义函数使用 func 关键字,具体格式如下:
其中:
  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
  • 函数体:实现指定功能的代码块。
我们先来定义一个求两个数之和的函数:
函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数:

函数的调用

定义了函数之后,我们可以通过函数名()的方式调用函数。 例如我们调用上面定义的两个函数,代码如下:
注意,调用有返回值的函数时,可以不接收其返回值。

函数参数

类型简写
函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:
上面的代码中,intSum 函数有两个参数,这两个参数的类型均为 int,因此可以省略 x 的类型,因为 y 后面有类型说明,x 参数也是该类型。
可变参数
可变参数是指函数的参数数量不固定。Go 语言中的可变参数通过在参数名后加...来标识。
注意:可变参数通常要作为函数的最后一个参数。
举个例子:
固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,示例代码如下:
本质上,函数的可变参数是通过切片来实现的。

函数返回值

Go 语言中通过 return 关键字向外输出返回值。
函数多返回值
Go 语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。
举个例子:
返回值命名
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过 return 关键字返回。
例如:

函数变量作用域

全局变量
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。
局部变量
局部变量是函数内部定义的变量, 函数内定义的变量无法在该函数外使用

函数内定义的变量无法在该函数外使用

例如下面的示例代码 main 函数中无法使用 testLocalVar 函数中定义的变量 x:

如果局部变量和全局变量重名,优先访问局部变量

接下来我们来看一下语句块定义的变量,通常我们会在 if 条件判断、for 循环、switch 语句上使用这种定义变量的方式。
还有我们之前讲过的 for 循环语句中定义的变量,也是只在 for 语句块中生效:

函数类型与变量

定义函数类型
我们可以使用 type 关键字来定义一个函数类型,具体格式如下:
上面语句定义了一个 calculation 类型,它是一种函数类型,这种函数接收两个 int 类型的参数并且返回一个 int 类型的返回值。
简单来说,凡是满足这个条件的函数都是 calculation 类型的函数,例如下面的 add 和 sub 是calculation 类型。
add 和 sub 都能赋值给 calculation 类型的变量。
函数类型变量
我们可以声明函数类型的变量并且为该变量赋值:

高阶函数

高阶函数分为函数作为参数和函数作为返回值两部分。
函数作为参数
函数可以作为参数:
函数作为返回值
函数也可以作为返回值:

匿名函数和闭包

匿名函数
函数当然还可以作为返回值,但是在 Go 语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:
匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
匿名函数多用于实现回调函数和闭包。
闭包
闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。 首先我们来看一个例子:
变量 f 是一个函数并且它引用了其外部作用域中的 x 变量,此时 f 就是一个闭包。 在 f 的生命周期内,变量 x 也一直有效。
闭包进阶示例 1:
闭包进阶示例 2:
闭包进阶示例 3:
闭包其实并不复杂,只要牢记闭包=函数+引用环境。

defer 语句

Go 语言中的 defer 语句会将其后面跟随的语句进行延迟处理。在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 定义的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
举个例子:
输出结果:
由于 defer 语句延迟调用的特性,所以 defer 语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
defer 执行时机
在 Go 语言的函数中 return 语句在底层并不是原子操作,它分为给返回值赋值和 RET 指令两步。而 defer 语句执行的时机就在返回值赋值操作后,RET 指令执行前。具体如下图所示:
notion image
defer 经典案例 1
阅读下面的代码,写出最后的打印结果。
defer 经典案例 2
问,上面代码的输出结果是?(提示:defer 注册要延迟执行的函数时该函数所有的参数都需要确定其值)

内置函数 panic/recover

notion image
Go 语言中目前(Go1.12)是没有异常机制,但是使用 panic/recover 模式来处理错误。 panic可以在任何地方引发,但 recover 只有在 defer 调用的函数中有效。 首先来看一个例子:

panic/recover 的基本使用

输出:
程序运行期间 funcB 中引发了 panic 导致程序崩溃,异常退出了。这个时候我们就可以通过recover 将程序恢复回来,继续往后执行。
注意:
  1. recover()必须搭配 defer 使用。
  1. defer 一定要在可能引发 panic 的语句之前定义。

defer 、recover 实现异常处理

defer 、panic、recover 抛出异常

Golang time 包以及日期函数

time 包

时间和日期是我们编程中经常会用到的,在 golang 中 time 包提供了时间的显示和测量用的函数。

time.Now()获取当前时间

我们可以通过 time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。示例代码如下:
注意:%02d 中的 2 表示宽度,如果整数不够 2 列就补上 0

Format 方法格式化输出日期字符串

获取当前的时间戳

时间戳是自 1970 年 1 月 1 日(08:00:00GMT)至当前时间的总毫秒数。它也被称为 Unix 时间戳(UnixTimestamp)。
基于时间对象获取时间戳的示例代码如下:

时间戳转换为日期字符串(年-月-日 时:分:秒)

使用 time.Unix()函数可以将时间戳转为时间格式。

now.Format 把时间戳格式化成日期

日期字符串转换成时间戳

时间间隔

time.Duration 是 time 包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration 表示一段时间间隔,可表示的最长时间段大约 290 年。
time 包中定义的时间间隔类型的常量如下:
例如:time.Duration 表示 1 纳秒,time.Second 表示 1 秒。

时间操作函数

Add
我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go 语言的时间对象有提供
Add 方法如下:
举个例子,求一个小时之后的时间:
Sub
求两个时间之间的差值:
返回一个时间段 t-u。如果结果超出了 Duration 可以表示的最大值/最小值,将返回最大值/最小值。要获取时间点 t-d(d 为 Duration),可以使用 t.Add(-d)。
Equal
判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用 t==u 不同,这种方法还会比较地点和时区信息。
Before
如果 t 代表的时间点在 u 之前,返回真;否则返回假。
After
如果 t 代表的时间点在 u 之后,返回真;否则返回假。

定时器

1、使用 time.NewTicker(时间间隔)来设置定时器
2、time.Sleep(time.Second) 来实现定时器

Golang 中的指针

关于指针

通过前面的教程我们知道变量是用来存储数据的,变量的本质是给存储数据的内存地址起了一个好记的别名。比如我们定义了一个变量 a := 10 ,这个时候可以直接通过 a 这个变量来读取内存中保存的 10 这个值。在计算机底层 a 这个变量其实对应了一个内存地址。
指针也是一个变量,但它是一种特殊的变量,它存储的数据不是一个普通的值,而是另一个变量的内存地址。
notion image
要搞明白 Go 语言中的指针需要先知道 3 个概念:指针地址、指针类型和指针取值
Go 语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和 *(根据地址取值)

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go 语言中使用&字符放在变量前面对变量进行取地址操作。 Go 语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string 等。
取变量指针的语法如下
则其中:
  • v : 代表被取地址的变量,类型为 T
  • ptr : 用于接收地址的变量,ptr 的类型就为*T,称做 T 的指针类型。*代表指针。
举个例子:
我们来看一下 b := &a 的图示:
notion image

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
输出如下
总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

指针传值示例

new 和 make

我们先来看一个例子:
执行上面的代码会引发 panic,为什么呢? 在 Go 语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的 new 和 make。 Go 语言中 new 和 make 是内建的两个函数,主要用来分配内存。

new 函数分配内存

new 是一个内置的函数,它的函数签名如下:
其中
  • Type 表示类型,new 函数只接受一个参数,这个参数是一个类型
  • Type 表示类型指针,new 函数返回一个指向该类型内存地址的指针。
实际想开发中 new 函数不太常用,使用 new 函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
本节开始的示例代码中 var a *int 只是声明了一个指针变量 a 但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new 函数对 a 进行初始化之后就可以正常对其赋值了:

make 函数分配内存

make 也是用于内存分配的,区别于 new,它只用于 slice、map 以及 channel 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make 函数的函数签名如下:
make 函数是无可替代的,我们在使用 slice、map 以及 channel 的时候,都需要使用 make 进行初始化,然后才可以对它们进行操作。这个我们在前面的教程中都有说明,关于 channel我们会在后续的章节详细说明。本节开始的示例中 var b map[string]int 只是声明变量 b 是一个 map 类型的变量,需要像下面的示例代码一样使用 make 函数进行初始化操作之后,才能对其进行键值对赋值:

new 与 make 的区别

  1. 二者都是用来做内存分配的。
  1. make 只用于 slice、map 以及 channel 的初始化,返回的还是这三个引用类型本身
  1. 而 new 用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

Golang 中的结构体

关于 Golang 结构体

Golang 中没有“类”的概念,Golang 中的结构体和其他语言中的类有点相似。和其他面向对象语言中的类相比,Golang 中的结构体具有更高的扩展性和灵活性。
Golang 中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型就无法满足需求了,Golang 提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称 struct。也就是我们可以通过 struct 来定义自己的类型了。

Golang type 关键词自定义类型和类型别名

Golang 中通过 type 关键词定义一个结构体,在讲解结构体之前,我们首先给大家看看通过type 自定义类型以及定义类型别名。

自定义类型

在 Go 语言中有一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型, Go 语言中可以使用 type 关键字来定义自定义类型。
上面代码表示:将 myInt 定义为 int 类型,通过 type 关键字的定义,myInt 就是一种新的类型,它具有 int 的特性。

类型别名

Golang1.9 版本以后添加的新功能。
类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像一个孩子小时候有大名、小名、英文名,但这些名字都指的是他本人。
我们之前见过的 rune byte 就是类型别名,他们的底层定义如下:

自定义类型和类型别名的区别

类型别名与自定义类型表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
结果显示 a 的类型是 main.newInt,表示 main 包下定义的 newInt 类型。b 的类型是 int 类型。

结构体定义初始化的几种方法

结构体的定义

使用 type 和 struct 关键字来定义结构体,具体代码格式如下:
其中:
  • 类型名:表示自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:表示结构体字段的具体类型。
举个例子,我们定义一个 person(人)结构体,代码如下:
同样类型的字段也可以写在一行,
注意**:**结构体首字母可以大写也可以小写,大写表示这个结构体是公有的,在其他的包里面可以使用。小写表示这个结构体是私有的,只有这个包里面才能使用。

结构体实例化(第一种方法)

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。结构体本身也是一种类型,我们可以像声明内置类型一样使用 var 关键字声明结构体类型。

结构体实例化(第二种方法)

我们还可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
从打印的结果中我们可以看出 p2 是一个结构体指针。
注意:在 Golang 中支持对结构体指针直接使用.来访问结构体的成员。p2.name = "张三" 其实在底层是(*p2).name = "张三"

结构体实例化(第三种方法)

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次 new 实例化操作。

结构体实例化(第四种方法) 键值对初始化

注意:最后一个属性的,要加上

结构体实例化(第五种方法) 结构体指针进行键值对初始化

当某些字段没有初始值的时候,这个字段可以不写。此时,没有指定初始值的字段的

结构体实例化(第六种方法) 使用值的列表初始化

使用这种格式初始化时,需要注意:
1.必须初始化结构体的所有字段。
2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3.该方式不能和键值初始化方式混用。

结构体方法和接收者

在 go 语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接收者的函数。接收者的概念就类似于其他语言中的 this 或者 self。
方法的定义格式如下:
其中
  • *接收者变量:**接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Person 类型的接收者变量应该命名为 p,Connector 类型的接收者变量应该命名为 c 等。
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。
实例 1:给结构体 Person 定义一个方法打印 Person 的信息

值类型的接收者

当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的 this 或者 self。

给任意类型添加方法

在 Go 语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。举个例子,我们基于内置的 int 类型使用 type 关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针。

嵌套匿名结构体

注意:当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。

关于嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。

结构体的继承

Go 语言中使用结构体也可以实现其他编程语言中的继承。

Golang 结构体和 Json 相互转换 序列化 反序列化

关于 JSON 数据

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。RESTfull Api 接口中返回的数据都是 json 数据。
Json 的基本格式如下:

结构体与 JSON 序列化

比如我们 Golang 要给 App 或者小程序提供 Api 接口数据,这个时候就需要涉及到结构体和Json 之间的相互转换
Golang JSON 序列化是指把结构体数据转化成 JSON 格式的字符串,Golang JSON 的反序列化是指把 JSON 数据转化成 Golang 中的结构体对象
Golang 中的 序列化 和 反序列化 主要通过 "encoding/json" 包 中 的 json.Marshal() 和json.Unmarshal()方法实现

结构体对象转化成 Json 字符串

Json 字符串转换成结构体对象

结构体标签 Tag

Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag 在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
结构体 tag 由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对 tag,不同的键值对之间使用空格分隔。
注意事项: 为结构体编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在 key 和 value 之间添加空格。

嵌套结构体和 JSON 序列化反序列化

关于 Map、切片的序列化反序列化

Map 和切片也可以进行序列化和反序列化,这个我们讲完接口后再去给大家详细讲解

Golang 中的 go mod 以及 Golang 包详解

Golang 中包的介绍和定义

包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,Go 语言为我们提供了很多内置包,如 fmt、strconv、strings、sort、errors、time、encoding/json、os、io 等。
Golang 中的包可以分为三种:1、系统内置包 2、自定义包 3、第三方包
系统内置包:Golang 语言给我们提供的内置包,引入后可以直接使用,如 fmt、strconv、strings、sort、errors、time、encoding/json、os、io 等。
自定义包:开发者自己写的包
  • *第三方包:**属于自定义包的一种,需要下载安装到本地后才可以使用,如前面给大家介绍的"github.com/shopspring/decimal"包解决 float 精度丢失问题。

Golang 包管理工具 go mod

在 Golang1.11 版本之前如果我们要自定义包的话必须把项目放在 GOPATH 目录。Go1.11 版本之后无需手动配置环境变量,使用 go mod 管理项目,也不需要非得把项目放到 GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目 , Go1.13 以后可以彻底不要 GOPATH了。

go mod init 初始化项目

实际项目开发中我们首先要在我们项目目录中用 go mod 命令生成一个 go.mod 文件管理我们项目的依赖。
比如我们的 golang 项目文件要放在了 itying 这个文件夹,这个时候我们需要在 itying 文件夹里面使用 go mod 命令生成一个 go.mod 文件
notion image
notion image

go mod 其他命令

notion image
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 (校验依赖 检查下载的第三方库有没有本地修改,如果有修改,则会返回非 0,否则验证成功。)
why
explain why packages or modules are needed (解释为什么需要依赖)

Golang 中自定义包

包(package)是多个 Go 源码的集合,一个包可以简单理解为一个存放多个.go 文件的文件
夹。该文件夹下面的所有 go 文件都要在代码的第一行添加如下代码,声明该文件归属的包。
package 包名
注意事项:
  • 一个文件夹下面直接包含的文件只能归属一个 package,同样一个 package 的文件不能在多个文件夹下。
  • 包名可以不和文件夹的名字一样,包名不能包含 - 符号。
  • 包名为 main 的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含 main 包的源代码则不会得到可执行文件。

定义一个包

如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。在 Go 语言中只需要将标识符的首字母大写就可以让标识符对外可见了。
1.定义一个包名为 calc 的包,代码如下:
2.main.go 中引入这个包
访问一个包里面的公有属性方法的时候需要通过**包名称.**去访问

导入一个包

单行导入
单行导入的格式如下:
多行导入
多行导入的格式如下:
匿名导入包
如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。具体的格式如下:
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。
自定义包名
在导入包名的时候,我们还可以为导入的包设置别名。通常用于导入的包名太长或者导入的包名冲突的情况。具体语法格式如下:
单行引入定义别名:
多行引入定义别名:
notion image

Golang 中 init() 初始化函数

init() 函数介绍
在 Go 语言程序执行时导入包语句会自动触发包内部 init()函数的调用。需要注意的是:init() 函数没有参数也没有返回值。 init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。
包初始化执行的顺序如下图所示:
notion image
init() 函数执行顺序
Go 语言包会从 main 包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go 编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
在运行时,被最后导入的包会最先初始化并调用其 init()函数, 如下图示:
notion image

Golang 中使用第三方包

我们可以在 https://pkg.go.dev/ 查找看常见的 golang 第三方包
1 、找到我们需要下载安装的第三方包的地址
比如前面给大家演示的解决 float 精度损失的包 decimal
2 、安装这个包
第一种方法:go get 包名称 (全局)
第二种方法:go mod download (全局)
依赖包会自动下载到$GOPATH/pkg/mod,多个项目可以共享缓存的 mod,注意使用 go mod download 的时候首先需要在你的项目里面引入第三方包
第三种方法:go mod vendor 将依赖复制到当前项目的 vendor 下 (本项目)
将依赖复制到当前项目的 vendor 下
  • *注意:**使用 go mod vendor 的时候首先需要在你的项目里面引入第三方包
3 、看文档使用这个包
包安装完毕后我们就可以看文档使用这个包了...

Golang 中的接口

接口的介绍

1、现实生活中的接口
现实生活中手机、相机、U 盘都可以和电脑的 USB 接口建立连接。我们不需要关注 usb 卡槽大小是否一样,因为所有的 USB 接口都是按照统一的标准来设计的。
notion image
2、Golang 中的接口(interface)
Golang 中的接口是一种抽象数据类型,Golang 中接口定义了对象的行为规范,只定义规范不实现。接口中定义的规范由具体的对象来实现。通俗的讲接口就一个标准,它是对一个对象的行为和规范进行约定,约定实现接口的对象必须得按照接口的规范。

Golang 接口的定义

在 Golang 中接口(interface)是一种类型,一种抽象的类型。接口(interface)是一组函数 method 的集合,Golang 中的接口不能包含任何变量。
在 Golang 中接口中的所有方法都没有方法体,接口定义了一个对象的行为规范,只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想
Golang 中的接口也是一种数据类型,不需要显示实现。只需要一个变量含有接口类型中的所有方法,那么这个变量就实现了这个接口。
Golang 中每个接口由数个方法组成,接口的定义格式如下:
其中:
  • 接口名:使用 type 将接口定义为自定义的类型名。Go 语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer 等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
演示:定义一个 Usber 接口让 Phone Camera 结构体实现这个接口
演示: Computer 结构体中的 Work 方法必须传入一个 Usb 的接口

空接口

Golang 中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。空接口表示没有任何约束,因此任何类型变量都可以实现空接口。
空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型。
案例:
1 、空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
2 map 的值实现空接口
使用空接口实现可以保存任意值的字典。
3 、切片实现空接口

类型断言

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。
如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言,其语法格式:
其中:
  • x : 表示类型为 interface{}的变量
  • T : 表示断言 x 可能是的类型。
该语法返回两个参数,第一个参数是 x 转化为 T 类型后的变量,第二个值是一个布尔值,若为 true 则表示断言成功,为 false 则表示断言失败。
举个例子:
上面的示例中如果要断言多次就需要写多个 if 判断,这个时候我们可以使用 switch 语句来
实现:
注意:类型.(type)只能结合 switch 语句使用
因为空接口可以存储任意类型值的特点,所以空接口在 Go 语言中的使用十分广泛。
  • *关于接口需要注意的是:**只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

结构体值接收者和指针接收者实现接口的区别

值接收者:
如果结构体中的方法是值接收者,那么实例化后的结构体值类型和结构体指针类型都可以赋值给接口变量
指针接收者:
如果结构体中的方法是指针接收者,那么实例化后结构体指针类型都可以赋值给接口变量,结构体值类型没法赋值给接口变量。

一个结构体实现多个接口

Golang 中一个结构体也可以实现多个接口

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

 

评论
Loading...