学习视频:8 小时转职 Golang 工程师,这门课很适合有一定开发经验的小伙伴,强推!

参考博客链接:https://blog.csdn.net/weixin_43734095/category_11618348.html

学习路线:https://blog.csdn.net/weixin_45304503/article/details/127041166

【Golang 快速入门1】基础语法 + 面向对象

Golang 语言特性

Golang 的优势

极简单的部署方式:可直接编译成机器码、不依赖其他库、直接运行即可部署。

静态类型语言,编译的时候可以检查出大多数问题。

语言层面的并发:天生的基因支持、充分的利用多核

1
2
3
4
5
6
7
8
9
10
11
12
// Go 语言实现并发的代码
func goFunc(i int) {
fmt.Println("goroutine ", i, " ...")
}

func main() {
for i := 0; i < 1000; i++ {
go goFunc(i) // 开启一个并发协程
}
time.Sleep(time.Second)
}

强大的标准库:runtime 系统调度机制、高效的 CG 垃圾回收、丰富的标准库

“大厂” 领军:Google、facebook、Tencent、Baidu、七牛、字节 …

不同语言的斐波那契数列算法 编译 + 运行 时间对比:

Golang 的应用场景

1、云计算基础设施领域

代表项目:docker、kubernetes、etcd、consul、cloud flare CDN、七牛云存储 等。

2、基础后端软件

代表项目:tidb、influxdb、 cockroach 等。

3、微服务

代表项目:go-kit、 micro、 monzo bank 的 typhon、bilibili 等。

4、互联网基础设施

代表项目:以太坊、hyperledger 等。

Golang 明星作品:DockerKubernetes

Golang 的不足

1、包管理,大部分包都托管在 Github 上。

像我们熟悉的 maven、npm 等都有专门的包管理组织;

托管在 Github 上的代码容易被作者个人操作影响到使用该项目的工程。

2、无泛化类型。

据说很快就会有了。

3、所有 Exception 都用 Error 来处理(有争议)。

4、对 C 的降级处理,并非无缝,没有 C 降级到 asm 那么完美。(序列化问题)

Golang 基础语法

main主方法和注意点

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

// 导多个包
import (
"fmt"
"time"
)

// Golang 有无 ; 都可,建议不加
func main() { // 函数的 { 必须和函数名同行, 否则编译报错
fmt.Println("Hello Go!")
time.Sleep(1 * time.Second)
}

Go语言的格式化输出中%d%T%v%b等的含义

格式化指令含义
%%%字面量
%b一个二进制整数,将一个整数格式转化为二进制的表达方式
%c一个Unicode的字符
%d十进制整数
%o八进制整数
%x小写的十六进制数值
%X大写的十六进制数值
%U一个Unicode表示法表示的整型码值
%s输出以原生的UTF8字节表示的字符,如果console不支持utf8编码,则会乱码
%t以true或者false的方式输出布尔值
%v使用默认格式输出值,或者如果方法存在,则使用类性值的String()方法输出自定义值
%T输出值的类型
%+v结构体字段名称+值
%#v结构体的值

1、变量

输出变量类型的方法:

1
2
3
var a int
fmt.Printf("type of a = %T\n", a)
// type of a = int

局部变量的声明:

1
2
3
4
5
6
7
8
9
10
11
// 方法一:声明一个变量 默认的值是0
var a int

// 方法二:声明一个变量,初始化一个值
var b int = 100

// 方法三:初始化的时候, 省去数据类型,通过值自动匹配当前的变量的数据类型
var c = 100

// 方法四:(常用) 省去var关键字,直接自动匹配
d := 100

全局变量的声明:以上只有方法四不支持(编译会报错)

多变量的声明:

1
2
3
4
5
6
7
8
9
// 单行写法
var xx, yy int = 100, 200
var kk, ll = 100, "Aceld"

// 多行写法
var (
vv int = 100
jj bool = true
)

2、常量与 iota

使用 const 定义常量,常量是只读的,不允许修改。

1
2
3
4
5
6
const a int = 10

const (
a = 10
b = 20
)

const 可以用来定义枚举:

1
2
3
4
5
const (
BEIJING = 0
SHANGHAI = 1
SHENZHEN = 3
)

const 可以和 iota 一起使用来定义有规则的枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const (
// 可以在const() 添加一个关键字 iota, 每行的iota都会累加1, 第一行的iota的默认值是0
BEIJING = iota // iota = 0
SHANGHAI // i ota = 1
SHENZHEN // iota = 2
)

const (
a, b = iota+1, iota+2 // iota = 0, a = iota + 1, b = iota + 2, a = 1, b = 2
c, d // iota = 1, c = iota + 1, d = iota + 2, c = 2, d = 3
e, f // iota = 2, e = iota + 1, f = iota + 2, e = 3, f = 4

g, h = iota * 2, iota *3 // iota = 3, g = iota * 2, h = iota * 3, g = 6, h = 9
i, k // iota = 4, i = iota * 2, k = iota * 3 , i = 8, k = 12
)

3、string字符串

对于字符串操作的 4 个包:bytes、strings、strconv、unicode

  • bytes 包操作 []byte。因为字符串是只读的,因此逐步构创建字符串会导致很多分配和复制,使用 bytes.Buffer 类型会更高。
  • strings 包提供 切割、索引、前缀、查找、替换 等功能。
  • strconv 包提供 布尔型、整型数、浮点数 和对应字符串的相互转换,还提供了双引号转义相关的转换。
  • unicode 包提供了 IsDigit、IsLetter、IsUpper、IsLower 等类似功能,用于给字符分类。

