《golong入门教程
Go语言学习笔记
本菜鸟的Go语言学习笔记,历时1个月,包含了Go语言大部分的基本语法(不敢说全部),学习期间参考了各种视频,阅读了各种文章,主要参考名单如下:
在这里小声说两句:Go语言对于并发的支持非常nice,在现在这个卷的时代,多学习一门编程语言,就多一点竞争力,Go语言还是我比较推荐学习的一门编程语言
文章目录
基本语法
Go 语言在很多特性上和C语言非常相近。如果各位看官有C语言基础(或者其他编程语言基础),那么本章的内容阅读起来将会非常轻松,但如果读者没有编程语言基础也没关系,因为本章的内容非常简单易懂。
注释
注释就是为了
增强代码的可读性
,注释起来的代码或者文字不会被程序执行,程序会自动略过注释的部分
写注释是一个非常好的习惯,十分建议多写注释,否则以后你会想打死不写注释的自己╮( ̄▽ ̄)╭
单行注释
package main // 单行注释,这一行都不会被程序执行 (~ ̄▽ ̄~) func main() { println("Hello World") }
多行注释
package main /** 不会被执行(~ ̄▽ ̄~) 多行注释 <( ̄︶ ̄)> 不会被执行(~ ̄▽ ̄~) */ func main() { println("Hello World") }
变量
Go语言是静态类型语言,因此变量(variable)是有明确类型的,编译器也会检查变量类型的正确性。
字面上来理解,变量就是会变化的量。
在数学概念中,变量表示没有固定值且可改变的数。
从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。
这里的
name
就是变量,右边的“玄德”就是给这个变量赋值,所以这个变量
name
的值就是“玄德”
var name string = "玄德"
不用写分号真不错啊<( ̄︶ ̄)>
变量的声明
在G语言中,声明一个变量一般使用
var
关键字
var name type
这里的
type
表示变量的类型,就如上面的字符串类型:
string
// 定义一个字符型变量 name
var name string
// 定义一个整形变量 age
var age int
Go语言和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。
这样做的好处就是可以避免像C语言中那样含糊不清的声明形式,例如:
int* a, b;
, 其中只有 a 是指针而 b 不是,如果你想要这两个变量都是指针,则需要将它们分开书写。
在 Go 语言中,可以轻松地将它们都声明为指针类型:
var a, b *int
变量的命名遵循驼峰命名法,即首个字母小写,每个新单词的首字母大写,例如:userName、email
标准格式
Go语言变量声明的标准格式
var 变量名 变量类型
变量声明以关键字 var 开头,后置变量类型,行尾无须分号。
批量格式
如果觉得单独定义比较麻烦,那么就可以使用批量定义变量,使用关键字 var 和括号,可以将一组变量定义放在一起。
初始值
当一个变量被声明后,如果没有给这个变量赋值,那么系统将自动赋予它们默认值:
- 整型和浮点型的默认值是 0 和0.0
- 字符串变量的默认值是空字符串
- 布尔型变量默认值是 false
- 切面、函数、指针变量的默认值是 nil。
var (
name string
age int
c [] float32
d func() bool
e struct {
x int
}
)
Go语言的基本类型
- bool
- string
- int、int8、int16、int32、int64
- uint、uint8、uint16、uint32、uint64、uintptr
- byte // uint8 的别名
- rune // int32 的别名 代表一个 Unicode 码
- float32、float64
- complex64、complex128
变量的初始化
标准格式
var 变量名 类型 = 表达式
比如,定义一个玄德的信息,可以这么表示
var name string = "玄德" // 定义字符型变量name
var age int = 20 // 定义整形变量 age
println(name, age)
在标准格式的基础上,如果将类型省去,编译器会尝试根据等号右边的表达式推导变量的类型。
var name = "玄德"
等号右边的部分在编译原理里被称做右值。
简短变量
除 var 关键字外,还可使用更加简短的变量定义和初始化语法。
名字 := 表达式
编译器会根据右值类型推断出左值对应的类型
需要注意的是,简短模式(short variable declaration)有以下限制:
- 定义变量,同时显式初始化。
- 不能提供数据类型。
- 只能用在函数内部。
因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量声明和初始化,var 形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方
注意:由于使用了:=,而不是=,因此推导声明写法的左值变量必须是没有定义过的变量,若定义过,将会发生编译错误
func main() {
x:=100
a,s:=1, "abc"
}
变量的交换
编程最简单的算法之一,莫过于变量交换。交换变量的常见算法需要一个中间变量进行变量的临时保存。用传统方法编写变量交换代码如下:
var a int = 100
var b int = 200
var t int
t = a
a = b
b = t
Println(a, b)
而在go语言,如今内存已经不再是紧缺资源,所以可以更简单的实现:
var a int = 100
var b int = 200
b, a = a, b
Println(a, b)
匿名变量
匿名变量的特点是一个下画线
_
,
_
本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),
但任何赋给这个标识符的值都将被抛弃
,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。例如:
package main
func test() (int, int) {
return 100, 200
}
func main() {
a, _ := test()
_, b := test()
println(a, b) //100, 200
}
在编码过程中,可能会遇到没有名称的变量、类型或方法。虽然这不是必须的,但有时候这样做可以极大地增强代码的灵活性,这些变量被统称为匿名变量。
匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。
变量的作用域
一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。
了解变量的作用域对我们学习Go语言来说是比较重要的,因为Go语言会在编译时检查每个变量是否使用过,一旦出现未使用的变量,就会报编译错误。如果不能理解变量的作用域,就有可能会带来一些不明所以的编译错误。
根据变量定义位置的不同,可以分为以下三个类型:
- 函数内定义的变量称为局部变量
- 函数外定义的变量称为全局变量
- 函数定义中的变量称为形式参数
局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。
func main() {
//声明局部变量 a 和 b 并赋值
var a int = 3
var b int = 4
//声明局部变量 c 并计算 a 和 b 的和
c := a + b
fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}
全局变量
在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源文件需要使用“import”关键字引入全局变量所在的源文件之后才能使用这个全局变量。
全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。
Go语言程序中全局变量与局部变量名称可以相同,但是函数体内的局部变量会被优先考虑。
// 声明全局变量
var a float32 = 3.14
func main() {
//声明局部变量
var a int = 3
fmt.Printf("a = %d\n", a)
}
常量
常量是一个简单的标识符,在程序运行时,不会被修改的量
常量的数据只能是布尔型、数字型(整数型、浮点型和复数)和字符串型。
const name [type] = value
在Go语言中,你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。
- 显式类型定义:
const b string = "abc"
- 隐式类型定义:
const b = "abc"
多个相同类型的声明可以简写为:
const c_name1, c_name2 = value1, value2
iota 常量计数器
常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。
在一个 const 声明语句中,第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一。
在其它编程语言中,这种类型一般被称为枚举类型。
func main() {
// iota 默认为0
const (
// 一组常量中,如果某个常量没有初始值,默认和上一行一致
a = iota // 0
b // 1
c // 2
)
// 下一个const语句重新计数
const (
// 常量被赋值不会打断 iota 计数
d = iota // 0
e = "hello" // hello
f = iota // 2
)
fmt.Println(a, b, c, d, e, f)
}
基本数据类型
Go语言是静态类型语言,在 Go 编程语言中,数据类型用于声明函数和变量,数据类型就是告诉编译器要向系统申请一块多大的内存空间,并知道这块空间表示什么
布尔类型
一个布尔类型的值只有两种:true 或 false。if 和 for 语句的条件部分都是布尔类型的值,并且
==
和
<
等比较操作也会产生布尔型的值。
func main() {
// 默认值为 false
var flag bool
var flag1 bool = false
var flag2 bool = true
println(flag)
println(flag1)
println(flag2)
}
数字类型
整型 int 和浮点型 float32、float64,Go语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码
Go也有基于架构的类型,例如:uint 无符号、 int有符号
- uint8: 无符号 8 位整形(0 到 255)
- uint16:无符号 16 位整形(0 到 65535)
- uint32:无符号 32 位整形(0 到 4294967295)
- uint64:无符号 64 位整形(0 到 18446744073709551615)
- int8: 有符号 8 为整形(-128 到 127)
- int16:有符号 16 位整形(-32768 到 32767)
- int32:有符号 32 位整形(-2147483648 到 2147483647)
- int64:有符号 64 位整形(-9223372036854775808 到 9223372036854775807)
func main() {
// 定义一个整形
var age int = 20
fmt.Printf("%d \n", age) // 20
// 定义一个浮点型
// 默认6位小数打印 3.140000
var test float64 = 3.14
fmt.Printf("%f", test) // 3.140000
}
其他数字类型
- byte 类似 uint8
- rune 类似 int32
- uint 32 或 64 位
- int 与 uint 一样大小
- uintptr 无符号整形,用于存放指针
字符类型
字符有两种形式,分别是字符和字符串('' 和 “”)
字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。
一个字符串是一个
不可改变的字节序列
,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。
与C++、Java、Python不同(Java 始终使用 2 个字节),Go语言中字符串也可能根据需要占用 1 至 4 个字节,这样做不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。
func main() {
// \n 为转义字符,表示换行
var str string
str = "Hello,World"
fmt.Printf("%T,%s\n", str, str)
// 获取指定的字节
fmt.Printf("第四个字节是:" + "%c\n", str[4])
// 单引号和双引号区别,单引号 字符,ASCII字符码
v1 := 'A'
v2 := "A"
fmt.Printf("%T,%d\n", v1, v1)
fmt.Printf("%T,%s\n", v2, v2)
// 中国的编码表:GBK
// Unicode编码表:号称兼容了全世界的文字
v3 := '中'
fmt.Printf("%T,%d\n", v3, v3)
// 字符串拼接
println("hello" + ",xuande")
}
常用转义字符:
- \n:换行符
- \r:回车符
- \t:tab 键
- \u 或 \U:Unicode 字符
- \:反斜杠自身
定义多行字符串
在Go语言中,使用双引号书写字符串的方式是字符串常见表达方式之一,这种双引号字面量不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用反引号(键盘上 1 键左边的键,长这样" ` ")
const str = `第一行
第二行
第三行
\r\n
`
fmt.Println(str)
UTF-8 和 Unicode 有何区别?
Unicode 与 ASCII 类似,都是一种字符集。
字符集为每个字符分配一个唯一的 ID,我们使用到的所有字符在 Unicode 字符集中都有一个唯一的 ID,例如上面例子中的 a 在 Unicode 与 ASCII 中的编码都是 97。汉字“你”在 Unicode 中的编码为 20320,在不同国家的字符集中,字符所对应的 ID 也会不同。而无论任何情况下,Unicode 中的字符的 ID 都是不会变化的。
UTF-8 是编码规则,将 Unicode 中字符的 ID 以某种方式进行编码,UTF-8 的是一种变长编码规则,从 1 到 4 个字节不等。编码规则如下:
- 0xxxxxx 表示文字符号 0~127,兼容 ASCII 字符集。
- 从 128 到 0x10ffff 表示其他字符。
根据这个规则,拉丁文语系的字符编码一般情况下每个字符占用一个字节,而中文每个字符占用 3 个字节。
广义的 Unicode 指的是一个标准,它定义了字符集及编码规则,即 Unicode 字符集和 UTF-8、UTF-16 编码等。
指针类型
数据类型转换
在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。
由于Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明:
valueOfTypeB = typeB(valueOfTypeA)
类型 B 的值 = 类型 B(类型 A 的值)
func main() {
a := 5.0 // float
b := int(a) // 强转为int
fmt.Printf("%T,%f\n", a, a)
fmt.Printf("%T,%d\n", b, b)
}
型转换只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(将 int16 转换为 int32)。当从一个取值范围较大的类型转换到取值范围较小的类型时(将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。
运算符
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 其他运算符
Go语言运算符优先级
先计算乘法后计算加法,说明乘法运算符的优先级比加法运算符的优先级高。所谓优先级,就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。
优先级 | 分类 | 运算符 | 结合性 |
---|---|---|---|
1 | 逗号运算符 | , | 从左到右 |
2 | 赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= | 从右到左 |
3 | 逻辑或 | || | 从左到右 |
4 | 逻辑与 | && | 从左到右 |
5 | 按位或 | | | 从左到右 |
6 | 按位异或 | ^ | 从左到右 |
7 | 按位与 | & | 从左到右 |
8 | 相等/不等 | ==、!= | 从左到右 |
9 | 关系运算符 | <、<=、>、>= | 从左到右 |
10 | 位移运算符 | <<、>> | 从左到右 |
11 | 加法/减法 | +、- | 从左到右 |
12 | 乘法/除法/取余 | *(乘号)、/、% | 从左到右 |
13 | 单目运算符 | !、*(指针)、& 、++、--、+(正号)、-(负号) | 从右到左 |
14 | 后缀运算符 | ( )、[ ]、-> | 从左到右 |
注意:优先级值越大,表示优先级越高
算术运算符
简单的加减乘除,取余就是相除之后的余数,++ 代表自增、-- 代表自减
func main() {
var a int = 10
var b int = 3
// + - * / % ++ --
println(a + b) // 13
println(a - b) // 7
println(a * b) // 30
println(a / b) // 3
println(a % b) // 1
a++ // a = a + 1
println(a) // 11
b-- // a = a - 1
println(b) // 12
println(a, b) // 11 2
}
关系运算符
关系运算符的结果都是布尔值,它尝尝会出现在判断语句当中
func main() {
var a int = 11
var b int = 10
// == 等于 = 赋值
// 关系运算符,结果都是bool(布尔值)
println(a == b) // 等于
println(a != b) // 不等于
println(a > b)
println(a < b)
println(a >= b)
println(a <= b)
}
逻辑运算符
逻辑运算符等同于高中数学里面的与、或、非
- 逻辑与(&&):两边都是真(True),则条件为真(True),否则为假(False)
- 逻辑或(||):两边有一个为 True,怎该条件为 True,否则为 False
- 逻辑非( ! ):如果为 True,则变为 False,反之亦然
func main() {
var a bool = true
var b bool = false
var c bool
// 逻辑与(&&):两边都为真,结果为真,反之为假
c = a && b
println(c) // false
// 逻辑或(||):两边都为假,结果为假,反之为真
c = a || b
println(c) // true
// 逻辑非(!):改变布尔值,真变假、假变真
c = a
println(c) // true
c = !a
println(c) // false
}
位运算符
乍一看与逻辑运算符很相似,但它俩可是天差地别,位运算符是对二进制进行操作
二进制
:通常用两个不同的符号0(代表零)和1(代表一)来表示,我们平常使用的是十进制,逢十进一;二进制是逢二进一
十进制 | 二进制 |
---|---|
0 | 0 |
1 | 1 |
2 | 10 |
3 | 11 |
...... | ..... |
10 | 1010 |
11 | 1011 |
所有的位运算都是建立在二进制上的
- 按位与( & ):都为1结果为1,否则为0
- 按位或( | ):都为0结果为0,否则为1
- 按位异或( ^ ):不同为1,相同为0
- 左移运算符( << ):二进制位全部左移若干位
- 右移运算符( >> ):二进制位全部右移若干位
func main() {
/**
位运算案例
60 0011 1100 | 60 0011 1100
13 0000 1101 | ------------
------------- | << 0666666 1000
& 0000 1100 | >> 0001 6666660
| 0011 1101 |
^ 0011 0001 |
*/
var a uint = 60
var b uint = 13
var c uint = 0
c = a & b
fmt.Printf("十进制:%d, 二进制:%b\n", c, c) // 十进制:12, 二进制:1100
c = a | b
fmt.Printf("十进制:%d, 二进制:%b\n", c, c) // 十进制:61, 二进制:666666101
c = a ^ b
fmt.Printf("十进制:%d, 二进制:%b\n", c, c) // 十进制:49, 二进制:110001
c = a << 2
fmt.Printf("十进制:%d, 二进制:%b\n", c, c) // 十进制:240, 二进制:66666610000
c = a >> 2
fmt.Printf("十进制:%d, 二进制:%b\n", c, c) // 十进制:15, 二进制:6666661
}
赋值运算符
简化操作,这里演示一下+=和-=,其他同理
func main() {
var a int = 66
var b int
// = 赋值
b = a
println(b) // 66
// += 等效于 b = a + b
b += a
println(b) // 132
// -= 等效于 b = a - b
b -= a
println(b) // 66
}
其他运算符
- &:返回变量存储的地址
- *:指针变量
指针
流程控制
程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的“经脉”
Go 语言的常用流程控制有 if 和 for,而 switch 和 goto 主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制。
程序的流程结构分为三种:顺序结构、选择结构、循环结构
顺序结构:从上到下,逐行执行
选择结构:条件满足才会继续执行
- if
- switch
- select
循环结构:条件满足会被反复执行0~n次
- for
选择结构
- if
- switch
- select
if语句
在Go语言中,关键字 if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号
{}
括起来的代码块,否则就忽略该代码块继续执行后续的代码。
func main() {
var score int
fmt.Println("请输入成绩:")
fmt.Scanln(&score) // 对score的地址进行赋值
fmt.Printf("你输入的成绩为:%d\n", score)
if score >= 90 && score <= 100 {
fmt.Println("评级为:A")
} else if score >= 80 && score < 90 {
fmt.Println("评级为:B")
} else if score >= 70 && score < 80 {
fmt.Println("评级为:C")
} else if score >= 60 && score < 70 {
fmt.Println("评级为:D")
} else {
fmt.Println("评级为:不及格")
}
}
if嵌套语句
有时一个判断条件是无法满足需求的,这时就需要if语句嵌套了
if 布尔表达式1 {
// 布尔表达式1为true时执行
if 布尔表达式2 {
//布尔表达式2为true时执行
}
}
switch语句
Go语言 switch 语句的表达式不需要为常量,甚至不需要为整数,case 按照从上到下的顺序进行求值,直到找到匹配的项,
如果 switch 没有表达式,则对 true 进行匹配
,因此,可以将 if else-if else 改写成一个 switch。
Go语言改进了 switch 的语法设计,每一个case分支都是
唯一的
,从上到下依次测试,因此不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行。
func main() {
var a = "hello"
switch a {
case "hello":
fmt.Println(1) // 匹配成功,输出1,终止switch
case "world":
fmt.Println(2) // 不会执行
default:
fmt.Println(0) // case分支全部匹配失败时,执行该语句
}
}
Go语言的 switch 语句可以一分支多值,也可以case后面添加表达式
一分支多值
var a = "mum" switch a { case "mum", "daddy": // 注意逗号 fmt.Println("family") }
分支表达式
var r int = 11 switch { // 默认匹配itrue case r > 10 && r < 20: fmt.Println(r) }
fallthrough——兼容C语言的 case 设计
在Go语言中 case 是一个独立的代码块,执行完毕后不会像C语言那样紧接着执行下一个 case,但是为了兼容一些移植代码,依然加入了
fallthrough
关键字来实现这一功能。
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough // case穿透,不管下个条件是否满足,都会执行
case s != "world":
fmt.Println("world")
}
循环结构
与多数语言不同的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构,关键字 for 的基本使用方法与C语言和C++中非常接近
- for
- break
for语句
for循环是一个循环控制结构,可以执行指定次数的循环。
func main() {
sum := 0
// for 条件起始值; 循环条件; 操作控制变量
for i := 0; i <= 6; i++ {
sum += i
}
fmt.Println(sum) // 21
}
可以看到比较大的一个不同在于 for 后面的条件表达式不需要用圆括号
()
括起来,Go语言还进一步考虑到无限循环的场景,让开发者不用写无聊的
for(;;){}
和
do{} while(1);
func main() {
sum := 0
for {
sum++
fmt.Println(sum) // 无限循环(记得停止,否则会卡死)
}
}
continue 结束本次循环,开启下一次循环
Go语言中 continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加标签时,表示开始标签对应的循环
func main() {
for i := 1; i <= 6; i++ {
if i == 5 {
continue
}
fmt.Println(i)
}
fmt.Println("==============我是分界线(~ ̄▽ ̄~)=================")
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
continue OuterLoop // 跳转到标签
}
}
}
}
break
Go语言中 break 语句可以结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的 for、switch 和 select 的代码块上。
func main() {
sum := 0
for {
sum++
if sum > 100 {
break // 跳出循环
}
}
fmt.Println(sum)
}
键值循环
for range 结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道(channel),for range 语法上类似于其它语言中的 foreach 语句。
func main() {
var str string = "hello,xuande"
// 返回下标和对应的值
for key, val := range str {
fmt.Printf("key:%d val:%c\n", key, val)
}
}
案例
九九乘法表
func main() { // 遍历, 决定处理第几行 for y := 1; y <= 9; y++ { // 遍历, 决定这一行有多少列 for x := 1; x <= y; x++ { fmt.Printf("%d*%d=%d ", x, y, x*y) } // 手动生成回车 fmt.Println() } }
打印6x6方阵
func main() { for j := 0; j < 5; j++ { for i := 0; i <= 5; i++ { fmt.Print("* ") } fmt.Println() } }
打印松树
func main() { // 上半部分 for i := 0; i < 5; i++ { for j := 5; j >= i; j-- { print(" ") } for k := 0; k < 2*i+1; k++ { print("*") } println() } // 下半部分 for i := 0; i < 2; i++ { for j := 0; j < 5; j++ { print(" ") } for k := 0; k < 3; k++ { print("*") } println() } }
冒泡排序
package main import "fmt" func main() { // 定义切片,进行引用传递 arr := []int{5, 1, 4, 2, 8} // 使用协程加快速度 go bubbleSort(arr) fmt.Println(arr) } func bubbleSort(arr []int) { var flag = true var n = len(arr) for i := 0; i < n-1; i++ { for j := 0; j < n-i-1; j++ { if arr[j] > arr[j+1] { temp := arr[j] arr[j] = arr[j+1] arr[j+1] = temp flag = false } } if flag { continue } } }
函数
函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,其可以提高应用的模块性和代码的重复利用率。
Go 语言支持普通函数、匿名函数和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。
Go 语言函数:
- 函数是基本的代码块,用于执行一个任务
- Go语言最少有个main()函数
- 函数本身可以作为值进行传递
- 支持匿名函数和闭包(closure)
- 函数可以满足接口
- 可以通过函数来划分不同的功能,逻辑上每个函数执行的是指定的任务
- 函数声明告诉了编译器函数的名称、返回类型和参数
函数的声明
函数构成了代码执行的逻辑结构,在Go语言中,函数的基本组成为:关键字 func、函数名、参数列表、返回值、函数体和返回语句,每一个程序都包含很多的函数,函数是基本的代码块。
Go语言里面拥三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者 lambda 函数
- 方法
Go语言函数定义格式如下:
func 函数名(形参列表)(返回值列表){
函数体
}
形式参数列表描述了函数的参数名以及参数类型,这些参数作为局部变量,其值由参数调用者提供,形式参数简单来说就是用来
接收外部数据
的参数。
返回值列表描述了函数返回值的变量名以及类型,如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。
func main() {
// 实际参数:4,2
// 形参和实参必须对应
result := add(4, 2)
print(result) // 打印结果:6
}
// 定义一个相加函数add(函数名)
// 形式参数:a,b;返回值列表:int;
func add(a, b int) int {
c := a + b
// 如果函数有返回值,那么就必须使用return语句
return c
}
函数访问规则
函数名首字母小写,只能在本包访问,函数名首字母大写,可以在本包和其他包访问
内置函数
Golang设计者为了编程方便,提供了一些函数,这些函数由于不用导包就可以直接使用,所以这些函数被称为内置函数/内建函数
内置函数位置:builtin包下
常用函数
- len函数:统计字符串的长度,按字节进行统计
- new函数:分配内存,主要用来分配值类型(int、float、string、array、struct)
- make函数:分配内存,主要用来分配引用类型(指针、切片、管道、接口等)
可变参数
可变参数是指函数传入的参数个数是可变的(类型确定,个数不确定)
func main() {
fmt.Println("传入2个参数:")
getSum(1, 2)
fmt.Println("传入3个参数:")
getSum(1, 2, 66)
}
// ... 可变参数
func getSum(args ...int) {
sum := 0
for _, arg := range args {
sum += arg
fmt.Println(arg)
}
fmt.Println("sum:", sum)
}
注意:
- 可变参数只能放在参数列表的最后
- 一个函数最多只有一个可变参数
任意类型的可变参数
之前的例子中将可变参数类型约束为 int,如果你希望传任意类型,可以指定类型为
interface{}
,下面是Go语言标准库中 fmt.Printf() 的函数原型:
func Printf(format string, args ...interface{}) {
// ...
}
用 interface{} 传递任意类型数据是Go语言的惯例用法,使用 interface{} 仍然是类型安全的,这和 C/C++ 不太一样
参数传递
数据的存储特点分为:
- 值传递:操作的是数据的本身,int、string、bool、float64、array。。。
- 引用传递:操作的是数据的地址,slice、map、chan。。。
值传递
使用普通变量作为函数参数的时候,在传递参数时只是对变量值得拷贝,即将实参的值复制给变参,当函数对变参进行处理时,并不会影响原来实参的值。
func main() {
// 定义数组
arr1 := [4]int{1, 2, 3, 4}
fmt.Println("默认数据arr1", arr1) // 默认数据arr1 [1 2 3 4]
update(arr1)
fmt.Println("调用函数后数据arr1", arr1) // 调用函数后数据arr1 [1 2 3 4]
}
func update(arr2 [4]int) {
fmt.Println("接受数据arr2:", arr2) // 接受数据arr2: [1 2 3 4]
arr2[0] = 66
fmt.Println("修改后数据arr2:", arr2) // 修改后数据arr2: [66 2 3 4]
}
解析:
- arr2的数据是从arr1复制的,所以它俩处在不同的内存空间
- 由于是拷贝的副本,所以修改arr2并不会影响arr1
- 值类型的数据,默认都是值传递:基础类型、array、struct
引用传递
函数的变量不仅可以使用普通变量,还可以使用指针变量,使用指针变量作为函数的参数时,在进行参数传递时将是一个地址,即将实参的内存地址复制给变参,这时对变参的修改也将会影响到实参的值。
func main() {
// 定义切片
s1 := []int{1, 2, 3, 4}
fmt.Println("默认数据s1", s1) // 默认数据s1 [1 2 3 4]
update(s1)
fmt.Println("调用函数后数据s1", s1) // 调用函数后数据s1 [66 2 3 4]
}
func update(s2 []int) {
fmt.Println("接受数据s2:", s2) // 接受数据s2: [1 2 3 4]
s2[0] = 66
fmt.Println("修改后数据s2:", s2) // 修改后数据s2: [66 2 3 4]
}
解析:
- 引用传递传递的是内存地址
- 由于传递的是地址,所以是同一片内存空间
- 修改变参s2会影响到实参s1
递归函数
很对编程语言都支持递归函数,Go语言也不例外,所谓递归函数指的是在
函数内部调用函数自身
的函数,从数学解题思路来说,递归就是把一个大问题拆分成多个小问题,再各个击破,在实际开发过程中,递归函数可以解决许多数学问题,如计算给定数字阶乘、产生斐波系列等。
构成递归需要具备以下条件:
- 一个问题可以被拆分成多个子问题;
- 拆分前的原问题与拆分后的子问题除了数据规模不同,但处理问题的思路是一样的;
- 不能无限制的调用本身,子问题需要有退出递归状态的条件。
注意:编写递归函数时,一定要有终止条件,否则就会无限调用下去,直到内存溢出。
斐波那锲数列
func main() {
result := 0
for i := 1; i <= 10; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is: %d\n", i, result)
}
}
func fibonacci(n int) (res int) {
if n <= 2 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}
函数变量
在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f,并将一个函数名为 fire() 的函数赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数
func main() {
var f func()
f = fire
f()
}
func fire() {
fmt.Println("fire")
}
匿名函数
Go语言支持匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递,Go语言支持随时在代码里定义匿名函数。
匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成。
匿名函数的定义格式如下:
func(参数列表)(返回参数列表){
函数体
}
匿名函数的定义就是没有名字的普通函数定义。
func main() {
// 匿名函数赋值给变量
f := func() {
fmt.Println("我是匿名函数")
}
// 调用匿名函数
f()
// 定义时调用
func(data int) {
fmt.Println("xuande", data)
}(666)
}
回调函数
根据Go语言数据类型的特点,可以将一个函数作为另外一个函数的参数
func main() {
// 使用匿名函数打印切片内容
visit([]int{1, 2, 3, 4}, func(v int) {
fmt.Println(v)
})
}
// 遍历切片的每个元素, 通过给定函数进行元素访问
func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
defer
Go语言的 defer 语句会将其后面跟随的语句进行
延迟处理
,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
func main() {
f("1")
fmt.Println("2")
defer f("3") // 会被延迟到最后执行
fmt.Println("4")
}
func f(s string) {
fmt.Println(s)
}
多个defer处理顺序
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)
func main() {
fmt.Println("defer begin")
// 将defer放入延迟调用栈
defer fmt.Println(1)
defer fmt.Println(2)
// 最后一个放入, 位于栈顶, 最先调用
defer fmt.Println(3)
fmt.Println("defer end")
}
解析:
- 代码的延迟顺序与最终的执行顺序是反向的。
- 延迟调用是在 defer 所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。
案例:对文件关闭的操作时使用defer
func fileSize(filename string) int64 {
f, err := os.Open(filename)
if err != nil {
return 0
}
// 延迟调用Close, 此时Close不会被调用
defer f.Close()
info, err := f.Stat()
if err != nil {
// defer机制触发, 调用Close关闭文件
return 0
}
size := info.Size()
// defer机制触发, 调用Close关闭文件
return size
}
recove
Go语言的recover函数用于捕获和处理运行时异常,可以在程序中捕获到panic异常,并执行相应的处理操作,从而避免程序崩溃。
package main
import (
"fmt"
)
func main() {
division()
fmt.Println("上面错误已捕获,我依旧能执行")
}
func division() {
// 捕获错误并处理
defer func() {
err := recover()
if err != nil {
fmt.Println("division函数出现错误", err)
}
}()
num1 := 10
num2 := 0
result := num1 / num2
fmt.Println(result)
}
闭包
Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:
函数 + 引用环境 = 闭包
一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念。
闭包(Closure)在某些编程语言中也被称为 Lambda 表达式。
闭包结构中的外层函数的局部变量不会随着外层函数的结束而销毁,因为内层函数还在继续使用
func main() {
r1 := inc()
fmt.Println(r1()) // 1
fmt.Println(r1()) // 2
fmt.Println(r1()) // 3
r2 := inc()
fmt.Println(r2()) // 1
fmt.Println(r2()) // 2
fmt.Println(r1()) // 4
}
// 自增函数
func inc() func() int {
// 局部变量
i := 0
// 匿名函数
fun := func() int { // 内层函数
i++
return i
}
return fun
}
闭包实现生成器
func main() {
// 创建一个玩家生成器
generator := playerGen("xuande")
// 返回玩家的名字和血量
name, hp := generator()
// 打印值
fmt.Println(name, hp)
}
// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
// 血量一直为150
hp := 150
// 返回创建的闭包
return func() (string, int) {
// 将变量引用到闭包中
return name, hp
}
}
容器
变量在一定程度上能满足函数及代码要求。如果编写一些复杂算法、结构和逻辑,就需要更复杂的类型来实现。这类复杂类型一般情况下具有各种形式的存储和处理数据的功能,将它们称为“容器(container)”。
在很多语言里,容器是以标准库的方式提供,你可以随时查看这些标准库的代码,了解如何创建,删除,维护内存。
数组(array)
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组(数组是值传递)。
数组的定义
数组定义格式如下
var 数组变量名 [元素数量]Type
语法说明如下所示:
- 数组变量名:数组声明及使用时的变量名。
- 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
- Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。
数组的每个元素都可以通过索引下标来访问,索引下标的范围是从 0 开始到数组长度减 1 的位置,内置函数 len() 可以返回数组中元素的个数。
默认情况下,数组的每个元素都会被初始化为元素类型对应的零值,对于数字类型来说就是 0
func main() {
// 定义数组,默认为对应元素的零值
var a [3]int
// 仅打印元素
for _, v := range a {
fmt.Printf("未赋值,元素:%d\n", v)
}
// 数组赋值
a = [3]int{1, 2, 3}
// 打印索引和元素
for i, v := range a {
fmt.Printf("索引:%d,元素:%d\n", i, v)
}
// 仅打印元素
for _, v := range a {
fmt.Printf("元素:%d\n", v)
}
}
使用”...”省略号,表示数组的长度是根据初始化值的个数来计算
func main() {
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
}
多维数组
Go语言中允许使用多维数组,因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值,多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据。
声明多维数组的语法如下所示:
var 多维数组变量名 [每一维度的长度][每一维度的长度]...[每一维度的长度] 数组类型
func main() {
// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 打印二维数组
for i := range array {
for _, j := range array[i] {
println(j)
}
}
}
切片(slice)
数组虽然有特定的用处,但因为数组的长度固定不变,这就显得有些呆,所以Go语言就有一种特殊的数据类型
切片(slice)
切片默认指向一段连续内存区域,可以是数组,也可以是切片本身,另外切片是可以
动态增长
的。
切片是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口
由于切片是对数组的一个连续片段的引用,所以切片有3个字段的数据结构:
- 指向底层数组的指针
- 切片的长度
- 切片的容量
切片的定义
从数组或切片生成新的切片
从连续内存区域生成切片是常见的操作
slice [开始位置 : 结束位置]
语法说明如下:
- slice:表示目标切片对象;
- 开始位置:对应目标切片对象的索引;
- 结束位置:对应目标切片的结束索引。
func main() {
// 定义数组
var arr = [6]int{1, 2, 3, 4, 5, 6}
// 从数组切片(左闭右开)
slice := arr[1:3]
// 打印数组
fmt.Println("arr:", arr) // arr: [1 2 3 4 5 6]
// 打印切片
fmt.Println("slice:", slice) // slice: [2 3]
// 切片的元素个数
fmt.Println("切片的元素个数:", len(slice)) // 2
// 获取切片的容量,容量可以动态变化
fmt.Println("切片的容量:", cap(slice)) // 5
fmt.Printf("数组中下标为1的地址:%p:\n", &arr[1]) // 0xc0000c8038
fmt.Printf("切片中下标为0的地址:%p:\n", &slice[0]) // 0xc0000c8038
slice[1] = 66
fmt.Println("arr:", arr) // arr: [1 2 66 4 5 6]
fmt.Println("slice:", slice) // slice: [2 66]
}
make()函数构造切片
如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make( []Type, size, cap )
语法说明如下:
- Type:切片类型
- size: 切片长度
- cap: 切片容量,不影响 size,主要目的是降低多次分配空间而造成的性能问题。
func main() {
slice := make([]int, 6, 10)
fmt.Println(slice) // [0 0 0 0 0 0]
fmt.Println("切片长度:", len(slice)) // 切片长度: 6
fmt.Println("切片容量:", cap(slice)) // 切片容量: 10
}
注:make底层维护一个数组,但是这个数组对外不可见,无法操作这个数组
直接声明新的切片
定义一个切片,直接指定具体的数组,原理类似make()
func main() {
slice := []int{1, 2, 3, 4, 5, 6}
fmt.Println(slice) // [1 2 3 4 5 6]
fmt.Println("切片长度:", len(slice)) // 切片长度: 6
fmt.Println("切片容量:", cap(slice)) // 切片容量: 6
}
追加函数
Go语言的内置函数 append() 可以为切片动态添加元素
func main() {
var slice []int
slice = append(slice, 1) // 追加1个元素
fmt.Println(slice) // [1]
slice = append(slice, 1, 2, 3) // 追加多个元素, 手写解包方式
fmt.Println(slice) // [1 1 2 3]
slice = append(slice, []int{1, 2, 3}...) // 追加一个切片, 切片需要解包
fmt.Println(slice) // [1 1 2 3 1 2 3]
}
切片的扩容机制
在添加元素时,如果空间不足,切片就会进行扩容,此时切片长度将会发生改变,扩容的规律是按容量的 2 倍数进行扩容
扩容原理:
- 创建一个新数组,将老数组的值复制到新数组中
- 在新数组中追加值
- 底层指针由指向老数组改为指向新数组
- 底层的新数组不能直接维护,需要通过切片间接维护
列表(list)
列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等。
在Go语言中,列表使用 container/list 包来实现,内部的实现原理是
双链表
,列表能够高效地进行任意位置的元素插入和删除操作。
列表的定义
列表定义格式如下:
通过 container/list 包的 New() 函数初始化 list
变量名 := list.New()
通过 var 关键字声明初始化 list
var 变量名 list.List
列表没有具体元素类型的限制,因此,列表的元素可以是任意类型,但这既带来了便利,也引来一些问题,例如转换某种特定的类型时将会发生宕机。
func main() {
// 创建一个列表
l := list.New()
// 插入列表尾部
l.PushBack("first")
l.PushBack("second")
// 插入列表头部
l.PushFront("head")
// 遍历列表
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value) // head first second
}
}
从列表中删除元素
列表插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值以及与其他节点之间的关系等信息,从列表中删除元素时,需要用到这个结构进行快速删除。
func main() {
// 创建一个列表
l := list.New()
// 尾部添加后保存元素句柄
element := l.PushBack("first")
// 在fist之后添加second
l.InsertAfter("second", element)
// 在fist之前添加head
l.InsertBefore("head", element)
println("删除前:")
// 遍历列表
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value) // head first second
}
// 删除first元素
l.Remove(element)
println("删除后:")
// 遍历列表
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value) // head second
}
}
映射(map)
映射(map)是一种特殊的数据结构,它内置在Go语言中 ,它有一个 key(索引)和一个 value(值),我们将它成为键值对,所以这个结构也称为关联数组或字典,这是一种能够快速寻找值的理想结构,给定 key,就可以迅速找到对应的 value。
map 这种数据结构在其他编程语言中也称为字典(Python)、hashmap(Java)
映射的定义
map 是引用类型,它的声明方式如下:
var mapname map[keytype]valuetype
语法说明如下:
- mapname 为 map 的变量名。
- keytype 为键类型。
- valuetype 是键对应的值类型。
在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目。
func main() {
// 定义map变量
var m map[string]string
// 只声明map内存是没有分配空间的
// 必须通过make函数进行初始化,才会分配空间,map的长度可以动态增长
m = make(map[string]string, 10)
// 键值对存入map
m["key1"] = "张三"
m["key2"] = "李四"
m["key3"] = "王五"
// 输出集合
fmt.Println(m) // map[key1:张三 key2:李四 key3:王五]
m["key1"] = "阿巴阿巴阿巴"
// 重复的key将会被覆盖
fmt.Println(m) // map[key1:阿巴阿巴阿巴 key2:李四 key3:王五]
// value可以重复
m["key4"] = "李四"
fmt.Println(m) // map[key1:阿巴阿巴阿巴 key2:李四 key3:王五 key4:李四]
}
使用map的注意事项:
- map使用前一定要make
- map的key-value是无序的
- key不可以重复,重复会被最后一个value覆盖
- value是可以重复的
map的容量
与数组不同,map 可以根据新增的 key-value 动态的扩容,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,如果不标明cap,它将默认分配一个内存空间
make(map[keytype]valuetype, cap)
当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
操作map
func main() {
// 定义map
m := make(map[string]string)
// 增
m["k1"] = "v1"
m["k2"] = "v2"
m["k3"] = "v3"
fmt.Println("增:", m)
// 删
delete(m, "k3")
fmt.Println("删:", m)
// 改
m["k2"] = "vv2"
fmt.Println("改:", m)
// 查
value, flag := m["k2"]
fmt.Println("查:", m)
fmt.Println(value)
fmt.Println(flag)
}
遍历map
map的遍历只能用for-range循环
func main() {
scene := make(map[string]int)
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
for k, v := range scene {
fmt.Println(k, v)
}
}
map的清空机制
Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不过不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。
面向对象
Go语言也支持面向对象编程(OOP),但它和传统的面向对象编程有些许区别,并不是纯粹的面向对象语言。
Go 语言中的类型可以被实例化,使用
new
或
&
构造的类型实例的类型是类型的指针。
Go语言中并没有类(class),但它的
结构体(struct)
和其他编程语言的类有同等的地位,结构体的内嵌配合接口比面向对象具有更高的扩展性和灵活性,结构体不仅能拥有方法,而且每种自定义类型也可以拥有自己的方法。
结构体
Go语言可以通过自定义的方式形成新的类型,结构体就是这些类型中的一种复合类型,结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。
Go语言的结构体和Java中的类有相同的地位,甚至比java中的类更灵活
结构体定义
结构体成员也可以称为“字段”,这些字段有以下特性:
- 字段拥有自己的类型和值;
- 字段名必须唯一;
- 字段的类型也可以是结构体,甚至是字段所在结构体的类型。
使用关键字
type
可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。
结构体的声明方式如下:
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型
…
}
语法说明如下:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- struct{}:表示结构体类型,
type 类型名 struct{}
可以理解为将 struct{} 结构体定义为类型名的类型。 - 字段1、字段2……:表示结构体字段名,结构体中的字段名必须唯一。
- 字段1类型、字段2类型……:表示结构体各个字段的类型。
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存
结构体实例
结构体必须在定义并实例化后才能使用结构体的字段。
实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体各个实例间的内存是完全独立的。
基本的实例化方式
基本实例化格式如下:
var 实例 结构体类型
这是最常见的实例化方式,直接创建
// Student 定义学生结构体,将学生的各个属性统一放到结构体中管理
type Student struct {
Name string
Age int
School string
}
func main() {
// 创建学生结构体实例
var s Student
// 结构体赋值,使用点(.)来访问结构体的成员变量
s.Name = "玄德"
s.Age = 20
s.School = "清北幼儿园"
fmt.Println(s) // {玄德 20 清北幼儿园}
}
创建指针结构体
Go语言中,还可以使用 new 关键字对类型进行实例化,结构体在实例化后会形成指针类型的结构体。
创建格式如下:
var 实例 结构体指针 = new(结构体)
// 也可以简写
实例 := new(结构体)
Go语言让我们可以像访问普通结构体一样使用
.
来访问结构体指针的成员,这是因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖,将 s.Name 形式转换为 (*s).Name。
// Student 定义学生结构体,将学生的各个属性统一放到结构体中管理
type Student struct {
Name string
Age int
School string
}
func main() {
// 创建学生结构体实例
s := new(Student)
// 实例s 其实是指针 *s,s指向的是地址
(*s).Name = "玄德"
// 因为go语言的语法糖,简化了赋值方式,可以直接使用.
s.Age = 20
s.School = "清华幼儿园"
fmt.Println(s) // &{玄德 20 清华幼儿园}
}
结构体的地址实例化
在Go语言中,对结构体进行
&
取地址操作时,视为对该类型进行一次 new 的实例化操作
取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程
// Student 定义学生结构体,将学生的各个属性统一放到结构体中管理
type Student struct {
Name string
Age int
School string
}
func main() {
// 创建学生结构体实例
s := &Student{School: "清华幼儿园"}
(*s).Name = "玄德"
s.Age = 20
fmt.Println(s) // &{玄德 20 清华幼儿园}
}
结构体之间的转换
结构体是单独定义的类型,和其他类型转换时需要有完全相同的字段,转换时还需加上结构体类型
type Student struct {
Name string
}
type Person struct {
Name string
}
func main() {
s := &Student{""}
p := &Person{Name: "玄德"}
s = (*Student)(p)
fmt.Println(s) // &{玄德}
fmt.Println(p) // &{玄德}
}
结构体进行type重新定义(相当于取别名),Golang认为这是新的数据类型,但是相互间可以强转
type Student struct {
Name string
}
type Stu Student
func main() {
s1 := &Student{""}
s2 := &Stu{Name: "玄德"}
s1 = (*Student)(s2)
fmt.Println(s1) // &{玄德}
fmt.Println(s2) // &{玄德}
}
方法
Go语言的方法是作用在指定的数据类型上,和指定的数据类型相互绑定,因此
自定义类型,都可以有方法
,而不仅仅是struct
方法的定义
方法的声明方式如下:
type A struct {
字段 字段类型
}
func (a A) 方法名() {
方法体
}
方法和函数的定义类似,但方法需要传递结构体的类型
func (a A) 方法名()
相当于A结构体有一个test方法,
方法和结构体需要有绑定关系
由于结构体类型是值传递,所以方法的传递也是值传递,因此方法遵守值类型的传递机制,值拷贝传递
// 定义Person结构体
type Person struct {
Name string
}
// 给Person结构体绑定test方法
func (p Person) test() {
fmt.Println(p.Name)
}
func main() {
// 创建结构体对象
var p Person
p.Name = "玄德"
p.test()
}
如果希望在方法中,改变结构体变量的值,可以通过结构体指针的方式来处理
// 定义Person结构体
type Person struct {
Name string
}
// 给Person结构体绑定test方法
func (p *Person) test() {
p.Name = "阿巴阿巴" // 已简化,实际为(*p)
fmt.Println(p.Name)
}
func main() {
// 创建结构体对象
var p Person
p.Name = "玄德" // 已简化,实际为(&p)
p.test()
fmt.Println(p.Name)
}
方法访问规则
方法名首字母小写,只能在本包访问,方法名首字母大写,可以在本包和其他包访问
自定义类型方法
Go语言中的方法作用在指定的数据类型上,和指定的数据类型绑定,因此
自定义类型,都可以有方法
,不仅仅是结构体
type integer int
func (i integer) print() {
i = 60
fmt.Println("i = ", i)
}
func (i *integer) change() {
*i = 60
fmt.Println("i = ", *i)
}
func main() {
var i integer = 30
i.print()
fmt.Println(i)
i.change()
fmt.Println(i)
}
方法和函数的区别
方法:
- 需要绑定指定的数据类型
- 方法的调用是变量.方法名(实参列表)
- 因为编译器做了处理,所以指针类型和值类型都可以传入和接收
函数:
- 不需要绑定数据类型
- 函数的调用是函数名(实参列表)
- 参数类型是什么就要传入什么
type Student struct {
Name string
}
// 定义方法
func (s *Student) test01() {
fmt.Println(s.Name)
}
func (s *Student) test02() {
fmt.Println(s.Name)
}
// 定义函数
func method01(s Student) {
fmt.Println(s.Name)
}
func method02(s *Student) {
fmt.Println(s.Name)
}
func main() {
var s = Student{"玄德"}
// 调用函数
method01(s) // method01(&s)报错
method02(&s) // method02(s)报错
println("----------------分界线------------------")
// 调用方法
s.test01()
(&s).test01() // 虽然是指针类型调用,但传递还是值传递
s.test02() // 编译器做了处理,所以等价下面的语句
(&s).test02()
}
封装
封装的定义
封装(encapsulation)是把抽象出来的的字段和对字段的操作封装在一起,数据被保护在内部,程序的其他包只有通过被授权的操作方法,才能对字段进行操作
在程序设计的过程中要追求“高内聚,低耦合”。
高内聚:类的内部数据操作细节自己来完成,不允许外部干涉
低耦合:仅暴露少量的方法给外部使用
而封装,就是禁止直接访问一个对象中的数据,而是应该通过操作接口来访问,大白话就是该露的露,该藏的藏,你要访问必须经过我设置的规矩(方法),否则你就动不了它(字段)
实现封装
- 将结构体、 字段(属性)的首字母小写(让其它包不能使用,类似private,但不小写也有可能,因为go语言封装不那么严格)
- 给结构体所在包提供一个工厂模式的函数,首字母大写(类似一个构造函数)
- 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
- 提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值
代码实现
目录结构:
├─test
│ └─src
│ └─main
│ └─go
│ └─model
│ └─person.go
│ test.go
person.go
package model
import "fmt"
type person struct {
Name string
age int // 首字母小写,其他包不能直接访问
}
// 定义工厂模式的函数,相当于其他语言的构造器
func NewPerson(name string) *person {
return &person{
Name: name,
}
}
// 定义set和get方法,对age字段进行封装,因为在方法中可以定义一系列的限制操作,确保封装字段的安全合理性
func (p *person) SetAge(age int) {
if age > 0 && age < 150 {
p.age = age
} else {
fmt.Println("请输入正确的年龄")
}
}
func (p *person) GetAge() int {
return p.age
}
main.go
package main
import (
"Test/src/main/go/model"
"fmt"
)
func main() {
// 创建person结构体
p := model.NewPerson("玄德")
p.SetAge(20)
fmt.Println(p.Name) // 玄德
fmt.Println(p.GetAge()) // 20
fmt.Println(*p) // 玄德
}
继承
继承的定义
Go语言中的继承是通过
内嵌或组合
来实现的,当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出相同的部分重新定义一个新的结构体:
匿名结构体
,在这个匿名结构体中包含它们重合的属性和方法,其它的结构体不需要重新定义这些属性和方法,只需要嵌套一个匿名结构体即可。
继承的实现
在Go语言中,
如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法
,从而实现继承的特性
- 要实现继承,必须内嵌匿名结构体
- 结构体内嵌后可以使用匿名结构体的所有字段和方法,并且没有大小写限制
- 匿名结构体字段和方法的访问可以简化
- 结构体的匿名字段可以是基本数据类型
// 定义动物结构体
type Animal struct {
Age int
Weight float64
}
// 给Animal绑定方法
func (a *Animal) Shout() {
fmt.Print("现在我要说话了:")
}
func (a *Animal) ShowInfo() {
fmt.Printf("我的年龄是:%v岁,体重是:%vkg\n", a.Age, a.Weight)
}
// 定义结构体Cat
type Cat struct {
// 为了复用性,体现继承思维,加入匿名结构体
Animal
}
// Cat绑定特有方法
func (c *Cat) catShout() {
fmt.Println("喵~")
}
func main() {
// 创建Cat结构体实例
cat := &Cat{}
cat.Age = 3 // 原为cat.Animal.Age,已简化
cat.Weight = 10.6
cat.ShowInfo() // 我的年龄是:3岁,体重是:10.6kg
cat.Shout()
cat.catShout() // 现在我要说话了:喵~
}
继承的特性
就近访问原则
当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则,如果想要访问匿名结构体的字段或方法时,可以通过匿名结构体名来区分
type Animal struct {
Age int
}
func (a *Animal) f() {
fmt.Printf("我是父类的方法~,Age:%v\n", a.Age)
}
type Cat struct {
Animal
Age int
}
func (c *Cat) f() {
fmt.Printf("我是子类的方法~,Age:%v\n", c.Age)
}
func main() {
// 创建Cat结构体实例
cat := &Cat{}
cat.Age = 3 // 就近原则
cat.Animal.Age = 6
cat.f() // 就近原则
cat.Animal.f()
}
多重继承
Go语言中支持多继承,但为了保证代码的间接性,建议尽量不使用多重继承
type A struct {
a int
b string
}
type B struct {
c int
d string
}
type C struct {
A
B
}
func main() {
c := C{A{10, "aaa"}, B{20, "bbb"}}
fmt.Println(c)
}
接口
Go 语言的接口设计是
非侵入式
的,接口编写者无须知道接口被哪些类型实现,而接口的实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口,因为编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。
Go语言并不是一种 “传统” 的面向对象编程语言,它并没有类的概念,继承也是通过内嵌或组合来实现的,接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程。
Go语言里有非常灵活的接口概念,通过它可以实现很多面向对象的特性,这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义,当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
接口的声明
接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。
- 接口中可以定义一组方法,但不需要实现,不需要方法体。并且接口中不能包含任何变量。直到某个自定义类型要使用的时候,再根据具体情况把这些方法具体实现出来
- 实现接口要实现
所有的方法
才算是实现 - Go语言中的接口,不需要显式的实现接口
- 接口的目的是为了定义规范,具体由别人来实现即可
接口声明格式如下
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
部分参数讲解
- 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
- 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略
代码示例:
// 接口的定义:定义规则、定义规范,定义某种能力
type AnimalSay interface {
// 实现某种没有实现的方法
say()
}
// 接口的实现
type Cat struct {
}
type Dog struct {
}
// 实现接口的方法
func (c Cat) say() {
fmt.Println("喵~")
}
func (d Dog) say() {
fmt.Println("汪~")
}
// 定义一个函数
func shout(s AnimalSay) {
s.say()
}
func main() {
c := Cat{}
d := Dog{}
// 猫叫
shout(c)
// 狗叫
shout(d)
}
接口的特性
- 接口本身不能创建实例,但可以指向一个实现了该接口自定义类型的变量
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
- 一个自定义类型可以实现很多接口
- 一个接口可以继承多个接口,如果这时要实现接口,那么必须将
继承的几个接口方法全部实现 - interface类型默认是一个指针(引用类型),如果没有对interfce初始化就使用,那么将会输出空
- 空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把如何一个变量赋给空接口
多态
Go语言中的面向对象是抽象的,因此在Go语言中多态特征是
通过接口实现的
,可以按照统一的接口来调用不同的实现,这时接口变量就呈现出不同的形态
上方代码的这个函数,其中的
s
通过上下文来识别具体是什么类型的实例就完美体现了多态的表现
func shout(s AnimalSay) {
s.say()
}
接口体现多态特征
多态参数:其中的s就是多态参数
func shout(s AnimalSay) { s.say() }
多态数组:定义一个接口数组,里面存放各个结构体变量即可
var arr [3]AnimalSay arr[0] = Dog{"小狗1号"} arr[1] = Cat{"小猫1号"} arr[2] = Cat{"小猫2号"}
如果有过其他语言中面向对象的基础应该很好理解,但对于纯小白来说还是推荐去看更详细的资料
断言
类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型,也就是直接
判断是否是该类型的变量
。
Go语言中类型断言的语法格式如下:
value, ok := x.(T)
其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。
该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:
- 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
- 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
- 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
代码示例:
package main
import (
"fmt"
)
func main() {
// 定义接口
var x interface{}
x = 10
// 断言:x是否能转成int类型并赋值给value,flag判断是否成功
value, flag := x.(int)
fmt.Println(value, ",", flag) // 10 , true
}
搭配switch使用
类型断言还可以配合 switch 使用,示例代码如下:
func main() {
var a int
a = 10
getType(a)
}
func getType(a interface{}) {
switch a.(type) {
case int:
fmt.Println("the type of a is int")
case string:
fmt.Println("the type of a is string")
case float64:
fmt.Println("the type of a is float")
default:
fmt.Println("unknown type")
}
}
并发
程序(program):为了完成某种特定任务而编写的静态代码,程序是静态的,程序运行才产生了进程
进程(process):程序的一次执行过程,每个进程都会在内存中有自己的内存区域,进程是动态的,它有自己的生命周期,有产生、存在、消亡的过程
线程(thread):进程可以有多条线程,线程只是程序内部的一条执行路径
协程(goroutine ):协程是一种用户态的轻量级线程,这里的协程和其他语言的协程(coroutine)不一样
管道(channel):协程之间通信的桥梁,管道是双向的
并行(Concurrent):多个线程交替操作同一个资源类
并发(Paralled):多个线程同时操作多个资源类
死掉的程序只是存储器上的数据,活过来的程序就是进程,没错,进程是有生命的,他有自己的生命周期。
协程
Go主线程也可称为线程,也可以理解为进程,一个Go主线程上可以起多个协程,可以理解协程为轻量级的线程,资源消耗较小
协程的特点:有独立的栈空间、共享程度堆空间、调度由用户控制,是轻量级的线程(协程的本质是单线程)
Go语言高并发的的特性就是基于协程
认识协程
以下全部为知识点,有些枯燥,但看完后会更全面的的了解go语言的协程
协程为什么比线程快
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。从技术的角度来说,“协程就是你可以暂停执行的函数”。
在协程的调度切换时,可以将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程和线程的区别
一个线程可以多个协程,一个进程也可以单独拥有多个协程,线程进程都是同步机制,而协程则是异步。
协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态,线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
协程并不是取代线程,而且抽象于线程之上,线程是协程的资源,协程通过执行器(Interceptor) 来间接使用线程这个资源。
goroutine 和 coroutine
C#、python、Lua等语言都支持 coroutine 特性,两者虽然都可以将函数或者语句在独立的环境中运行,但go语言的goroutine支持并行执行,而coroutine始终顺序执行,goroutines 是通过通道来通信;而coroutines 通过让出和恢复操作来通信,goroutines 比 coroutines 更强大,也很容易从 coroutines 的逻辑复用到 goroutines。
狭义地说,goroutine 可能发生在多线程环境下,goroutine 无法控制自己获取高优先度支持;coroutine 始终发生在单线程,coroutine 程序需要主动交出控制权,宿主才能获得控制权并将控制权交给其他 coroutine。
goroutine 间使用 channel 通信,coroutine 使用 yield 和 resume 操作。
goroutine 属于抢占式任务处理,操作系统如果发现一个应用程序长时间大量地占用 CPU,那么用户有权终止这个任务。
协程的开启
协程的开启十分简单,只需要在函数前面加上
go
关键字即可
协程是和主线程一同执行的
示例代码如下:
package main
import (
"fmt"
"time"
)
// 主线程
func main() {
// 开启协程
go test()
for i := 0; i < 10; i++ {
fmt.Printf("我是主线程,执行了%v次\n", i+1)
// 阻塞一秒
time.Sleep(time.Second)
}
fmt.Printf("我是主线程,我要结束运行了")
}
func test() {
for i := 0; i < 10; i++ {
fmt.Printf("我是一个协程,执行了%v次\n", i+1)
// 阻塞一秒
time.Sleep(time.Second)
}
}
主死从随
如果主线程结束了,即使协程还没有执行完毕,那么协程也会跟着退出,但协程结束了并不会影响主进程
代码示例:
上方代码的test函数更改为无限循环即可
func test() {
for i := 0; i < 10; i++ {
fmt.Printf("我是一个协程,执行了%v次\n", i+1)
// 阻塞一秒
time.Sleep(time.Second)
}
}
匿名函数创建多个协程
func main() {
// 匿名函数+外部变量 = 闭包
for i := 0; i < 6; i++ {
// 启动一个协程
// 使用匿名函数,直接调用匿名函数
go func(n int) {
fmt.Printf("我是第%v个协程\n", n+1)
}(i)
}
// 阻塞一秒
time.Sleep(time.Second)
fmt.Printf("我是主线程,我要结束运行了")
}
WaitGroup
Go语言的WaitGroup是一种用于管理多个goroutine的工具,它可以帮助开发者确保所有goroutine都完成了任务,然后再继续执行下一步操作。简单来说就是控制协程的主死从随
WaitGroup的更多详情请查看Go语言中文标准库:
Go语言标准库
代码示例:
package main
import (
"fmt"
"sync"
)
// 定义WaitGroup
var wg sync.WaitGroup
func main() {
// 启动6个协程
for i := 1; i < 7; i++ {
wg.Add(1) // 协程开始时加1
go func(n int) {
defer wg.Done() // 协程执行完减1
fmt.Printf("你好,我是第%v个协程\n", n)
}(i)
}
// 阻塞主线程,当wg减为0时,停止阻塞
wg.Wait()
}
互斥锁
当协程操作同一个数据的时候会发生抢占资源的行为,导致数据结果不准确,这时,我们就需要互斥锁(Mutex)来解决这个问题(试试不加锁会出现什么(~ ̄▽ ̄~)),注:互斥锁性能、效率较低
Go语言的mutex(互斥锁)是一种用于在多个goroutine之间同步访问共享资源的机制。它可以防止多个goroutine同时访问共享资源,从而避免数据竞争。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var totalNum int
// 加入互斥锁
var lock sync.Mutex
func main() {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Println(totalNum)
}
func sub() {
defer wg.Done()
for i := 0; i < 1000; i++ {
// 加锁
lock.Lock()
totalNum -= 1
// 解锁
lock.Unlock()
}
}
func add() {
defer wg.Done()
for i := 0; i < 1000; i++ {
lock.Lock()
totalNum += 1
lock.Unlock()
}
}
读写锁
当我们遇到读多写少的场景时,由于读对数据不产生影响,所以推荐使用读写锁(RWMutex)
Go语言的RWMutex(读写锁)是一种同步机制,它可以同时允许多个读取操作,但只允许一个写入操作。它可以帮助程序员控制对共享资源的访问,以避免竞争条件和数据不一致的问题。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
// 加入读写锁
var lock sync.RWMutex
func main() {
wg.Add(6)
// 模拟读多写少
for i := 0; i < 5; i++ {
go read()
}
go write()
wg.Wait()
}
func read() {
defer wg.Done()
lock.RLock() // 如果只是读数据,这个锁不发挥作用,但读写同时发生时,锁就会发挥作用
fmt.Println("开始读取数据")
time.Sleep(time.Second)
fmt.Println("读取数据成功")
lock.RUnlock()
}
func write() {
defer wg.Done()
lock.Lock()
fmt.Println("开始修改数据")
time.Sleep(time.Second * 6)
fmt.Println("修改数据成功")
lock.Unlock()
}
捕获错误
package main
import (
"fmt"
"time"
)
func main() {
go printNum()
go division()
time.Sleep(time.Second)
}
func division() {
// 捕获错误并处理
defer func() {
err := recover()
if err != nil {
fmt.Println("division函数出现错误", err)
}
}()
num1 := 10
num2 := 0
result := num1 / num2
fmt.Println(result)
}
func printNum() {
for i := 0; i < 6; i++ {
fmt.Println(i + 1)
}
}
管道
简但来说管道(channel)是协程(goroutine )之间的通信机制
管道的特性:
管道可以实现多个goroutine之间的通信
管道可以实现数据的流动、缓冲、同步
管道可以实现数据的
安全传输
管道的性质:
管道的本质就是一个基于队列的数据结构,因此它的数据是
先进先出
的管道自身线程安全,多协程访问时,不需要加锁,因为它本身就是线程安全的
管道有类型,一个固定类型的管道只能存放固定类型的数据
Go语言的管道是一种编程模式,它可以让程序员将多个函数连接起来,每个函数处理输入数据,并将处理后的结果传递给下一个函数。管道可以让程序员更容易地处理复杂的数据处理任务,并且可以更快地完成任务。
管道入门
通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型,通道的元素类型就是在其内部传输的数据类型
管道的声明方式如下:
var 管道变量 chan 管道类型
管道是引用类型,它在内存里的值是一个地址,所以需要使用 make 进行创建,格式如下:
管道实例 := make(chan 数据类型)
管道创建后,就可以使用
<-
对通道进行发送和接收操作
管道变量 <- 值
代码示例:
package main
import "fmt"
func main() {
// 声明一个int类型的管道
var intChan chan int
// make进行初始化:管道可以存放3个int类型的数据
intChan = make(chan int, 3)
// 存放数据
intChan <- 6
intChan <- 66
intChan <- 666
// 从管道中读取数据
data1 := <-intChan
data2 := <-intChan
data3 := <-intChan
fmt.Println("data1:", data1)
fmt.Println("data2:", data2)
fmt.Println("data3:", data3)
// 输出管道
fmt.Printf("管道的实际长度:%v,管道的容量是:%v", len(intChan), cap(intChan))
}
注意:
- 管道不能存放大于容量的数据
- 如果接收方一直没有接收,那么发送操作将持续阻塞
- 在不使用协程的情况下,如果管道的数据已经全被取出,那么再取就会报错
管道的关闭
内置函数close可以关闭管道,
关闭后只能读取数据,但不能在写入数据
package main
func main() {
intChan := make(chan int, 6)
intChan <- 6
intChan <- 66
// 关闭管道
close(intChan)
// 写入管道
// intChan <- 666 // 报错:send on closed channel
// 读取管道
data := <-intChan
println(data) // 6
}
管道的遍历
管道的遍历使用for-range遍历
package main
import "fmt"
func main() {
intChan := make(chan int, 6)
for i := 0; i < 6; i++ {
intChan <- i
}
close(intChan)
// 遍历
for data := range intChan {
fmt.Println("data =", data)
}
}
单向管道
Go语言的类型系统提供了单方向的 channel 类型,顾名思义,单向 channel 就是只能用于写入或者只能用于读取数据。当然 channel 本身必然是同时支持读写的,否则根本没法用。
单向管道的声明
单向 channel 变量的声明非常简单,只能写入数据的通道类型为
chan<-
,只能读取数据的通道类型为
<-chan
单向管道的声明方式如下:
var 管道实例 chan <- 元素类型 // 只能写入数据的通道
var 管道实例 <- chan 元素类型 // 只能读取数据的通道
代码示例:
package main
import "fmt"
func main() {
// 声明只写管道
var ch1 chan<- int
ch1 = make(chan int, 6)
ch1 <- 66
//data1 := <-ch // 报错:cannot receive from send-only channel
fmt.Println("ch1地址:", ch1)
// 声明只读管道
var ch2 <-chan int
// ch2 <- 66 // 报错:cannot send to receive-only channel
if ch2 != nil {
data2 := <-ch2
fmt.Println("data2:", data2)
} else {
fmt.Println("ch2值为空")
}
}
select
Go语言的select关键字用于多路复用,它可以同时监听多个通道的数据流动,当某个通道有数据流动时,就会进行处理。
select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述,与 switch 语句相比,select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个 IO 操作
package main
import (
"fmt"
"time"
)
func main() {
// 定义int管道
intChan := make(chan int, 1)
go func() {
time.Sleep(time.Second * 6)
intChan <- 6
}()
// 定义string管道
strChan := make(chan string, 1)
go func() {
time.Sleep(time.Second)
strChan <- "玄德"
}()
select {
case data := <-intChan:
fmt.Println("intChan:", data)
case data := <-strChan:
fmt.Println("strChan:", data)
default:
fmt.Println("防止select被阻塞")
}
}
并发编程
并发可以让多个任务同时运行,从而提高程序的效率,Go语言利用协程和管道可以轻松做到百万并发量,至于是不是真的百万并发我也不知道(~ ̄▽ ̄~)
协程与管道
Go语言通过协程与管道可以实现复杂的并发编程,复习一下协程和管道吧
操作同一个管道
利用WaitGroup来阻塞
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
wg.Add(2)
intChan := make(chan int, 66)
// 开启读和写的协程,共同操作一个管道
go writeData(intChan)
go redaData(intChan)
// 等待协程
wg.Wait()
}
// 写
func writeData(intChan chan int) {
// 在函数退出时调用Done 来通知main 函数工作已经完成
defer wg.Done()
for i := 0; i < 66; i++ {
intChan <- i + 1
fmt.Println("写入的数据为:", i)
time.Sleep(time.Second)
}
close(intChan)
}
// 读
func redaData(intChan chan int) {
defer wg.Done()
for data := range intChan {
fmt.Println("读取的数据为:", data)
}
}
利用管道来阻塞
package main
import (
"fmt"
"time"
)
func main() {
// 声明被操作的管道
intChan := make(chan int, 66)
// 声明阻塞管道
exitChan := make(chan bool, 1)
// 开启读和写的协程,共同操作一个管道
go writeData(intChan)
go redaData(intChan, exitChan)
time.Sleep(time.Second)
fmt.Printf("运行完毕")
}
// 写
func writeData(intChan chan int) {
for i := 0; i < 66; i++ {
intChan <- i + 1
fmt.Println("写入的数据为:", i+1)
}
close(intChan)
}
// 读
func redaData(intChan chan int, exitChan chan bool) {
for data := range intChan {
fmt.Println("读取的数据为:", data)
}
// 读取完毕
exitChan <- true
close(exitChan)
}
求素数
求10000以内的素数,利用协程试一试,在不用协程试一试,看看哪个更快<( ̄︶ ̄)>
package main
import "fmt"
var intChan = make(chan int, 10000)
func main() {
go initChan(10000)
var primeChan = make(chan int, 10000)
var exitChan = make(chan bool, 8)
for i := 0; i <= 8; i++ {
go isPrime(intChan, primeChan, exitChan)
}
go func() {
for i := 0; i < 8; i++ {
<-exitChan
}
close(primeChan)
}()
for res := range primeChan {
fmt.Println("素数:", res)
}
}
func initChan(num int) {
for i := 1; i <= num; i++ {
intChan <- i
}
close(intChan)
}
func isPrime(intChan <-chan int, primeChan chan int, exitChan chan<- bool) {
var flag bool
for num := range intChan {
flag = true
for j := 2; j < num; j++ {
if num%j == 0 {
flag = false
continue
}
}
if flag {
primeChan <- num
}
}
exitChan <- true
}
生产者消费者
通过协程和管道实现生产者消费者模型╮( ̄▽ ̄)╭
代码实现:
package main
import (
"fmt"
"strconv"
)
func main() {
storageChan := make(chan Product, 1000)
shopChan := make(chan Product, 1000)
exitChan := make(chan bool, 1)
// 协程生产
for i := 0; i < 999; i++ {
go Producer(storageChan, 1000)
}
go Logistics(storageChan, shopChan)
go Consumer(shopChan, 1000, exitChan)
if <-exitChan {
return
}
}
// Product 商品
type Product struct {
Name string
}
// Producer 生产者
func Producer(storageChan chan<- Product, count int) {
for {
producer := Product{"商品: " + strconv.Itoa(count)}
storageChan <- producer
count--
fmt.Println("生产了", producer)
if count < 1 {
return
}
}
}
// Logistics 运输者
func Logistics(storageChan <-chan Product, shopChan chan<- Product) {
for {
product := <-storageChan
shopChan <- product
fmt.Println("运输了", product)
}
}
// Consumer 消费者
func Consumer(shopChan <-chan Product, count int, exitChan chan<- bool) {
for {
product := <-shopChan
fmt.Println("消费了", product)
count--
if count < 1 {
exitChan <- true
return
}
}
}
网络编程
Go语言里面提供了一个完善的 net/http 包,通过 net/http 包我们可以很方便的搭建一个可以运行的 Web 服务器。同时使用 net/http 包能很简单地对 Web 的路由,静态文件,模版,cookie 等数据进行设置和操作。
简单的Web服务器
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", index) // index 为向 url发送请求时,调用的函数
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
func index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "玄德的网址:http://xuande-hk.gitee.io")
}
浏览器访问localhost:8080,结果如下:
TCP网络协议通信
首先启动服务端,然后启动客户端
客户端
package main
import (
"bufio"
"fmt"
"net"
"os"
)
func main() {
fmt.Println("客户端启动中。。。。。。")
// 选择tcp协议,指定服务端ip和端口号
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("客户端连接失败:", err)
return
}
fmt.Println("连接成功:", conn)
// 通过客户端发送数据
reader := bufio.NewReader(os.Stdin) // os.Stdin表示终端标准输入
// 从终端读取一行用户输入的信息
str, err := reader.ReadString('\n')
if err != nil {
fmt.Println("终端输入失败:", err)
return
}
// 将数据发送给服务器
write, err := conn.Write([]byte(str))
if err != nil {
fmt.Println("数据发送失败:", err)
return
}
fmt.Printf("数据发送成功,共发送%d字节数据\n", write)
fmt.Printf("客户端结束连接。。。。。。")
}
服务端
package main
import (
"bufio"
"fmt"
"net"
"os"
)
func main() {
fmt.Println("服务端启动中。。。。。。")
// 监听客户端,同样需要选择tcp协议,指定服务端ip和端口号
listen, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("监听失败:", err)
return
}
// 监听成功,等待客户端连接
fmt.Println("启动成功,等待客户端连接")
for {
conn, err2 := listen.Accept()
if err2 != nil {
fmt.Println("客户端连接失败:", err)
return
} else {
fmt.Println("客户端连接成功:", conn)
fmt.Println("客户端信息为:", conn.RemoteAddr().String())
}
// 用协程处理客户端服务请求
go process(conn)
}
}
func process(conn net.Conn) {
// 关闭连接
defer conn.Close()
for {
// 读取数据的切片
buf := make([]byte, 1024)
// 从conn连接中读取数据
read, err := conn.Read(buf)
if err != nil {
return
}
// 服务端输出
fmt.Println("接收到客户端数据:" + string(buf[0:read]))
}
}
反射
Go语言中的反射是一种动态的编程技术,它允许程序在运行时获取有关自身结构和行为的信息,并可以根据这些信息动态地改变自身的行为。反射可以让程序更加灵活,可以更好地处理复杂的问题。
大多数现代的高级语言都以各种形式支持反射功能,反射是把双刃剑,功能强大但代码可读性并不理想,若非必要并不推荐使用反射。
了解反射
反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
C/C++ 语言没有支持反射功能,Lua、JavaScript 类动态语言因为其本身的语法特性并不需要反射,Java、C# 、Go等语言都支持完整的反射功能。
Go程序在运行期使用reflect包访问程序的反射信息。
- reflect.TypeOf(变量名),获取变量的类型,返回reflect.Type类型
- reflect.ValueOf(变量名),获取变量的值,返回reflect.Value类型(结构体类型里面包含关于变量的信息)
反射可以做什么?
- 反射可以在运行时动态获取变量的各种信息,比如变量的类型,类别等信息
- 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
- 通过反射,可以修改变量的值,可以调用关联的方法
反射的类型和种类
在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。例如需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。
反射具体的方法请查看:
Go语言中文标准库
的reflect包
反射的类型
Go语言程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称
基本类型的反射
package main
import (
"fmt"
"reflect"
)
func main() {
var num = 66
testReflect(num)
}
// 定义一个空接口的函数
func testReflect(i interface{}) {
// 调用TypeOf函数,返回reflect.Type类型的数据
reType := reflect.TypeOf(i)
fmt.Println("reType:", reType)
fmt.Printf("reType的具体类型是:%T \n", reType)
// 调用ValueOf函数,返回reflect.Value类型的数据
reValue := reflect.ValueOf(i)
fmt.Println("reValue:", reValue)
fmt.Printf("reValue的具体类型是:%T", reValue)
}
结构体的反射
package main
import (
"fmt"
"reflect"
)
// Student 学生结构体
type Student struct {
Name string
Age int
}
func main() {
stu := Student{
Name: "玄德",
Age: 20,
}
testReflect(stu)
}
// 定义一个空接口的函数
func testReflect(i interface{}) {
// 调用TypeOf函数,返回reflect.Type类型的数据
reType := reflect.TypeOf(i)
fmt.Println("reType:", reType)
fmt.Printf("reType的具体类型是:%T \n", reType)
// 调用ValueOf函数,返回reflect.Value类型的数据
reValue := reflect.ValueOf(i)
fmt.Println("reValue:", reValue)
fmt.Printf("reValue的具体类型是:%T \n", reValue)
// reValue转为空接口
i2 := reValue.Interface()
// 类型断言
n, flag := i2.(Student)
if flag {
fmt.Printf("学生的名字: %v\n学生的年龄:%v\n", n.Name, n.Age)
}
}
反射的种类
当需要区分一个大品种的类型时,就会用到种类(Kind)
Kind用于检查反射对象的类型,可以用来判断反射对象是否是指定的类型,以及反射对象的类型是什么。
获取变量种类的两种方式
- reflect.Type.Kind()
- reflect.Value.Kind()
种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
代码示例:
package main
import (
"fmt"
"reflect"
)
// Student 学生结构体
type Student struct {
Name string
Age int
}
func main() {
stu := Student{
Name: "玄德",
Age: 20,
}
testReflect(stu)
}
// 定义一个空接口的函数
func testReflect(i interface{}) {
// 调用TypeOf函数,返回reflect.Type类型的数据
reType := reflect.TypeOf(i)
fmt.Println("reType:", reType)
fmt.Printf("reType的具体类型是:%T \n", reType)
// 调用ValueOf函数,返回reflect.Value类型的数据
reValue := reflect.ValueOf(i)
fmt.Println("reValue:", reValue)
fmt.Printf("reValue的具体类型是:%T \n", reValue)
// 获取变量的类别
k1 := reType.Kind()
fmt.Println(k1)
k2 := reValue.Kind()
fmt.Println(k2)
}
反射的操作
反射操作时用到的函数
- Elem(),值指向的元素值,类似于语言层
*
操作。当值类型不是指针或接口时发生宕 机,空指针时返回 nil 的 Value - Setlnt(x int64),使用 int64 设置值。当值的类型不是 int、int8、int16、 int32、int64 时会发生宕机
通过反射修改变量
package main
import (
"fmt"
"reflect"
)
func main() {
// 声明整型变量a并赋初值
var a int = 66
// 获取变量a的反射值对象(a的地址)
valueOfA := reflect.ValueOf(&a)
// 取出a地址的元素(a的值)
valueOfA = valueOfA.Elem()
// 修改a的值为1
valueOfA.SetInt(666)
// 打印a的值
fmt.Println(valueOfA.Int())
}
通过反射修改结构体的值
注:结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,因此为了能修改结构体的值,需要将该字段导出。
package main
import (
"fmt"
"reflect"
)
type Cat struct {
Name string
Age int
}
func main() {
// 定义结构体实例
cat := Cat{Name: "猫猫", Age: 0}
fmt.Printf("修改前的名字: %v \n", cat.Name)
fmt.Printf("修改前的年龄: %v \n", cat.Age)
// 获取Cat实例地址的反射值对象
valueOfCat := reflect.ValueOf(&cat)
// 取出cat实例地址的元素
valueOfCat = valueOfCat.Elem()
// 获取并修改Name字段的值
valueOfCat.FieldByName("Name").SetString("小猫")
// 获取Age字段的值
catAge := valueOfCat.FieldByName("Age")
// 尝试设置age的值(如果字段没有被导出,这里会发生崩溃)
catAge.SetInt(1)
fmt.Printf("修改后的名字: %v \n", cat.Name)
fmt.Printf("修改后的年龄: %v \n", cat.Age)
}
通过反射操作结构体属性和方法
package main
import (
"fmt"
"reflect"
)
type Cat struct {
Name string
Age int
}
func (cat Cat) Test1() {
fmt.Println("这是第一个方法")
}
func (cat Cat) Test2() {
fmt.Println("这是第二个方法")
}
func (cat Cat) Test3(a int, b int) {
fmt.Println("这是第三个方法,这是一个求和方法")
fmt.Printf("%v + %v = %v", a, b, a+b)
}
func main() {
// 定义结构体实例
cat := Cat{Name: "猫猫", Age: 0}
// 获取Cat实例的反射值对象
valueOfCat := reflect.ValueOf(cat)
fmt.Printf("结构体实例反射值对象:%v \n", valueOfCat)
// 获取结构体内部的字段数量
field := valueOfCat.NumField()
fmt.Printf("结构体内部字段数量:%v \n", field)
// 获取具体字段
for i := 0; i < field; i++ {
fmt.Printf("第%d个字段的值是:%v \n", i+1, valueOfCat.Field(i))
}
// 获取结构体方法数量
method := valueOfCat.NumMethod()
fmt.Printf("结构体内部方法数量:%v \n", method)
// 调用Test2方法,方法首字母必须大写
valueOfCat.MethodByName("Test2").Call(nil)
// 调用Test3方法,传入参数
var params []reflect.Value
params = append(params, reflect.ValueOf(3))
params = append(params, reflect.ValueOf(3))
valueOfCat.MethodByName("Test3").Call(params)
}
结果如下:
结构体实例反射值对象:{猫猫 0}
结构体内部字段数量:2
第1个字段的值是:猫猫
第2个字段的值是:0
结构体内部方法数量:3
这是第二个方法
这是第三个方法,这是一个求和方法
3 + 3 = 6
文件处理
Go语言可以读写标准格式(如 XML 和 JSON 格式)的文件以及自定义的纯文本和二进制格式文件。现在我们可以灵活地使用 Go语言提供的所有工具,并利用闭包来避免重复性的代码,同时在某些情况下充分利用 Go语言对面向对象的支持,特别是对为函数添加方法的支持。
文件是保存数据的地方,是数据源的一种,比如txt文件、word、Excel、jpg等都是文件。文件最主要的作用就是保存数据。
其中Go语言内置的OS包下的File结构体封装了对文件的操作
Go语言中文标准库:
Go语言标准库
OpenFile文件打开模式Constants
O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
O_RDWR int = syscall.O_RDWR // 读写模式打开文件
O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件
O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用,文件必须不存在
O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O
O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件
权限控制(linux/unix系统生效,windows下设置无效,windows放入0666即可)
文本文件
对文本文件操作,离不开IO流,IO流是程序和数据源之间沟通的桥梁,可以比喻为程序和数据源之间的一条水管,一点一点的流过去
写纯文本文件
由于Go语言的 fmt 包中打印函数强大而灵活,写纯文本数据非常简单直接,示例代码如下所示:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
//创建一个新文件,写入内容
filePath := "./output.txt"
// 更改OpenFile参数可以调整只读、只写、读写、追加模式
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("打开文件错误= %v \n", err)
return
}
//及时关闭
defer file.Close()
//写入内容
str := "你好,我是玄德\n" // \n\r表示换行 txt文件要看到换行效果要用 \r\n
//写入时,使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 3; i++ {
writer.WriteString(str)
}
//因为 writer 是带缓存的,因此在调用 WriterString 方法时,内容是先写入缓存的
//所以要调用 flush方法,将缓存的数据真正写入到文件中。
writer.Flush()
}
读纯文本文件
打开并读取一个纯文本格式的数据跟写入纯文本格式数据一样简单。要解析文本来重建原始数据可能稍微复杂,这需根据格式的复杂性而定。
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
//打开文件
file, err := os.Open("./output.txt")
if err != nil {
fmt.Println("文件打开失败 = ", err)
}
//及时关闭 file 句柄,否则会有内存泄漏
defer file.Close()
//创建一个 *Reader , 是带缓冲的,缓冲区:4096字节
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n') //读到一个换行就结束
if err == io.EOF { //io.EOF 表示文件的末尾
break
}
fmt.Print(str)
}
fmt.Println("文件读取结束...")
}
复制文件
package main
import (
"fmt"
"io/ioutil"
)
func main() {
file1Path := "./output.txt"
file2Path := "./output2.txt"
data, err := ioutil.ReadFile(file1Path)
if err != nil {
fmt.Printf("文件打开失败=%v\n", err)
return
}
err = ioutil.WriteFile(file2Path, data, 0666)
if err != nil {
fmt.Printf("文件打开失败=%v\n", err)
}
}
写在最后
我们都是站在巨人的肩膀上,感谢所有愿意分享知识的人
参考名单:
- 感谢狂神的Go语言基础:
狂神b站地址 - 感谢C语言中文网严站长:
C语言中文网 - 感谢马士兵教育珊珊老师:
b站珊珊老师 - 感谢马士兵教育申专老师:
b站申专老师 - 感谢ChatGPT语言模型:
ChatGPT官网
颜文字:
<( ̄︶ ̄)> <( ̄︶ ̄)/ \( ̄︶ ̄)/ ╰( ̄▽ ̄)╭ (╯-_-)╯╧╧
╮( ̄▽ ̄)╭ (~ ̄▽ ̄~)╮( ̄▽ ̄")╭ (  ̄^ ̄)︵θ︵θ︵θ︵θ︵☆(>口<-)