跟着尚硅谷B站视频记的笔记
入门
-
go 编译和运行源代码
- go build 编译源代码,生成可执行文件
- go build -o
newName.exe
name.go
- go build -o
- go run 直接编译运行代码
- go build 编译源代码,生成可执行文件
-
godoc
-
gofmt 格式化代码
1 遇到的问题
1.1 安装环境问题
换成国内源
go env -w GOPROXY=https://goproxy.cn,direct
1.2 报错
package XXX is not in GOROOT (X:\XXX\Go\src\XXX)
解决方法:go env -w GO111MODULE=off
2 语法
2.1 语法注意事项
- go 语言定义的变量或者 import 的包如果没有使用,代码不能编译通过。
2.2 基本语法
2.2.1 声明变量
var 变量名 变量类型
// 通常在函数内使用
s := ""
// 声明后若不赋值,则使用默认值
var s string
// 建议使用前两种形式
var s = ""
var s string = ""
有多个变量需要声明时,可以使用:
var (
n1 = 100
n2 = 200
)
注意事项
- 在函数外部定义的变量是全局变量。
- 变量在同一个作用域内不可重名。
- 在对字符串(string类型)的数据进行加法时,直接拼接。
3 数据类型
uint 的 u 表示 无符号。
3.1 基本数据类型
整型
类型 | 有无符号 | 占用空间 | 备注 |
---|---|---|---|
int |
有 | 32位系统:4 Bytes 64位系统:8 Bytes |
|
int8 |
1 Byte | 不可以加溢出 int8 范围的整数,即大于会等于 128 的数字 |
|
int16 |
2 Bytes | ||
int32 |
4 Bytes | ||
int64 |
8 Bytes | ||
uint |
无 | 同 int |
|
rune |
有 | 同 int32 4 Bytes |
与 int32 等价,表示一个 Unicode 码 |
byte |
无 | 同 uint8 1 Bytes |
储存单个字符时使用 |
- 查看某个变量占用的字节数:
unsafe.Sizeof(variable)
浮点型
类型 | 占用空间 |
---|---|
float32 (单精度) |
4 Bytes |
float64 (双精度) |
8 Bytes |
-
浮点数都是有符号的。
-
位数部分可能丢失,造成精度损失。如:
var num1 float32 = -123.0000901 var num2 float64 = -123.0000901 // num1 和 num2 实际的值分别为 // -123.000090 和 -123.0000901
-
浮点型默认声明为
float64
类型。 -
浮点型还可表示为科学计数法形式,使用
E
或e
,如num := 1.23E2
。
没有专门类型的字符型
-
使用
byte
来储存。 -
Go 的字符串是由字节组成的。
-
如果保存的字符的对应码值大于 255,可以使用
int
型来储存。
-
如果需要以安装字符的方式输出,使用格式化输出(如:
fmt.Printf("%c", \'a\')
) -
允许使用转义字符,如:
fmt.Printf("%c好的", \'\t\')
。 -
可以进行运算。
布尔型
true
和 false
。占用空间为 1 Byte。
字符串型
- Go 中,字符串是不可变的;所以字符串一旦赋值,就不能修改。
- 可以使用双引号(
""
)和反引号(``)- 双引号:会识别转义字符;
- 反引号:以字符串的原有形式输出,包括转义字符;可实现防止攻击、输出源代码等功能。
- 字符串的拼接:
str1 + str2
- 如果需要多行,
+
需要保留在上一行。
- 如果需要多行,
3.2 各数据类型的默认值
数据类型(基本) | 默认值 |
---|---|
整型 | 0 |
浮点型 | 0 |
字符串型 | "" |
布尔型 | false |
数据类型(引用) | 默认值 |
---|---|
数组 | 和其元素类型有关 |
指针 | nil |
slice | nil |
map | nil |
- nil 即还没有分配空间。
3.3 数据类型转换
由于 Go 中的数据类型不能自动转换,所以在不同类型的变量之间赋值时需要显式转换。
示例:newType(variable)
,把 变量 variable
转换为 newType
类型。
var i float32 = 10.01
n1 := int(i)
fmt.Println(n1)
// 输出为:10
3.3.1 基本数据类型和 string 型的转换
基本数据 → string
-
[方式1]
fmt.Sprint("%参数", 表达式)
:Sprint根据format参数生成格式化的字符串并返回该字符串。-
参数需与表达式的数据类型相匹配。
-
例:
var num1 int = 99 var num2 float64 = 23.456 var b bool = true var myChar byte = \'h\' var str string str = fmt.Sprintf("%d", num1) fmt.Printf("str type %T str=%q\n", str, str) str = fmt.Sprintf("%f", num2) fmt.Printf("str type %T str=%q\n", str, str) str = fmt.Sprintf("%t", b) fmt.Printf("str type %T str=%q\n", str, str) str = fmt.Sprintf("%c", myChar) fmt.Printf("str type %T str=%q\n", str, str) // 输出为 // str type string str="99" // str type string str="23.456000" // str type string str="true" // str type string str="h"
-
-
[方式2] 使用 strconv 包的函数
func FormatBool(b bool) string func FormatFloat(f float64, fmt byte, prec, bitSize int) string // str = strconv.FormatFloat(num, \'f\', 10, 64) // 表示格式为\'f\',保留位数为10,小数类型是float64类型。 func FormatInt(i int64, base int) string // str = strconv.FormatInt(num, 10) func FormatUint(i uint64, base int) string
string → 基本数据类型
使用 strconv 包的函数
func ParseBool(str string)(value bool, err error)
// 返回两个值(value和err)
func ParseFloat(s string, bitSize int)(f float64, err error)
// 返回的数值是float64型的数值
func ParseInt(s string, base int, bitSize int)(i int64, err error)
// int64
func ParseUint(s string, b int, bitSize int)(n uint64, err error)
// uint64
例:
var str string = "true"
var b bool
b, _ = strconv.ParseBool(str)
// b = true
var str2 string = "1234590"
var n1 int64
n1, _ = strconv.ParseInt(str2, 10, 64)
// n1 = 12345
var str3 string = "123.456"
var f1 float64
f1, _ = strconv.ParseFloat(str3, 64)
// f1 = 123.456
-
如果需要其他类型的,得到结果之后进行显示转换即可。
-
应确保 string 型的数据能转换成有效的数据。
在不能转换为有效的数据时,得到的数据是相应数据类型的默认值。
- 如将
"hello"
转为整数时,得到0
。
- 如将
3.4 派生数据类型
指针 Pointer
-
获取变量的地址:
&v
-
定义指针类型的变量:
var ptr *type = &v
-
获取指针指向变量的值:
*ptr
-
值类型都有对应的指针类型,形式为
*type
- 值类型包括:int系列、float系列、bool、string、数组和结构体。
3.5 自定义数据类型
-
语法
type typeName 数据类型 // 相当于一个别名 // 如: type myInt int // type mySum func(int, int) int
3.6 值类型和引用类型
-
范围
- 值类型:int系列、float系列、bool、string、数组和结构体。
- 引用类型:指针、切片slice、map、管道chan、interface等。
-
使用特点
- 值类型:变量直接存储值,内存通常在栈中分配。
- 引用类型:变量储存的是一个地址,内存通常在堆上分配;当没有任何变量引用该地址时,该地址对应的数据空间成为一个垃圾,由GC来回收。
-
栈和堆的示意图
4 标识符
4.1 空标识符( _
)
空标识符可以表示其他的标识符,但是其对应的值会被忽略(可用于忽略函数的某个返回值)。
仅能作为占位符使用,不能作为标识符使用。
4.2 保留关键字(25个)
不能使用保留关键字作为标识符(共25个)
4.3 预定义标识符(36个)
4.4 标识符命名的注意事项
-
包名:保持 package 的名字和目录保持一致,简短但有意义,不要和标准库冲突(如 fmt )。
-
变量名、函数名、变量名:使用驼峰命名法。
-
若变量名、函数名、变量名的首字母大写,则可以被其他的包访问(公开的);
若小写,则只能在当前包中使用(私有的)。
(Go 中没有 public 和 private 关键字)
-
5 运算符
5.1 位运算符
运算符 | 功能 | 运算规则 |
---|---|---|
& |
参与运算的两数个对应的二进位相与 | 只有同时为1时,结果为1 |
| |
参与运算的两数个对应的二进位相或 | 只有两个都为0时,结果才为0 |
^ |
参与运算的两数个对应的二进位相异或 | 二进位不同时为1,否则为0 |
<< |
把左边的运算数各二进制位全部左移若干位 | 高位丢弃,低位补0; 左移 n 位即乘 \(2^n\) |
>> |
把左边的运算数各二进制位全部右移若干位 | 左补 n 个符号位, 右移 n 位即除以 \(2^n\) |
5.2 运算符的优先级
5.3 注意事项
-
除号
/
:在整数之间做除法运算时,结果只保留整数部分。 -
自增、自减运算符
++
--
:-
只能作为独立的表达式使用。
以下是错误例:a = i++ // 错误
-
只能写在变量的后面。
-
-
Go 语言不支持三目运算符。
6 控制台输入
6.1 方法
-
使用 fmt 包中的
Scanln
(换行时结束扫描) ,Scanf
函数例:
var name string fmt.Scanln(&name) fmt.Scanf("%s", &name)
7 程序结构
顺序、分支、循环
7.1 分支 if
switch
-
if 语句
if expression { statement } else if expression { statement } else { statement }
-
switch 语句
switch expression { case exp1, exp2, ... : statement case exp3, exp4, ... : statement default: statement }
-
switch 的 case 中,不用再加 break 。
-
switch 和 case 后是一个表达式(可以是常量、变量、带有返回值的函数等)。
- switch 后的表达式可以为空,此时可以类似 if-else 语句形式。
-
switch 穿透:可以在 case 中的 statement 末尾加
fallthrough
,执行下一个 case 中的语句。 -
Type Switch:switch 语句可以用于 type-switch ,用以判断某个 interface 变量中实际指向的变量类型。
例:
var x interface{} // var y = 10.0 // x = y switch i := x.(type) { case nil: fmt.Print("x的类型是: %T", i) case int: fmt.Print("x: int") case float64: fmt.Print("x: float64") case func(int) float64: fmt.Print("x: func(int)") default: fmt.Print("unknown") }
-
-
if 语句和 switch 语句的比较
- switch 语句:判断的具体数值不多,且符合整型、浮点型、字符型、字符串型,建议使用。
- if 语句:其他情况,如区间判断,结果为 bool 型的判断。if 语句的适用范围更广。
7.2 循环 for
7.2.1 for 和 for-range
-
for 语句
for 循环变量初始化; 循环控制条件; 循环变量迭代 { statement } // 类似while循环的形式 for { statement }
-
for-range 语句
-
可用于遍历字符串和数组。
例:
var str string = "Hello World!" for index, val := range str { statement }
-
-
两种语句的区别
-
for 语句按照字节对字符串进行遍历,如果字符串含有中文,可能会出现乱码的情况。解决方法:将
str
转成[]rune
切片。而 for-range 语句按照字符进行遍历,没有上面的问题。
-
7.2.2 break continue 和 goto return 语句
-
带标签的 break 语句
break 出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块。
例:
label1: for i := 0; i < 4; i++ { for j := 1; j < 10; j++ { break label1 } fmt.Println("j = ", j) }
-
带标签的 continue 语句
结束本次循环,进入指定的下一次循环。
-
goto 语句(不建议使用):跳转控制语句,跳转至指定标签
语法:
goto label // 执行到此行时跳转至label所在行,继续执行程序 statement label: statement
-
return 语句:用于跳出所在的方法或函数。
- 如果在 main 函数中,表示终止 main 函数,即终止程序。
- 如果在其他函数中,表示跳出该函数。
8 函数、包和错误处理
8.1 包
-
含义:包的本质实际上就是创建不同的文件夹,用以存放程序文件。
-
作用:
- Go 语言以包的形式来管理文件和项目目录结构。
- 三大作用:
- 区分相同名字的函数、变量等标识符。
- 当程序文件很多时,可以很好地管理项目。
- 控制函数、变量等访问范围,即作用域。
-
打包的方法:
package pkgName
-
引入包的基本用法:
import "包的路径"
引入多个包:
import ( "pkg1" "pkg2" )
路径从 $GOPATH 的 src 下开始(不用带 src)。
-
注意事项
-
文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
-
使用别的包的函数或变量,需要先引入相应的包。
-
package
在前,import
在后。 -
跨包访问:需要调用的函数首字母大写。
-
取别名:可以给包取别名。但是取别名后,原来的包名就不能使用。(包名较长时可以使用)
例:
package main import ( "fmt" util "go_packages/util" )
-
同一个包下,不能有相同的函数名和相同的全局变量名。否则会有重复定义的报错。
-
如果需要编译成可执行程序文件,需要将该包声明为
package main
。写库则名可以自定义。
-
8.2 函数
语法:
func funcName (args) (返回值类型列表) {
// statement
return 返回值列表
}
8.2.1 init
函数
每个源文件都可以包含一个 init
函数,该函数会在执行 main
函数之前被 Go 的运行框架调用。
主要完成初始化工作。
-
注意事项
-
如果一个文件同时包含全局变量定义,
init
函数和main
函数。则执行的顺序为:全局变量定义 →
init
→main
。
-
8.2.2 匿名函数
只希望使用一次时考虑使用。匿名函数也可以实现多次调用。
8.2.2.1 使用方法
-
使用方法 1:定义时。该匿名函数只能调用一次。
res1 := func (n1 int, n2 int) int { return n1 + n2 }(10, 20)
-
使用方法 2:将匿名函数赋给一个变量。可通过多次调用变量来实现多次调用匿名函数。
a := func (n1 int, n2 int) int { return n1 + n2 } res2 := a(10, 20)
8.2.2.2 全局匿名函数
即将匿名函数赋给一个全局变量。
8.2.3 return
语句
Go 预言的函数支持返回多个值。
-
在函数返回多个值时,如果不需要某个值,可用
_
代替。 -
返回值只有一个时,
返回值类型列表
处可以不写()
。 -
可以对返回值进行命名。
例:
func getSumAndSub(n1 int, n2 int) (sum int, sub int) { sub = n1 - n2 sum = n1 + n2 return }
8.2.4 注意事项
-
形参列表和返回值列表的数据类型可以是值类型和引用类型。
-
函数名首字母为大写,则可被别的包的调用。
-
基本数据类型和数组默认都是值传递的,即进行值拷贝,不会影响原来的值。
如果希望调用的函数修改原有的值,则可进行引用传递。
-
Go 不支持函数重载。
-
函数也是一种数据类型,可赋值给一个变量。通过该变量可以对函数进行调用。
例:
因此函数也可作为形参。
例:
func myFunc(funvar func(int, int) int, num1 int, num2 int) int { return funvar(num1, num2) }
-
Go 支持可变参数。
func sum1(args... int) sum int { // statement } func sum2(n1 int, args... int) sum int { // statement }
args
是 slice 切片,通过args[index]
可以访问各个值。- 形参列表中有可变参数,需要放在形参列表的最后。
8.2.5 闭包
-
含义:闭包是一个函数和与其相关的引用环境组合的一个整体(实体)。
例:
-
说明:
- 返回的是一个匿名函数。该匿名函数引用了其外部的函数的变量(
suffix
)。因此该匿名函数和suffix
形成一个整体,构成闭包。 - 反复调用
f2
时,suffix
仅初始化一次。
- 返回的是一个匿名函数。该匿名函数引用了其外部的函数的变量(
8.2.6 函数的延时机制 defer
-
作用:在函数执行完毕后,及时释放资源。
-
使用:
-
当执行到一个
defer
时,不会立即执行其后的语句,而是将其后的语句压入的一个独立的栈中,然后继续执行下一个语句。 -
当函数执行完毕后,会在该独立的栈中,从栈顶去除语句执行。(遵守栈 先入则后出的机制)。
-
入栈时,会将相关的值拷贝,同时入栈。
例:
n1 := 1 defer fmt.Println(n1) // 输出为 1 n1++
-
-
流程:
- 通常的做法是,在创建资源后(如打开了文件,获取了数据库的连接,或者是锁资源),可以执行
defer file.close()
,defer connect.close()
等。 - 在
defer
后,可以继续使用创建的资源。 - 当函数执行完毕后,系统会依次从独立栈中取出语句,关闭资源。
- 通常的做法是,在创建资源后(如打开了文件,获取了数据库的连接,或者是锁资源),可以执行
8.2.7 函数的作用域
赋值语句不能在函数体外。
8.2.8 常用函数
8.2.8.1 字符串常用函数
-
字符串的长度(按字节)
len(str)
-
字符串遍历(处理有中文的问题)
r := []rune(str)
-
字符串转整数
n, err = strconv.Atoi("12")
整数转字符串
str = strconv.Itoa(12)
-
字符串转
[]byte
:var bytes = []byte("Hello World")
[]byte
转字符串:str = string([]byte{97, 98, 99})
-
10进制转2、8、16进制:
str = strconv.FomatInt(num, base)
-
查找子字符串是否在指定的字符串中:
strings.Contains("seafood", "food") // true
-
统计一个字符串有几个指定的字符串:
strings.Count("what\'s your name?", "a") // 2
-
字符串比较(不区分大小写)
strings.EqualFold("abc", "Abc")
-
返回字符串第一次出现的 index 值,如果没有则返回 -1
strings.Index("efg_abc", "abc") // 4
返回字符串最后一次出现的 index 值,如果没有则返回 -1
strings.LastIndex("go golang", "go") // 3
-
将指定的子字符串替换为另一个子串
strings.Replace("go golang", "go", "go language", n) // n表示替换几个,若为-1则全部替换
-
拆分成字符串数组(以某个指定的字符为分隔标识)
strings.Split("How are you?", " ")
-
大小写转换
strings.ToLower("GO")
strings.ToUpper("go")
-
去掉指定的字符:
去掉左右的空白符
strings.TrimSpace
去掉左右两边指定的字符
string.Trim("!g!o!", "!") // "g!o"
去掉左边或右边的指定字符
string.TrimLeft("!g!o!", "!")
strings.TrimRight("!g!o!", "!")
-
判断字符串是否以指定的字符串开头
strings.HasPrefix("http://", "http")
判断字符串是否以指定的字符串结尾
strings.HasSuffix("winter.jpg", ".jpg")
8.2.8.2 时间、日期相关的函数
导入 time 包。
-
time.Time
类型,用于表示时间。now := time.Now() // 获取当前时间
-
通过
now
可以获取年月日、时分秒。now.Year() now.Hour()
-
格式化时间。
[方法 1] Printf 或 SPrintf。
[方法 2] time.Format() 方法。其中,"2006-01-02 15:04:05" 的数字固定的。可以根据需要自由组合。
now.Format("2006-01-02 15:04:05")
-
时间的常量
const ( Nanosecond = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond Minute = 60 * Second Hour = 60 * Minute )
-
time 的 Unix 和 UnixNano 方法
8.2.8.3 内置函数
- new:用于分配内存,主要用来分配值类型,如 int, float32, struct 返回的是指针。
- make:用于分配内存,主要用来分配引用类型,如 channel, map, slice。
8.3 错误处理
处理方式 defer, panic, recover。Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。
8.3.1 自定义错误
使用 errors.New
和 panic
内置函数。
errors.New("错误说明")
,返回一个 error 类型的值,表示一个错误。- panic 内置函数,接受一个 interface{} 类型的值(即任何值)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序。
9 数组与切片
- 在 Go 中,数组是值类型,切片是引用类型。
9.1 数组
9.1.1 数组的基本使用
-
数组定义:
var arrayName [size]int
-
数组的内存布局:
-
数组首元素的地址就是数组的首地址。
-
数组各元素之间的地址间隔根据数组的类型决定,如 int64 是 8,int32 是 4。
-
-
数组的遍历:
[方式 1] 常规遍历
[方式 2] for-range 遍历
for index, value := range array01 { // statement }
- 如果不需要下标,可用
_
代替。
- 如果不需要下标,可用
-
注意事项:
- 数组的长度一旦声明或定义,不可动态变化。
- 数组创建后,如果没有赋值,有默认值。默认值同上。
- 在进行函数传参时,需要考虑数组的长度。
9.2.2 多维数组
-
定义语法:
var 数组名 [大小][大小]类型 // 如:var arr [2][3]int
-
使用方法:
-
先声明/定义,再赋值。
-
直接初始化。
var arr3 [2][3]int = [2][3]int{{1, 2, 3}, {4, 5, 6}}
-
-
遍历方法:
- 两层 for 循环。
- 两层 for-range 循环。
9.2 切片 slice
9.2.1 切片的基本使用
-
切片是数组的一个引用,因此切片是引用类型。
-
使用和数组类似。
-
切片的长度可以变化,即可认为切片是一个可以动态变化的数组
-
语法:
var 切片名 []类型 // 如 var a []int
-
内存布局:
-
slice 从底层来看,是一个数据结构(struct 结构体)
type slice struct { ptr *[2]int len int cap int }
-
-
切片的使用:
-
[方式 1]:定义一个切片,之后让切片引用一个已经创建的数组。
var arr [5]int = [...]int {1, 2, 3, 4, 5} var slice = arr[1:3]
-
[方式 2]:语法如下,
var 切片名 []type = make([]type, len, [cap])
-
type 数据类型,len 大小,cap 切片容量。
-
cap 可选,如果选择 cap, 则 cap 必须 ≥ len。
-
例:
var slice []float64 = make([]float64, 5, 10)
-
-
-
切片的遍历
类似于数组。
-
增加元素:
append()
函数 -
切片的拷贝:
copy()
函数copy(slice1, slice2) // 将slice2的元素依次赋给slice1
9.2.2 String 与切片
- string 底层是一个 byte 数组,因此 string 也可进行切片处理。
- string 是不可变的。如果需要修改字符串:
- 可以先将 string 型转换成 []byte 型。
- []rune -> 修改 -> 重写转成 string。
10 map
map 是 key-value 数据结构,又称为字段或者关联数组。类似于其他编程语言的集合。
10.1 map 的基本使用
10.1.1 map 的声明
-
语法:
var map 变量名 map[keyType]valueType
-
key 不可为 slice, map 和 function,因为这几个无法用 == 来判断。
key 通常为 int 或者 string 型。
-
valueType 的类型和 key 的基本一样。
key 通常为数字(整数、浮点数),string,map,struct。
-
例(map 的声明):
var a map[string]string var b map[int]string
- 声明不会分配内存,需要 make 初始化后才可赋值和使用。
-
注意事项:
- map 的 key 不能重复。如果重复了,则以最后的为准。
- map 的 keyValue 是无序的。
-
10.1.2 map 的基本使用
-
[方式 1]
var a map[string]string a = make(map[string]string, 10) a["no1"] = "test1" a["no2"] = "test2"
-
[方式 2]
a := make(map[string]string) a["no1"] = "test1" a["no2"] = "test2"
-
[方式 3]
a := map[string]string { "no1" : "test1" "no2" : "test2" }
10.1.3 map 的增删改查 和 遍历
-
增加和更新
mapName["key"] = value // key还不存在就是增加,存在则是更新
-
删除
delete(mapName, "key") // 如果key存在则删除该keyValue,否则不操作(不会报错)
删除所有 key 的方法:① 遍历; ② 直接 make() 一个新的,原来的key成为垃圾,被 go 回收。
-
查找
例:
val, exist := a["no1"] if exist { // statement } else { // statement }
-
遍历
使用 for-range 的结构遍历。
10.1.4 map 切片、排序
-
切片
如果切片的数据类型是 map,则称其为 map 切片。这样,map 的个数就可以动态变化了。
-
例:
// 声明 var monsters []map[string]string monsters = make([]map[string]string, 2) // ...... newMonster := map[string]string { "name" : "unknown" "age" : "200" } monsters = append(monsters, newMonster)
-
-
排序
go 中的 map 默认是无序的,也不按照添加的顺序存放(因此每一次遍历得到的结果顺序可能不一样)。
可以先对 key 排序,然后根据 key 值遍历输出。
10.1.5 注意事项
- [动态] map 的容量达到后,再增加元素,会自动扩容(不会发生 panic)。
- map 的 value 经常使用 struct 类型,更适合管理复杂的数据(优于 value 类型是 map 的情况)。
11 结构体 (面向对象)
-
结构体是值类型。不同的结构体变量的字段相互独立。
-
Go 基于结构体 struct 实现面向对象。
-
通过 匿名字段 实现继承。
11.1 结构体的基本使用
11.1.1 声明和创建结构体变量
-
声明语法:
type 结构体名称 struct { field1 type field2 type }
-
字段(属性, field),方法。
-
创建结构体变量
- [方式 1] 直接声明:
var person Person
。 - [方式 2] {}:
var person Person = Person{}
。 - [方式 3 返回指针] &:
var person *Person = new(Person)
。 - [方式 4 返回指针] {}:
var person *person = &Person
- [方式 1] 直接声明:
-
通过指针访问结构体字段的方法:
(*person).Name = "Tom"
或person.Name
。 -
内存分配机制:
- 结构体的所有字段在内存中是连续的。
11.1.2 字段(属性, field)
-
在创建结构体变量之后,字段会被赋默认值。
-
和其他类型进行转换时需要有完全相同的字段(名字、个数和类型)。
-
对结构体进行 type 重新定义相当于取别名,但 Go 认为取别名后的是新的数据类型。相互间可以通过通过显式转换。
例:
type Student struct { Num int } type Stu Student
-
struct 的每个字段上,可以写一个 tag,该 tag 可以 通过反射机制获取。常见的使用场景为序列化和反序列化。
例:
type Student struct { Name string `json:"name"` Age int `json:"age"` } // 1 创建一个Student变量 stu1 := Student{"stu1", 20} // 2 将Student变量序列化为json格式字符串 jsonStr, err := json.Marshal(stu1)
11.1.3 方法
-
声明的语法:
type A struct { Num int } func (a A) test() { // statement fmt.Prinln(a.Num) }
(a A)
体现 test 方法和 A 类型绑定。
func (receiver type) methodName (参数列表) (返回值列表) { // 函数体 return 返回值 }
-
receiver
和type
进行绑定。receiver
是type
类型的一个变量(实例)type
可以是结构体,也可以是其他自定义类型。 -
return 语句不是必须的。
-
方法的调用和传递机制
-
变量调用方法时,该变量本身也会作为一个参数传递到方法中。
如果变量是值类型,则进行值拷贝;如果是引用类型,则进行地址拷贝。
-
-
注意事项
- 如果想要在方法中修改结构体变量的值,可以通过结构体指针的方式处理。
- Go 中的方法作用在指定的数据类型上(即和指定的数据类型绑定),因此自定义类型都可以有方法。
- 注意:方法的首字母大写则可在包外访问,否则只能在包内访问。
- 如果一个类型有
String()
方法,那么fmt.Println()
方法会默认调用该变量的String()
方法进行输出。
11.1.4 函数和方法的区别
-
调用方式不同。
-
对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法,反之亦然。
11.2 工厂模式
即定义一个函数,起到和构造函数类似的作用。(Go 没有构造函数)
如对于 student 结构体,有
func NewStudent(n string, a int) *student {
return *student{
name: n,
age: a,
}
}
之后,就可以使用该函数在别的包下创建该结构体的实例(先导入)。
- 该函数返回的是指向该结构体实例的指针。
12 面向对象
12.1 封装、继承、多态
12.1.1 封装
[首字母小写]
将结构体、字段的首字母小写。
[首字母大写]
提供一个工厂模式的函数。
提供 Set 和 Get 方法。
12.1.2 继承
继承
在 Go 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
例:
type Goods struct {
Name String
Price int
}
type Book struct {
Goods // 嵌套匿名结构体 Goods
Writer string
}
-
注意事项:
-
封装并创建实例 book 后,以上面的代码为例,可以通过
book.Goods.Name
和book.Name
访问 Name 字段。 -
当结构体和匿名结构体有相同的字段或方法时,编译器遵从就近访问原则。若需要匿名函数体重名的字段和方法,可以通过如
book.Goods.Name
的方式实现。 -
结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身 没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
-
组合:一个结构体嵌套了一个有名结构体。
-
在访问组合的结构体的字段或方法时,必须带上有名结构体的名字。
type D struct { a A } var d D d.a.Name = "abc"
-
-
嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。
-
多重继承
如果 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套匿名结构体的字段和方法。
尽量不使用多重继承。
12.1.3 多态
12.1.3.1 接口 interface
-
语法
type 接口名 interface { method1(参数列表) 返回值列表 method2(参数列表) 返回值列表 ... } // 实现接口的所有方法 func (t 自定义类型) method1(参数列表) 返回值列表 { // 方法实现 } func (t 自定义类型) method2(参数列表) 返回值列表 { // 方法实现 }
-
说明:
- 接口中,所有的方法都没有方法体。即接口的方法都是没有实现的方法。
- Go 中的接口,不需要显示的实现。一个变量,若含有接口类型中的所有方法,那么这个变量就是实现这个接口。(Go 中没有 implement 关键字)
-
注意事项
- 接口本身不能创建实例,但是可以指向一个实现了改接口的自定义类型的变量(即实例)。
- 一个自定义类型需要将某个接口的所有方法都实现,则认为这个自定义类型实现了该接口。
- 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。
- 只要是自定义数据类型就可以实现接口。
- 一个自定义类型可以实现多个接口。
- 接口中不能含有任何变量。
- 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法全部实现。
- interface 类型默认是一个指针,若未经初始化使用,则会输出
nil
。 - 空接口没有实现任何方法,因此所有类型都实现了空接口。可以把任一变量赋给空接口。
12.1.3.2 多态
多态特征通过接口实现。
-
接口体现多台的两种形式
-
多态参数
即可以接收 A 类型的变量,又可以接收 B 类型的变量。
-
多态数组
-
12.2 类型断言
将接口转为具体类型时,需要使用类型断言,如下:
var x interface {}
var b2 = float32 =1.1
x = b2 // 看空接口可以接收任何类型
// 使用类型断言
y := x.(float32)
-
说明:
在进行类型断言时,如果类型不匹配,就会报 panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型。
-
带检测的类型断言
var x interface {} var b2 = float32 =1.1 x = b2 // 看空接口可以接收任何类型 // 使用类型断言(带检测的) if y, ok := x.(float32); ok { // convert succeed } else { // convert fail }
13 文件操作
13.1 文件的读写
os.File 封装所有文件相关操作,File 是一个结构体
-
打开和关闭文件
func Open(name string) (file *File, err error) func (f *File) Close() error
-
应用实例
-
读取文件的内容并显示在终端(带缓冲区的方式),使用 os.Open, file.Close, bufio.NewReader(), reader.ReadString 函数和方法。
例:
func main() { file, err := os.Open("test.txt") if err != nil { fmt.Println("Open file error:", err) return } defer file.Close() reader := bufio.NewReader(file) for { str, err := reader.ReadString(\'\n\') if err == io.EOF { break } fmt.Println(str) } fmt.Println("文件读取结束") }
-
读取文件的内容并显示在终端(使用 ioutil 一次将整个文件读入到内存中),这种方式适用于文件不大的情况。相关方法和函数(ioutil.ReadFile)
-
os.OpenFile 函数
func OpenFile(name string, flag int, perm FileMode) (file *File, err error)
说明:os.OpenFile 是一个更一般性的文件打开函数,它会是使用指定的选项(如O_RDONLY 等)、指定的模式(如 0666 等)打开指定的文件。
-
如果操作成功,则返回的文件对象可用于 I/O;
-
如果出错,错误的底层类型是 PathError。
-
形参说明:
-
flag 指的是文件打开模式(可以组合)。
-
perm 指的是权限控制(linux / unix)。
r -> 4 w -> 2 x -> 1
-
-
因为 writer 是带缓存,因此在调用 WriterString 方法时,其实 内容是先写入到缓存的,所以需要调用 Flush 方法,将缓冲的数据真正写入到文件中, 否则文件中会没有数据。
-
-
编写一个程序,将一个文件的内容,写入到另外一个文件。
使用 ioutil.ReadFile / ioutil.WriteFile 完成。
filename := "C:/Users/Jackkee/Desktop/test.txt" content, err := ioutil.ReadFile(filename) if err != nil { fmt.Println("ReadFileError:", err) return } filename2 := "C:/Users/Jackkee/Desktop/newTest.txt" err = ioutil.WriteFile(filename2, content, 0666) if err != nil { fmt.Println("WriteFileError:", err) return }
-
-
判断文件是否存在
使用 os.Stat() 函数返回的错误值进行判断:
- 如果返回的错误为 nil, 则文件或文件夹存在。
- 如果错误类型使用 os.IsNotExist() 判断为 true,说明文件或文件夹不存在。
- 如果返回的错误为其他类型,则不确定是否存在。
func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err }
13.2 命令行参数
-
os.Args 是一个 string 的切片,用于存储所有的命令行参数。
-
使用 flag 包来解析命令行参数。
例:
func main() { var user string var pwd string var host string var port int // &user 就是接受用户命令行中输入的-u后的参数值。 // "u" 就是 -u 制定参数 // "" 默认值 // "用户名,默认为空" 说明 flag.StringVar(&user, "u", "", "用户名,默认为空") flag.StringVar(&pwd, "ped", "", "密码,默认为空") flag.StringVar(&host, "h", "localhost", "主机名,默认为localhost") flag.IntVar(&port, "port", 3306, "端口号,默认为3306") // 必须使用下面的方法,进行转换 flag.Parse() fmt.Printf("user=%s, pwd=%s, host=%s, port=%d", user, pwd, host, port) }
13.3 json
{"key1":val1, "key2":val2}
- 检验 json 格式是否正确的网站:JSON在线解析及格式化验证 - JSON.cn
13.3.1 json 的序列化
指将有 key-value 结构的数据类型(结构体、map、切片等)序列化成 json 字符串。
-
序列化结构体
type Student struct { // 给字段添加tags Name string `json:"name,omitempty"` Age int `json:"age,omitempty"` Habit string `json:"habit,omitempty"` } func struct2Json() { stu := Student{ Name: "test", Age: 10, Habit: "singing", } data, err := json.Marshal(stu) if err != nil { fmt.Println("Error: ", err) } fmt.Println(string(data)) }
- 对结构体进行序列化时,字段名首字母应大写。
-
序列化 map
func map2Json() { var testMap map[string]interface{} testMap = make(map[string]interface{}) testMap["name"] = "testMap1" testMap["age"] = 18 data, err := json.Marshal(testMap) if err != nil { fmt.Println("序列化错误:", err) } fmt.Println(string(data)) }
-
序列化 slice
func slice2Json() { var slice2 []map[int]interface{} var map2 map[int]interface{} map2 = make(map[int]interface{}) map2[0] = "i\'m sorry" map2[1] = "the old taylor can\'t come to the phone right now" slice2 = append(slice2, map2) data, err := json.Marshal(slice2) if err != nil { fmt.Println(err) } fmt.Println(string(data)) }
-
序列化基本数据类型
func float64ToJson() { // 对基本数据类型进行序列化意义不大 var num1 = 123.456 data, err := json.Marshal(num1) if err != nil { fmt.Println(err) } fmt.Println(string(data)) }
13.3.2 json 的反序列化
指将 json 字符串转换成对应的数据类型(如结构体、map、切片)。
-
json 反序列化为 struct
type Student struct { Name string Age int Height float64 } func json2Struct() { str := "{\"Name\":\"test\",\"Age\":18,\"Height\":180.0}" var stu Student // v interface{} // 如果 v 是 nil 或者 v 不是指针,该方法返回一个错误(InvalidUnmarshalError) // If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError. err := json.Unmarshal([]byte(str), &stu) if err != nil { fmt.Println(err) } fmt.Println(stu) }
-
Unmarshal 方法的第二个参数 v 如果是 nil 或者 v 不是指针,该方法返回一个错误(InvalidUnmarshalError)。
即 v 必须是指针类型。
-
-
json 反序列化为 map
func json2Map() { str := "{\"Name\":\"test\",\"Age\":18,\"Height\":180.0}" var testMap map[string]interface{} err := json.Unmarshal([]byte(str), &testMap) if err != nil { fmt.Println(err) } fmt.Println(testMap) }
- 反序列化 map 不需要 make 函数。因为 make 函数的操作已经被封装到 Unmarshal 方法中。
-
json 反序列化为 slice
func json2Slice() { str := "[{\"Name\":\"test\",\"Age\":18,\"Height\":180.0}]" var slice []map[string]interface{} err := json.Unmarshal([]byte(str), &slice) if err != nil { fmt.Println(err) } fmt.Println(slice) }
- 说明
- 在反序列化 json 字符串时,要哦确保反序列化后的数据类型和原来序列化之前的数据类型保持一致。
- 如果 json 字符串是通过程序得到的,则不需要再对
"
转移处理,即不用在"
前加\
。
14 单元测试
Go 中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试。
可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。
-
说明
-
测试用例的文件名必须以 _test.go 结尾。
-
测试用例的函数必须以 Test 开头。
-
一个测试样例中,可以有多个测试用例函数。
-
运行测试用例的指令:
go test // 如果运行正确,无日志;错误时会输出日志 go test -v // 正确或错误都会输出日志
-
相关的方法:
- 出现错误时,可以用
t.Fatalf
格式化输出错误信息,并退出程序。 t.Logf
方法可以输出相应的日志。
- 出现错误时,可以用
-
测试用例函数可以不在 main 函数中运行。
-
PASS 表示测试用例运行成功,FAIL 表示运行失败。
-
测试单个文件时,一定要带上被测试的原文件。
go test -v cal_test.go cal.go
-
测试单个方法:
go test -v -test.run TestAddUpper
-
15 goroutine 和 channel
15.1 并发和并行
-
多线程程序在单核上运行,就是并发。
-
多线程程序在多核上运行,就是并行。
-
各自的特点:
- 并发
- 多个任务作用在一个 cpu。
- 从微观的角度看,在一个时间点上,只有一个任务在执行。
- 并行
- 多个任务作用在多个 cpu。
- 从微观的角度看,在一个时间点上,有多个任务在同时执行。
- 并行的速度相对较快。
- 并发
15.2 Go 协程和 Go 主线程
-
Go 主线程:一个 Go 线程上,可以起多个协程。协程是轻量级的线程[编译器做优化]。
-
Go 协程的特点
- 有独立的栈空间。
- 共享程序堆空间。
- 调度由用户控制。
- 协程是轻量级的线程。
示意图如下:
实例程序:
func test() { for i := 0; i< 10; i++ { fmt.Println("test() Hello World" + strconv.Itoa(i)) time.Sleep(time.Second) } } func main() { // 开始协程 go test() for i := 0; i < 10; i++ { fmt.Println("main() Hello Golang" + strconv.Itoa(i)) time.Sleep(time.Second) } }
-
执行流程图
-
小结
- 主线程是一个物理线程,直接作用于 CPU。重量级,非常耗费 CPU 资源。
- 协程是从主线程开启的,是轻量级的线程。逻辑态,对资源耗费相对较小。
- (Go 在并发上的优势)Go 的协程可以开启上万个协程。
15.2.1 goroutine 的调度模型
15.2.1.1 MPG 模式
M -> 操作系统的主线程
P -> 协程执行需要的上下文
G -> 协程
-
运行状态 1:
-
运行状态 2:
15.2.1.2 [go 1.8 以前]设置 go 运行的 CPU 数
num := runtime.NumCPU
runtime.GOMAXPROCS(num)
- go 1.8 以后,默认使程序运行在多个核上,可以不用设置。
- go 1.8 以前需要设置,以获取更高的运行效率。
15.2.2 管道 channel
加锁 lock
-
不同 goroutine 之间的通讯方式
-
全局变量的互斥锁。
没有对全局变量 m 加锁,会出现资源争夺问题,提示 concurrent map writes。
-
使用 channel 解决。
-
-
使用全局变量加锁同步改进程序。
lock.Lock() // 加锁 mapTest[n] = ans lock.Unlock() // 解锁
-
缺点:
-
主线程等待所有 goroutine 全部完成的时间很难确定。
如果主线程休眠时间过长,会增加等待时间;
如果果断,可能还有 goroutine 处于工作状态,此时它们会随着主线程的退出而销毁。
-
使用全局变量加锁同步来实现通讯,不利于利用多个协程对全局变量的读写操作。
-
-
channel 的基本信息
-
本质是一个数据结构队列,示意图如下。
-
数据 先进先出(FIFO: First In First Out)
-
channel 本身是线程安全的:多 goroutine 访问时,无需加锁。
-
channel 有类型,一个 string 型的 channel 只能存放 string 型数据。
-
示意图:
定义与声明
var 变量名 chan 数据类型
// 如:var intChan chan int
- 说明:
- channel 是引用类型。
- channel 必须初始化才可写入数据,即 make() 后才可使用。
- channel 具有类型,只能写入指定数据类型。
初始化、写入数据至 channel、从 channel 读取数据及其注意事项
-
例:
var intChan chan int // 创建一个可以存放3个int型值的channel intChan = make(chan int, 3) fmt.Printf("intChan的值=%v, 本身的地址=%p\n", intChan, &intChan) // 向管道中写入数据,不可超过其容量(cap) intChan <- 10 num := 211 intChan <- num intChan <- 50 fmt.Printf("channel len=%v, cap=%v\n", len(intChan), cap(intChan)) // 读取 channel 中的数据(取出) var num2 int num2 = <-intChan fmt.Println("num2=", num2) fmt.Printf("channel len=%v, cap=%v\n", len(intChan), cap(intChan)) // 在没有使用协程的情况下,若管道中的数据已经全部取出,再取就会报告 deadlock num3 := <-intChan num4 := <-intChan num5 := <-intChan // fatal error: all goroutines are asleep - deadlock! fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
注意事项
-
channel 中只能存放指定的数据类型。
但将类型指定为
interface{}
,就可以实现任意数据类型的存放。 -
channel 的数据不能超过其最大容量。
但从 channel 中取出数据后,可以继续放入数据。
-
在没有使用协程的情况下,如果 channel 数据取完时再取,会报错(deadlock):
fatal error: all goroutines are asleep - deadlock!
-
从
interface{}
型的 channel 中取出数据时,需要进行类型断言。例:
var interfaceChan chan interface{} interfaceChan = make(chan interface{}, 3) cat1 := Cat{Name:"tom", Age:10,} cat2 := Cat{Name: "jerry", Age:10,} interfaceChan <- cat2 interfaceChan <- cat1 interfaceChan <- 10 // 取出 newCat := <- interfaceChan a := newCat.(Cat) // 类型断言。不可以直接newCat.Name fmt.Println("newCat.Name=",a.Name)
遍历和关闭
-
channel 的关闭
使用 close() 可以关闭 channel。
在关闭 channel 后,就不可再向 channel 写入数据。但仍然可以从中读取数据。
-
channel 的遍历
channel 支持以 for-range 方式进行遍历。
-
注意:
- 在遍历时,如果 channel 没有关闭,则会出现 deadlock 的错误。
- 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后就会推出遍历。
-
应用 1 (Page 442):
func writeData(intChan chan int) { for i := 1; i <= 50; i++ { intChan <- i } close(intChan) } func readData(intChan chan int, exitChan chan bool) { for { v, ok := <-intChan if !ok { break } fmt.Println("readData读取到数据", v) } exitChan <- true close(exitChan) } func main() { var intChan = make(chan int, 50) var exitChan = make(chan bool, 1) go writeData(intChan) go readData(intChan, exitChan) for { _, ok := <-exitChan if !ok { break } } }
-
应用 2 - 阻塞:
如果只是向管道写入数据,而没有读取(编译器运行发现一个管道只有写,没有读,则改管道会阻塞。与读、写管道的速度快慢无关)
-
应用 3 - 运用协程和 channel 判断素数
package main import "fmt" func putNum(intChan chan int) { for i := 1; i <= 100; i++ { intChan <- i } close(intChan) } func primeNum(intChan chan int, primeCh
-
请发表评论