如果 string 中包含汉字,要注意:

  • UTF-8 编码中,一个汉字需要 3 个字节,通过 len() 获取的是字符串占据的字节数

如果 string 中包含汉字,要注意:

  • UTF-8 编码中,一个汉字需要 3 个字节,通过 len() 获取的是字符串占据的字节数
1
2
str1 := "hello 世界"
fmt.Println(len(str1)) // 12
  • 如果想要得到字符串本身的长度,可以将 string 转为 rune 数组再计算:
1
2
str2 := "hello 世界"
fmt.Println(len([]rune(str2))) // 8

字符串遍历

byteuint8 的别名

runeint32 的别名,相当于 Go 里面的 char

如果包含汉字,以下遍历方式会出现乱码:

1
2
3
4
5
6
str := "你好世界!"

for i := 0; i < len(str); i++ {
fmt.Printf("%c", str[i])
}
// ä½ å¥½ä¸çï¼%
  • 解决方案1:转成 rune 切片再遍历
1
2
3
4
5
6
str := "你好世界!"
newStr := []rune(str)
for i := 0; i < len(newStr); i++ {
fmt.Printf("%c", newStr[i])
}
// 你好世界!
  • 解决方案2:使用 range 来遍历

range 按照字符遍历,前面的 for 按照字节遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
str := "你好世界123"
for index, value := range str {
fmt.Printf("index = %d value = %c\n", index, value)
}
/*
index = 0 value = 你
index = 3 value = 好
index = 6 value = 世
index = 9 value = 界
index = 12 value = 1
index = 13 value = 2
index = 14 value = 3
*/

strings 包

字符串比较:使用 strings.Compare 比较两个字符串的字典序

1
2
3
strings.Compare("aaa", "bbb") // -1
strings.Compare("baa", "abb") // 1
strings.Compare("aaa", "aaa") // 0

查找函数:使用 strings.Index 查找字符串中子串的位置(第 1 个),不存在返回 -1

1
strings.Index("hello world", "o") // 4

类似的,使用 strings.LastIndex 查找字符串子串出现的最后一个位置,不存在返回 -1

1
strings.Index("hello world", "o") // 4

Count、Repeat

使用 strings.Count 统计子串在整体中出现的次数:

1
strings.Count("abc abc abab abc", "abc") // 3

使用 strings.Repeat 将字符串重复指定次数:

1
strings.Repeat("abc", 3) // abcabcabc

Replace、Split、Join

strings.Replace 实现字符串替换

1
2
3
4
5
6
7
8
str := "acaacccc"

// 局部替换 param3: 替换次数,< 0 则全部替换
strings.Replace(str, "a", "b", 2) // bcbacccc
strings.Replace(str, "a", "b", -1) // bcbbcccc

// 全部替换
strings.ReplaceAll(str, "a", "b") // bcbbcccc

strings.Split 实现字符串切割

1
2
3
4
str := "abc,bbc,bbd"

slice := strings.Split(str, ",")
fmt.Println(slice) // [abc bbc bbd]

strings.Join 实现字符串拼接

1
2
3
4
slice := []string{"aab", "aba", "baa"}

str := strings.Join(slice, ",")
fmt.Println(str // aab,aba,baa

bytes 包

Buffer 是 bytes 包中定义的 type Buffer struct {...},Buffer 是一个变长的可读可写的缓冲区。

创建缓冲器bytes.NewBufferStringbytes.NewBuffer

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
buf1 := bytes.NewBufferString("hello")
buf2 := bytes.NewBuffer([]byte("hello"))
buf3 := bytes.NewBuffer([]byte{'h', 'e', 'l', 'l', 'o'})

fmt.Printf("%v,%v,%v\n", buf1, buf2, buf3)
fmt.Printf("%v,%v,%v\n", buf1.Bytes(), buf2.Bytes(), buf3.Bytes())

buf4 := bytes.NewBufferString("")
buf5 := bytes.NewBuffer([]byte{})
fmt.Println(buf4.Bytes(), buf5.Bytes())
}
1
2
3
hello,hello,hello
[104 101 108 108 111],[104 101 108 108 111],[104 101 108 108 111]
[] []

写入缓冲器WriteWriteStringWriteByteWriteRuneWriteTo

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
buf := bytes.NewBufferString("a")
fmt.Printf("%v, %v\n", buf.String(), buf.Bytes())
// a, [97]

buf.Write([]byte("b")) // Write
buf.WriteString("c") // WriteString
buf.WriteByte('d') // WriteByte
buf.WriteRune('e') // WriteRune
fmt.Printf("%v, %v\n", buf.String(), buf.Bytes())
// abcde, [97 98 99 100 101]
}

缓冲区原理介绍:Go 字节缓冲区底层以字节切片做存储,切片存在长度 len 与容量 cap

  • 缓冲区从长度 len 的位置开始写,当 len > cap 时,会自动扩容
  • 缓冲区从内置标记 off 位置开始读(off 始终记录读的起始位置)
  • 当 off == len 时,表明缓冲区已读完,读完就重置缓冲区 len = off = 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
byteSlice := make([]byte, 20)
byteSlice[0] = 1 // 将缓冲区第一个字节置1
byteBuffer := bytes.NewBuffer(byteSlice) // 创建20字节缓冲区 len = 20 off = 0
c, _ := byteBuffer.ReadByte() // off+=1
fmt.Printf("len:%d, c=%d\n", byteBuffer.Len(), c) // len = 20 off =1 打印c=1
byteBuffer.Reset() // len = 0 off = 0
fmt.Printf("len:%d\n", byteBuffer.Len()) // 打印len=0
byteBuffer.Write([]byte("hello byte buffer")) // 写缓冲区 len+=17
fmt.Printf("len:%d\n", byteBuffer.Len()) // 打印len=17
byteBuffer.Next(4) // 跳过4个字节 off+=4
c, _ = byteBuffer.ReadByte() // 读第5个字节 off+=1
fmt.Printf("第5个字节:%d\n", c) // 打印:111(对应字母o) len=17 off=5
byteBuffer.Truncate(3) // 将未字节数置为3 len=off+3=8 off=5
fmt.Printf("len:%d\n", byteBuffer.Len()) // 打印len=3为未读字节数 上面len=8是底层切片长度
byteBuffer.WriteByte(96) // len+=1=9 将y改成A
byteBuffer.Next(3) // len=9 off+=3=8
c, _ = byteBuffer.ReadByte() // off+=1=9 c=96
fmt.Printf("第9个字节:%d\n", c) // 打印:96
}

缓冲区:

1
2
3
4
5
6
7
8
9
10
func main() {
buf := &bytes.Buffer{}
// 写缓冲区
buf.WriteString("abc?def")
// 从缓冲区读(分隔符为 ?)
str, _ := buf.ReadString('?')

fmt.Println("str = ", str)
fmt.Println("buff = ", buf.String())
}
1
2
str =  abc?
buff = def

缓冲区读数据ReadReadByteReadByesReadStringReadRuneReadFrom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func main() {
log.SetFlags(log.Lshortfile)
buff := bytes.NewBufferString("123456789")
log.Println("buff = ", buff.String()) // buff = 123456789

// 从缓冲区读取4个字节
s := make([]byte, 4)
n, _ := buff.Read(s)
log.Println("buff = ", buff.String()) // buff = 56789
log.Println("s = ", string(s)) // s = 1234
log.Println("n = ", n) // n = 4

// 从缓冲区读取4个字节
n, _ = buff.Read(s)
log.Println("buff = ", buff.String()) // buff = 9
log.Println("s = ", string(s)) // s = 5678
log.Println("n = ", n) // n = 4

n, _ = buff.Read(s)
log.Println("buff = ", buff.String()) // buff =
log.Println("s = ", string(s)) // s = 9678
log.Println("n = ", n) // n = 1

buff.Reset()
buff.WriteString("abcdefg")
log.Println("buff = ", buff.String()) // buff = abcdefg

b, _ := buff.ReadByte()
log.Println("b = ", string(b)) // b = a
log.Println("buff = ", buff.String()) // buff = bcdefg

b, _ = buff.ReadByte()
log.Println("b = ", string(b)) // b = b
log.Println("buff = ", buff.String()) // buff = cdefg

bs, _ := buff.ReadBytes('e')
log.Println("bs = ", string(bs)) // bs = cde
log.Println("buff = ", buff.String()) // buff = fg

buff.Reset()
buff.WriteString("编译输出GO")
r, l, _ := buff.ReadRune()
log.Println("r = ", r, ", l = ", l, ", string(r) = ", string(r))
// r = 32534 , l = 3 , string(r) = 编

buff.Reset()
buff.WriteString("qwer")
str, _ := buff.ReadString('?')
log.Println("str = ", str) // str = qwer
log.Println("buff = ", buff.String()) // buff =

buff.WriteString("qwer")
str, _ = buff.ReadString('w')
log.Println("str = ", str) // str = qw
log.Println("buff = ", buff.String()) // buff = er

file, _ := os.Open("doc.go")
buff.Reset()
buff.ReadFrom(file)
log.Println("doc.go = ", buff.String()) // doc.go = 123

buff.Reset()
buff.WriteString("中国人")
cbyte := buff.Bytes()
log.Println("cbyte = ", cbyte) // cbyte = [228 184 173 229 155 189 228 186 186]
}

strconv 包

字符串转 []byte

1
sum := []byte("hello")

字符串 —> 整数:使用 strconv.Atoistrconv.ParseInt

1
2
3
4
5
6
7
8
9
// 按照 10进制 转换,返回 int 类型
i, _ := strconv.Atoi("33234")
fmt.Printf("%T\n", i) // int

// param1:要转化的字符串
// param2:转换的进制,如 2,8,16,32
// param3:返回bit的大小(注意,字面量显示还是 int64)
i2, _ := strconv.ParseInt("33234", 10, 0)
fmt.Printf("%T\n", i2) // int64

字符串 —> 浮点数:使用 strconv.ParseFloat

1
2
3
4
5
6
// 参数类似 ParseInt
val, _ := strconv.ParseFloat("33.33", 32)
fmt.Printf("type: %T\n", val) // type: float64

val2, _ := strconv.ParseFloat("33.33", 64)
fmt.Printf("type: %T\n", val2) // type: float64

整数 —> 字符串:使用 strconv.Iotastrconv.FormatInt

1
2
3
4
5
6
7
8
num := 180

// 默认按照10进制转换
f1 := strconv.Itoa(num)

// param1: 要转换的数字(必须是int64类型)
// param2: 转换的进制
f2 := strconv.FormatInt(int64(num), 10)

浮点数 —> 整数:使用 strconv.FormatFloat

1
2
3
4
5
6
7
num := 23423134.323422
fmt.Println(strconv.FormatFloat(float64(num), 'f', -1, 64)) // 普通模式
fmt.Println(strconv.FormatFloat(float64(num), 'b', -1, 64)) // 二进制模式
fmt.Println(strconv.FormatFloat(float64(num), 'e', -1, 64)) // 科学记数法
fmt.Println(strconv.FormatFloat(float64(num), 'E', -1, 64)) // 同上,显示为E
fmt.Println(strconv.FormatFloat(float64(num), 'g', -1, 64)) // 指数大时用科学记数,否则普通模式
fmt.Println(strconv.FormatFloat(float64(num), 'G', -1, 64)) // 同上,显示为E
1
2
3
4
5
6
23423134.323422
6287599743057036p-28
2.3423134323422e+07
2.3423134323422E+07
2.3423134323422e+07
2.3423134323422E+07

字符串 和 bool 类型转换

1
2
3
4
5
6
7
// string --> bool
flagBool, _ := strconv.ParseBool("true")
// It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False.
// Any other value returns an error.

// bool --> string
flagStr := strconv.FormatBool(true)

unicode 包

/src/unicode/letter.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 判断字符 r 是否为大写格式
func IsUpper(r rune) bool

// 判断字符 r 是否为小写格式
func IsLower(r rune) bool

// 判断字符 r 是否为 Unicode 规定的 Title 字符
// 大部分字符的 Title 格式就是其大写格式
// 只有少数字符的 Title 格式是特殊字符
// 这里判断的就是特殊字符
func IsTitle(r rune) bool

// ToUpper 将字符 r 转换为大写格式
func ToUpper(r rune) rune

// ToLower 将字符 r 转换为小写格式
func ToLower(r rune) rune

// ToTitle 将字符 r 转换为 Title 格式
// 大部分字符的 Title 格式就是其大写格式
// 只有少数字符的 Title 格式是特殊字符
func ToTitle(r rune) rune

// To 将字符 r 转换为指定的格式
// _case 取值:UpperCase、LowerCase、TitleCase
func To(_case int, r rune) rune

/src/unicode/digit.go

1
2
// IsDigit 判断 r 是否为一个十进制的数字字符
func IsDigit(r rune) bool

/src/unicode/graphic.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// IsNumber 判断 r 是否为一个数字字符 (类别 N)
func IsNumber(r rune) bool

// IsLetter 判断 r 是否为一个字母字符 (类别 L)
// 汉字也是一个字母字符
func IsLetter(r rune) bool

// IsSpace 判断 r 是否为一个空白字符
// 在 Latin-1 字符集中,空白字符为:\t, \n, \v, \f, \r,
// 空格, U+0085 (NEL), U+00A0 (NBSP)
// 其它空白字符的定义有“类别 Z”和“Pattern_White_Space 属性”
func IsSpace(r rune) bool

// IsControl 判断 r 是否为一个控制字符
// Unicode 类别 C 包含更多字符,比如代理字符
// 使用 Is(C, r) 来测试它们
func IsControl(r rune) bool

// IsGraphic 判断字符 r 是否为一个“图形字符”
// “图形字符”包括字母、标记、数字、标点、符号、空格
// 他们分别对应于 L、M、N、P、S、Zs 类别
// 这些类别是 RangeTable 类型,存储了相应类别的字符范围
func IsGraphic(r rune) bool

// IsPrint 判断字符 r 是否为 Go 所定义的“可打印字符”
// “可打印字符”包括字母、标记、数字、标点、符号和 ASCII 空格
// 他们分别对应于 L, M, N, P, S 类别和 ASCII 空格
// “可打印字符”和“图形字符”基本是相同的,不同之处在于
// “可打印字符”只包含 Zs 类别中的 ASCII 空格(U+0020)
func IsPrint(r rune) bool

// IsPunct 判断 r 是否为一个标点字符 (类别 P)
func IsPunct(r rune) bool

// IsSymbol 判断 r 是否为一个符号字符
func IsSymbol(r rune) bool

// IsMark 判断 r 是否为一个 mark 字符 (类别 M)
func IsMark(r rune) bool

// IsOneOf 判断 r 是否在 set 范围内
func IsOneOf(set []*RangeTable, r rune) bool

4、循环语句

go 语言中的 for 循环有 3 种形式:

1
2
3
for init; condition; post { }
for condition { }
for { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
numbers := [6]int{1, 2, 3, 5}

for i := 0; i < len(numbers); i++ {
fmt.Println(numbers[i])
}

i := 0
for i < len(numbers) {
fmt.Println(numbers[i])
i++
}

for i, x := range numbers {
fmt.Printf("index: %d, value: %d\n", i, x)
}

// 无限循环
for {
fmt.Println("endless...")
}
}

range

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}

// 忽略value, 只取index, 支持 string/array/slice/map
for i := range numbers {
fmt.Println(numbers[i])
}

// 忽略 index
for _, n := range numbers {
fmt.Println(n)
}

// 忽略全部返回值,仅迭代
for range numbers {
}

m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Println(k, v)
}

}

注意:range 会复制对象

1
2
3
4
5
6
7
8
9
10
11
func main() {
a := [3]int{0, 1, 2}
for i, v := range a { // index, value 都是从复制品中取出
if i == 0 { // 修改前,先修改原数组
a[1], a[2] = 999, 999
fmt.Println(a) // 确认修改有效 [0, 999, 999]
}
a[i] = v + 100 // 使用复制品中取出的 value 修改原数组
}
fmt.Println(a) // [100, 101, 102]
}

5、函数

多返回值

单返回值的函数:

1
2
3
func foo1(a string, b int) int {
return 100
}

多返回值的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 返回多个返回值,匿名的
func foo2(a string, b int) (int, int) {
return 666, 777
}

// 返回多个返回值,有形参名称的
func foo3(a string, b int) (r1 int, r2 int) {
// r1 r2 属于foo3的形参,初始化默认的值是0
// r1 r2 作用域空间 是foo3 整个函数体的{}空间
fmt.Println("r1 = ", r1) // 0
fmt.Println("r2 = ", r2) // 0

// 给有名称的返回值变量赋值
r1 = 1000
r2 = 2000

return
}

func foo4(a string, b int) (r1, r2 int) {
// 给有名称的返回值变量赋值
r1 = 1000
r2 = 2000

return
}

init 函数

每个 go 程序都会在一开始执行 init() 函数,可以用来做一些初始化操作:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func init() {
fmt.Println("init...")
}

func main() {
fmt.Println("hello world!")
}

如果一个程序依赖了多个包,它的执行流程如下图:

制作包的时候,项目路径如下:

1
2
3
4
5
6
$GOPATH/GolangStudy/5-init/ 
├── lib1/
│ └── lib1.go
├── lib2/
│ └── lib2.go
└── main.go

1
2
3
4
lib1 .init() ...
lib2 .init() ...
lib1Test()
lib2Test()

闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func a() func() int {
i := 0
b := func() int {
i++
fmt.Println(i)
return i
}
return b
}

func main() {
c := a()
c() // 1
c() // 2
c() // 3

a() // 无输出
}

6、指针(*取值 &取地址)

经典:在函数中交换两数的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func swap(pa *int, pb *int) {
var temp int
temp = *pa
*pa = *pb
*pb = temp
}

func main() {
var a, b int = 10, 20

swap(&a, &b) // 传地址

fmt.Println("a = ", a, " b = ", b)
}

7、defer

defer 声明的语句会在当前函数执行完之后调用:

1
2
3
4
func main() {
defer fmt.Println("main end")
fmt.Println("main::hello go ")
}
1
2
main::hello go 
main end

如果有多个 defer,依次入栈,函数返回后依次出栈执行:

上图执行顺序:func3() -> func2() -> func1()

关于 defer 和 return 谁先谁后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func deferFunc() int {
fmt.Println("defer func called...")
return 0
}

func returnFunc() int {
fmt.Println("return func called...")
return 0
}

func returnAndDefer() int {
defer deferFunc()
return returnFunc()
}

func main() {
returnAndDefer()
}

结论:return 之后的语句先执⾏,defer 后的语句后执⾏

8、数组和切片 slice

Golang 默认都是采用值传递,有些值天生就是指针:slice、map、channel。

注意:定长数组是值传递,slice 是指针传递

数组(固定长度数组和动态数组)

go语言之切片即动态数组

切片和数组的类型有什么不一样,我们可以打印一下,就可以知道两者的区别了,数组是容量的,所以中括号中有容量,切片的动态数组,是没有容量,这是数组和切片最大的区别

  • 切片操作是引用,修改切片后的数组也会导致原来的切片数据变化,这里和python是不同的
1
2
3
4
test8_4 := [20] int {0,1,2,3,4,5,6,7,8,9}
test8_5 := [] int {0,1,2,3,4,5,6,7,8,9}
fmt.Println(reflect.TypeOf(test8_4),reflect.TypeOf(test8_5))
//[20]int []int

声明数组的方式:(固定长度的数组)

1
2
3
var array1 [10]int
array2 := [10]int{1,2,3,4}
array3 := [4]int{1,2,3,4}

数组的长度是固定的,并且在传参的时候,严格匹配数组类型

1
2
3
4
5
6
7
8
9
10
11
// 传入参数的数组长度为4,则只能传递长度为4的数组
func printArray(myArray [4]int) {
fmt.Println(myArray) // [1 2 3 4]
myArray[0] = 666 // 数组是值传递
}

func main() {
myArray := [4]int{1, 2, 3, 4}
printArray(myArray)
fmt.Println(myArray) // [1 2 3 4]
}

myArray := [...]int{1, 2, 3, 4} 是自动计算数组长度,但并不是引用传递。


声明动态数组和声明数组一样,只是不用写长度。

1
2
3
4
5
6
7
8
9
10
11
// 不指定长度则是动态数组
func printArray(myArray []int) {
fmt.Println(myArray) // [1 2 3 4]
myArray[0] = 10 // 动态数组是引用传递
}

func main() {
myArray := []int{1, 2, 3, 4}
printArray(myArray)
fmt.Println(myArray) // [10 2 3 4]
}

切片 slice

slice 的声明方式:通过 make 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1 声明一个切片,并且初始化,默认值是1,2,3,长度是3
slice1 := []int{1, 2, 3} // [1 2 3]

// 2 声明一个切片,但是没有给它分配空间
var slice2 []int // slice2 == nil
// 开辟3个空间,默认值是0
slice2 = make([]int, 3) // [0 0 0]

// 3 声明一个切片,同时给slice分配3个空间,默认值是0
var slice3 []int = make([]int, 3) // [0 0 0]

// 4 声明一个切片,同时给slice分配3个空间,默认值是0,通过:=推导出slice是一个切片
slice4 := make([]int, 3) // [0 0 0]

len() 和 cap() 函数:

  • len:长度,表示左指针⾄右指针之间的距离。
  • cap:容量,表示指针至底层数组末尾的距离。

切⽚的扩容机制,append 的时候,如果长度增加后超过容量,则将容量增加 2 倍。

1.18 版本,当 cap小于 256 ,则按照 cap *2 扩容;当cap大于等于 256,则按照 cap + (cap+3*256)/4 扩容(目的,将扩容速率从 2 逐渐降低到 1.25))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var numbers = make([]int, 3, 5)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)

// 向numbers切片追加一个元素1, len = 4, [0,0,0,1], cap = 5
numbers = append(numbers, 1)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)

// 向numbers切片追加一个元素2, len = 5, [0,0,0,1,2], cap = 5
numbers = append(numbers, 2)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)

// 向一个容量cap已经满的slice 追加元素, len = 6, cap = 10
numbers = append(numbers, 3)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
1
2
3
4
len = 3, cap = 5, slice = [0 0 0]
len = 4, cap = 5, slice = [0 0 0 1]
len = 5, cap = 5, slice = [0 0 0 1 2]
len = 6, cap = 10, slice = [0 0 0 1 2 3]

slice 操作

slice 截取是浅拷贝,若想深拷贝需要使用 copy

可以通过设置下限以及上限设置截取切片 [lower-bound: upper-bound],实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
/* 创建切片 */
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
fmt.Println(numbers)

/* 打印原始切片 */
fmt.Println("number ==", numbers)

/* 打印子切片从索引1(包含)到索引4(不包含) */
fmt.Println("numbers[1:4] ==", numbers[1:4])

/* 默认下限为 0 */
fmt.Println("numbers[:3] ==", numbers[:3])

/* 默认上限为 len(s) */
fmt.Println("numbers[4:] ==", numbers[4:])

numbers1 := make([]int, 0, 5)
fmt.Println(numbers1)

/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
numbers2 := numbers[:2]
fmt.Println(numbers2)

/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
numbers3 := numbers[2:5]
fmt.Println(numbers3)
}
1
2
3
4
5
6
7
8
[0 1 2 3 4 5 6 7 8]
number == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
[]
[0 1]
[2 3 4]

利用 copy 函数拷贝切片,是深拷贝

1
2
3
4
5
slice1 := []int{1, 2, 3}
slice2 := make([]int, 3)
copy(slice2, slice1)
slice2[0] = 10
fmt.Println(slice1) // [1 2 3]

直接赋值切片,是浅拷贝

1
2
3
4
slice1 := []int{1, 2, 3}
slice2 := slice1
slice2[0] = 10
fmt.Println(slice1) // [10 2 3]

... 是 Go 的一种语法糖。

  • 用法 1:函数可以用来接受多个不确定数量的参数。
  • 用法 2:slice 可以被打散进行传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func test(args ...string) {
for _, v := range args {
fmt.Println(v)
}
}

func main() {
var ss = []string{
"abc",
"efg",
"hij",
"123",
}
test(ss...)
}

9、map

slice、map、channel 都是引用类型,声明后还需要初始化分配内存,即 make

map 的声明

map 的第一种声明方式:

1
2
3
4
5
6
7
8
9
10
11
12
// 声明myMap1是一种map类型 key是string,value是string
var myMap1 map[string]string
fmt.Println(myMap1 == nil) // true
// 使用map前,需要先用make给map分配数据空间
myMap1 = make(map[string]string, 10)

myMap1["one"] = "java"
myMap1["two"] = "c++"
myMap1["three"] = "python"

fmt.Println(myMap1)
// map[one:java three:python two:c++]

map 的第二种声明方式:

1
2
3
4
5
6
7
myMap2 := make(map[int]string)
myMap2[1] = "java"
myMap2[2] = "c++"
myMap2[3] = "python"

fmt.Println(myMap2)
// map[1:java 2:c++ 3:python]

map 的第三种声明方式:

1
2
3
4
5
6
7
8
myMap3 := map[string]string {
"one": "php",
"two": "c++",
"three": "python",
}

fmt.Println(myMap3)
// map[one:java three:python two:c++]

map 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func printMap(cityMap map[string]string) {
for key, value := range cityMap {
fmt.Println("key = ", key+", value = ", value)
}
}

func AddValue(cityMap map[string]string) {
// map 是引用传递
cityMap["England"] = "London"
}

func main() {
cityMap := make(map[string]string)
// 添加
cityMap["China"] = "Beijing"
cityMap["Japan"] = "Tokyo"
cityMap["USA"] = "NewYork"
// 删除
delete(cityMap, "China")
// 遍历
printMap(cityMap)
fmt.Println("-------")

// 修改
cityMap["USA"] = "DC"
// 利用函数添加 - map 是引用传递
AddValue(cityMap)
// 遍历
printMap(cityMap)
}
1
2
3
4
5
6
key =  Japan, value =  Tokyo
key = USA, value = NewYork
-------
key = England, value = London
key = Japan, value = Tokyo
key = USA, value = DC

判断 map 中 key 值是否存在:直接取值,返回有两个返回值,通过第 2 个返回值判断。

1
2
3
4
5
6
7
m := make(map[string]interface{})
m["a"] = "AAA"
if _, ok := m["ba"]; ok {
fmt.Println("存在")
} else {
fmt.Println("不存在")
}

10、error(panic和recover配合捕获异常)

捕获系统抛出异常:

1
2
3
4
5
6
7
8
9
10
11
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获:", err)
}
}()

nums := []int{1, 2, 3}
fmt.Println(nums[4]) // 系统抛出异常
// 捕获: runtime error: index out of range [4] with length 3
}

手动抛出异常并捕获:

1
2
3
4
5
6
7
8
9
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获:", err)
}
}()
panic("出现异常!") // 手动抛出异常
// 捕获: 出现异常!
}

返回异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func getCircleArea(radius float32) (area float32, err error) {
if radius < 0 {
// 构建个异常对象
err = errors.New("半径不能为负")
return
}
area = 3.14 * radius * radius
return
}

func main() {
area, err := getCircleArea(-5)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(area)
}
}

自定义异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type PathError struct {
path string
op string
createTime string
message string
}

func (p *PathError) Error() string {
return fmt.Sprintf("path=%s \nop=%s \ncreateTime=%s \nmessage=%s",
p.path, p.op, p.createTime, p.message)
}

func Open(filename string) error {

file, err := os.Open(filename)
if err != nil {
return &PathError{
path: filename,
op: "read",
message: err.Error(),
createTime: fmt.Sprintf("%v", time.Now()),
}
}

defer file.Close()
return nil
}

func main() {
err := Open("test.txt")
switch v := err.(type) {
case *PathError:
fmt.Println("get path error,", v)
default:
}
}

面向对象编程

1、type

利用 type 可以声明某个类型的别名(理解为声明一种新的数据类型)

1
2
3
4
5
6
7
type myint int

func main() {
var a myint = 10
fmt.Println("a = ", a)
fmt.Printf("type of a = %T\n", a)
}
1
2
a =  10
type of a = main.myint

2、struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 如果说类首字母大写,表示其他包也能够访问
type Book struct {
// 如果说类的属性首字母大写,表示该属性对外能够访问的,否则的话只能类的内部访问
title string
price string
}

func changeBook(book Book) {
// 传递一个book的副本
book.price = "666"
}

func changeBook2(book *Book) {
// 指针传递
book.price = "777"
}

func main() {
var book Book
book.title = "Golang"
book.price = "111"
fmt.Printf("%v\n", book) // {Golang 111}

changeBook(book)
fmt.Printf("%v\n", book) // {Golang 111}

changeBook2(&book)
fmt.Printf("%v\n", book) // {Golang 777}
}

一道 struct 与指针面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type student struct {
name string
age int
}

func main() {
m := make(map[string]*student)
stus := []student{
{name: "aaa", age: 18},
{name: "bbb", age: 23},
{name: "ccc", age: 28},
}
for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
//aaa => ccc
//bbb => ccc
//ccc => ccc

解决方法 1:

1
2
3
4
5
for _, stu := range stus {
// 方法1
temp := stu
m[stu.name] = &temp
}

解决方法 2:

1
2
3
4
for i, stu := range stus {
// 方法2
m[stu.name] = &stus[i]
}

3、方法

方法:包含了接受者的函数,接受者可以是命名类型或结构体类型的值或者指针。

方法和普通函数的区别

  • 对于普通函数,参数为值类型时,不能将指针类型的数据直接传递,反之亦然。
  • 对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法(反过来也可以)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1.普通函数
// 接收值类型参数的函数
func valueIntTest(a int) int {
return a + 10
}

// 接收指针类型参数的函数
func pointerIntTest(a *int) int {
return *a + 10
}

func structTestValue() {
a := 2
fmt.Println("valueIntTest:", valueIntTest(a))
// 函数的参数为值类型,则不能直接将指针作为参数传递
// fmt.Println("valueIntTest:", valueIntTest(&a))
// compile error: cannot use &a (type *int) as type int in function argument

b := 5
fmt.Println("pointerIntTest:", pointerIntTest(&b))
// 同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
// fmt.Println("pointerIntTest:", pointerIntTest(b))
// compile error:cannot use b (type int) as type *int in function argument
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 2.方法
type PersonD struct {
id int
name string
}

//接收者为值类型
func (p PersonD) valueShowName() {
fmt.Println(p.name)
}

//接收者为指针类型
func (p *PersonD) pointShowName() {
fmt.Println(p.name)
}

func structTestFunc() {
// 与普通函数不同,接收者为指针类型和值类型的方法,指针类型和值类型的变量均可相互调用

// 值类型调用方法
personValue := PersonD{101, "hello world"}
personValue.valueShowName()
personValue.pointShowName()

// 指针类型调用方法
personPointer := &PersonD{102, "hello golang"}
personPointer.valueShowName()
personPointer.pointShowName()
}

4、封装

Golang 中,类名、属性名、⽅法名 首字⺟大写 表示对外(其他包)可以访问,否则只能够在本包内访问。

1
2
3
4
5
6
7
// 如果类名首字母大写,表示其他包也能够访问
type Hero struct {
// 如果类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问
Name string
Ad int
level int // 只能本包访问
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (h *Hero) Show() {
fmt.Println("Name = ", h.Name)
fmt.Println("Ad = ", h.Ad)
fmt.Println("Level = ", h.level)
fmt.Println("---------")
}

func (h *Hero) GetName() string {
return h.Name
}

// 不用指针则传递的是副本,无法赋值
func (h *Hero) SetName(newName string) {
h.Name = newName
}

func main() {
hero := Hero{Name: "zhang3", Ad: 100}
hero.Show()

hero.SetName("li4")
hero.Show()
}
1
2
3
4
5
6
7
8
Name =  zhang3
Ad = 100
Level = 0
---------
Name = li4
Ad = 100
Level = 0
---------

5、继承(go中用组合代替继承)

Golang 通过匿名字段实现继承的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 父类
type Human struct {
name string
sex string
}

func (h *Human) Eat() {
fmt.Println("Human.Eat()...")
}

func (h *Human) Walk() {
fmt.Println("Human.Walk()...")
}

// 子类
type SuperMan struct {
Human // SuperMan类继承了Human类的方法
level int
}

// 重定义父类的方法Eat()
func (s *SuperMan) Eat() {
fmt.Println("SuperMan.Eat()...")
}

// 子类的新方法
func (s *SuperMan) Fly() {
fmt.Println("SuperMan.Fly()...")
}

func main() {
// 定义一个子类对象
// s := SuperMan{Human{"li4", "female"}, 88}
var s SuperMan
s.name = "li4"
s.sex = "male"
s.level = 88

s.Walk() // 父类的方法
s.Eat() // 子类的方法
s.Fly() // 子类的方法
}
1
2
3
Human.Walk()...
SuperMan.Eat()...
SuperMan.Fly()...

6、多态

Go 中接口相关文章:理解Duck Typing(鸭子模型)

Go 接口是一组方法的集合,可以理解为抽象的类型。它提供了一种非侵入式的接口。任何类型,只要实现了该接口中方法集,那么就属于这个类型。(如果没有完全实现接口的方法,接口的指针就不会指向类)

Golang 中多态的基本要素:

  • 有一个父类(有接口)
1
2
3
4
5
6
// 本质是一个指针
type AnimalIF interface {
Sleep()
GetColor() string // 获取动物的颜色
GetType() string // 获取动物的种类
}
  • 有子类(实现了父类的全部接口)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 具体的类
type Cat struct {
color string // 猫的颜色
}

func (c *Cat) Sleep() {
fmt.Println("Cat is Sleep")
}

func (c *Cat) GetColor() string {
return c.color
}

func (c *Cat) GetType() string {
return "Cat"
}
  • 父类类型的变量(指针)指向(引用)子类的具体数据变量
1
2
3
4
// 接口的数据类型,父类指针
var animal AnimalIF
animal = &Cat{"Green"}
animal.Sleep() // 调用的就是Cat的Sleep()方法, 多态

不同接收者实现接口

1
2
3
4
5
6
7
type Mover interface {
move()
}

type dog struct {
name string
}

值类型接收者实现接口:可以同时接收 值类型 和 指针类型。

Go 语言中有对指针类型变量求值的语法糖,dog 指针 dog2 内部会自动求值 *dog2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 可以同时接收 值类型 和 指针类型
func (d dog) move() {
fmt.Println(d.name, "is moving")
}

func main() {
var m Mover

var dog1 = dog{"dog1"}
m = dog1 // 可以接收值类型
m.move()

var dog2 = &dog{"dog2"}
m = dog2 // 可以接收指针类型
m.move()
}

指针类型接收者实现接口:只能接收指针类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 只能接收指针类型
func (d *dog) move() {
fmt.Println(d.name, "is moving")
}

func main() {
var m Mover

// 无法接收非指针类型
// var dog1 = dog{"dog1"}
// m = dog1
//m.move()

var dog2 = &dog{"dog2"}
m = dog2
m.move()
}

一道面试题:以下代码能否通过编译?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type People interface {
Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
if think == "sb" {
talk = "你是个大帅比"
} else {
talk = "您好"
}
return
}

func main() {
var peo People = Student{}
think := "bitch"
fmt.Println(peo.Speak(think))
}

不能。修改 var peo People = Student{}var peo People = &Student{} 即可。

7、通用万能类型和断言

interface{} 表示空接口,可以用它引用任意类型的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// interface{}是万能数据类型
func myFunc(arg interface{}) {
fmt.Println(arg)
}

type Book struct {
auth string
}

func main() {
book := Book{"Golang"}

myFunc(book)
myFunc(100)
myFunc("abc")
myFunc(3.14)
}

Golang 给 interface{} 提供类型断言机制,用来区分此时引用的类型:

注意断言这个操作会有两个返回值

1
2
3
4
5
6
7
8
9
10
func myFunc(arg interface{}) {
// 类型断言
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
} else {
fmt.Println("arg is string type, value = ", value)
fmt.Printf("value type is %T\n", value)
}
}

一个接口的值(简称接口值)是由一个 具体类型具体类型的值 两部分组成的。

这两部分分别称为接口的动态类型和动态值。

1
2
3
4
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

switch 判断多个断言:

1
2
3
4
5
6
7
8
9
10
11
12
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}