学习笔记|go

本文最后更新于:1 年前

1.准备工作

1.1 GOROOT和GOPATH

  • GOROOT:Go SDK的位置。
  • GOPATH:分为全局 GOPATH、项目 GOPATH、模块 GOPATH。

项目 GOPATH 就是项目的根目录,包含以下文件夹:

  • src/:Go 源代码的位置(例如.go,.c.**g.s**)。
  • pkg/:编译软件包代码的位置(例如.a)。
  • bin/:Go 构建的编译可执行程序的位置。

详情:GOROOT和GOPATH

1.2 hello world

1
2
3
4
5
6
7
package main

import "fmt"

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

1.3 goimports

在保存代码后,goimports 可自动导入需要的包,格式化代码(甚至将空格转化为 tab)

1.4 goland 快捷键

代码编辑

  • CTRL+X,剪切、删除当前光标所在行。
  • CTRL+D,复制当前光标所在行。
  • ALT+Q,查看当前方法的声明。
  • CTRL+P, 方法参数提示 。
  • CTRL+空格, 代码提示 。
  • Ctrl+Shift+Up/Down, 代码向上/下移动。
  • CTRL+Backspace,按单词进行删除。
  • SHIFT+ENTER,向下插入新行,即使光标在当前行的中间。
  • ALT+SHIFT+UP/DOWN,将光标所在行的代码上下移动。
  • CTRL+SHIFT+U,将选中内容进行大小写转化。

代码格式化

  • CTRL+ALT+T,把代码包在一个块内,例如if{…}else{…}。
  • CTRL+ALT+L,格式化代码。
  • CTRL+空格,代码提示。
  • CTRL+/,单行注释。
  • CTRL+SHIFT+/,进行多行注释。
  • CTRL+B,快速打开光标处的结构体或方法(跳转到定义处)。
  • CTRL+“+/-”,可以将当前方法进行展开或折叠。

查找和定位

  • CTRL+R,替换文本。
  • CTRL+F,查找文本。
  • CTRL+SHIFT+F,进行全局查找。
  • CTRL+G,快速定位到某行。
  • CTRL+N,查找类 。
  • CTRL+F,在当前窗口查找文本 。
  • CTRL+SHIFT+F,在指定窗口查找文本 。
  • CTRL+R,在当前窗口替换文本 。
  • CTRL+SHIFT+R,在指定窗口替换文本 。

文件相关快捷键

  • CTRL+E,打开最近浏览过的文件。
  • CTRL+SHIFT+E,打开最近更改的文件。
  • CTRL+N,可以快速打开struct结构体。
  • CTRL+SHIFT+N,可以快速打开文件。

2.基础语法

2.1 变量定义

  • var 声明变量类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a int    // 定义一个变量
var b, c int // 定义两个变量
var s string

// 赋初值
var aa int = 1
var bb, cc int = 2, 3
var ss string = "abc"

// 用括号集中定义
var (
a int
s string
)
  • 当赋初值时,不声明变量类型

当不声明类型时,可以不同类型可以一起定义。

1
var b, c, s = 2, 3, "abc"
  • 不使用 var 关键字【推荐

    := 表示定义
    = 表示赋值。
    注:只能在函数内使用这种定义方式。

1
2
3
4
5
6
7
a := 1
b, s := 2, "abc"

// 【注意】
a, b := 1, 2 // 定义 a, b
b, c := 3, 4 // 虽然 b 定义过,因为 c 没定义过,所以 b, c 可以使用 := 定义。
fmt.Println(a, b, c)
  • 包内定义和函数内定义

    • go 没有真正的全局变量,包内定义只能在包内使用。
    • 包内定义推荐使用 var () 集中定义。

2.2 内建变量类型

  • bool、string

  • 无符号整型:int(长度跟随操作系统位数)、int8、int16、int32、int64

  • 指针(长度跟随操作系统位数):unitptr

  • 字符类型(8位,1字节,同 int8):byte

  • 字符类型(32位,4字节,同 int32):rune

    utf-8 中,有的字符需要三个字节表示。

    使用 char 有很多问题,所以 go 没有 char 类型,使用 4 个字节来作为字符类型。

  • 浮点型:float32、float64

  • 复数:complex64、complex128

2.3 类型转化

只有强制类型转换。

1
2
3
4
5
6
7
8
// 参数、返回值均为 float64:func Sqrt(x float64) float64 {}

func triangle() {
a, b := 3, 4
var c int
c = int(math.Sqrt(float64(a*a + b*b))) // 所以此处必须强制转换
fmt.Println(c)
}

2.4 常量定义

不声明类型时,常量的数值可以作为各种类型使用,就相当于文本替换(C++中的宏定义)。

1
2
3
4
5
6
7
8
9
const file_name string = "abc.txt"     // 申明类型

const a, b = 3, 4 // 不声明类型,a、b 既可以做 int 也可以做 float

// 集中定义
const (
a, b = 3, 4
file_name = "abc.txt"
)

特殊的常量:枚举类型

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
func enums() {
const ( // 定义枚举类型
cpp = 0
java = 1
python = 3
golang = 4
)
fmt.Println(cpp, java, python, golang) // 0 1 3 4
}


---------------------
func enums() {
const (
cpp = iota // 创建自增的枚举,这样后面变量可不赋值,自动 +1
java
python
golang
)
fmt.Println(cpp, java, python, golang) // 0 1 3 4
}


---------------------
func enums() {
const (
b = 1 << (10 * iota) // 利用自增作为种子,构建表达式
kb
mb
gb
tb
)
fmt.Println(b, kb, mb, gb, tb) // 1 1024 1048576 1073741824 1099511627776
}

2.5 条件语句

  • if
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
const filename = "./abc.txt"
contents, err := ioutil.ReadFile(filename) // 读取文件,返回两个参数,内容和错误信息。
if err != nil { // 判断是否有错误信息
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}
}


---------------------
// 定义变量和 if 判断写到一起, 用分号做分割
// 注意:此时 contents 只能在 if 中使用。
func main() {
const filename = "./abc.txt"
if contents, err := ioutil.ReadFile(filename); err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}
}
  • switch
    • 默认每个 case 后自动有 break,否则需要使用 fallthrough。
    • panic 函数会终止程序,打印错误信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func grade(score int) string {
yourGrade := ""
switch {
case score >= 0 && score < 60:
yourGrade = "fail"
case score < 90:
yourGrade = "pass"
case score <= 100:
yourGrade = "super"
default:
panic(fmt.Sprintf("Wrong score: %d", score)) // 同 c++ 的 sprintf,将字符串格式化,当作 panic 的参数。
}
return yourGrade
}

2.6 循环

  • 条件没有括号。
  • 初始条件、结束条件、递增表达式都可以省掉。
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
func main() {
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Println(sum)
}

---------------------
// 省略初始条件
i := 1
for ; i <= 100; i++ {
sum += i
}


---------------------
// 省略初始条件、递增表达式,同其他语言的 while(go 没有 while)
i := 1
for i <= 100 {
sum += i
// ...
}


---------------------
// 省略初始条件、结束条件、递增表达式(死循环)
for {
sum += i
// ...
}

【注意】遍历数组时使用指针,需要保存临时变量。

go 中 for i,v :=range slice 中的 i、v 会保存本次循环的索引和值,直接使用 v 的地址会导致内容变更。

因此循环体内需要临时变量保存当前值。

1
2
3
4
5
6
7
8
9
10
for _, x := range nums {
x := x // 保存临时变量
if a == nil || x > *a {
a, b, c = &x, a, b
} else if *a > x && (b == nil || x > *b) {
b, c = &x, b
} else if b != nil && *b > x && (c == nil || x > *c) {
c = &x
}
}

2.7 函数

  • 函数定义

    函数名写在前面,返回类型在后面

1
2
3
4
5
6
7
8
func function(a, b int, s string) int {
if s == "+" {
return a + b
} else if s == "-" {
return a - b
}
return 0
}
  • 返回值可以返回两个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func function(a, b int) (int, int) {
return a + b, a - b
}

func main() {
max, min := function(1, 2) // 接收两个返回值。
tmp, _ := function(1, 2) // 只接收一个返回值,_ 用作占位符。
fmt.Println(max, min, tmp)
}


---------------------
// 函数体比较长,不推荐的写法
func function(a, b int) (max, min int) { // 返回值中声明变量名
max = a + b
min = a - b
return // 返回时可不写变量名。
}
  • 通常情况,两个返回值,一个返回结果一个返回错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func function(a, b int, s string) (int, error) {
if s == "+" {
return a + b, nil
} else if s == "-" {
return a - b, nil
}
return 0, fmt.Errorf("unsuported operation: %s", s) // 返回错误
}

func main() {
if res, err := function(1, 2, "*"); err != nil { // 判断是否有错
fmt.Println(err)
} else {
fmt.Println(res)
}
}
  • 函数作为参数

    同 C++ 中的回调函数。

1
2
3
4
5
6
7
8
9
// 注意函数做参数的定义方式(函数名称,函数参数,函数返回值)
func function(opt func(float64, float64) float64, a, b float64) float64 {
return opt(a, b)
}

func main() {
fmt.Println(function(math.Pow, 2, 3)) // 2 的 3 次方 = 8
fmt.Println(function(math.Max, 2, 3)) // 2 和 3 中最大 = 3
}
  • 匿名函数
1
2
3
4
5
6
7
8
9
10
11
func function(opt func(float64, float64) float64, a, b float64) float64 {
return opt(a, b)
}

func main() {
fmt.Println(function(
func(f1 float64, f2 float64) float64 { // 使用匿名函数作为 function 的参数,完成减法
return f1 - f2
}, 2, 3),
) // 2 - 3 = -1
}
  • 可变参数列表
    • go 没有默认参数、可选参数、函数重载、操作符号重载等花里胡哨的东西。
    • 只有 可变参数列表,相当于一个数组。
1
2
3
4
5
6
7
8
9
10
11
func sumArgs(values ...int) int {   // values ...int 就是可变参数列表
sum := 0
for i := range values {
sum += values[i]
}
return sum
}

func main() {
fmt.Println(sumArgs(1, 2, 3))
}

2.8 指针

  • 因为类型在后面,定义指针和使用指针是很好区分。
  • 指针不能运算(C 语言中指针的复杂来源于指针 + 1)。
1
2
3
4
var a int = 1
var p *int = &a // 定义指针
*p = 3 // 使用指针
fmt.Println(a) // 3

2.9 参数传递

  • C/C++ 可以值传递,也可以引用传递。
  • java/python 大部分类型是引用传递。
  • go 只有值传递,通过指针可以实现引用传递的效果。

2.10 数组

  • 定义
1
2
3
4
5
6
func main() {
var arr1 [3]int
arr2 := [3]int{1, 2, 3} // 使用 := 就应该赋初值
arr3 := [...]int{1, 2, 3} // 可省略数组个数,但是需要加 ...
var arr4 = [4][5]bool // 定义二维数组
}
  • 求长度

数组、slice、string(特殊的 slice) 求长度均使用 len 函数。

1
n := len(arr)
  • 遍历数组

    推荐使用 range 函数。

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

// 遍历数组
for i := 0; i < len(arr2); i++ {
fmt.Println(arr2[i])
}

// 遍历数组【推荐写法】
for i := range arr2 { // range 函数只获取数组下标
fmt.Println(arr2[i])
}

for i, v := range arr2 { // range 函数获取数组下标和值,r 表示下标,v 表示元素值
fmt.Println(i, v)
}

for _, v := range arr2 { // range 函数只获取数组的值
fmt.Println(v)
}
}
  • 数组是值类型
    • [3]int[4]int 是不同类型,类型不同不能直接调用。
    • 调用 func function(arr [3]int) ,传参会拷贝数组,值传递。(大部分语言的数组传递都是引用传递)
    • 在 go 中,一般不直接使用数组,使用切片。
1
2
3
4
5
6
7
8
9
10
11
// [3]int 和 [4]int 是不同类型,类型不同不能直接调用
func function(arr [3]int) { // 传参会拷贝数组,值传递
for i, v := range arr {
fmt.Println(i, v)
}
}

func main() {
arr := [3]int{1, 2, 3}
function(arr)
}
  • 指向数组的指针作为函数参数【一般不用这种方式,推荐使用切片】
1
2
3
4
5
6
7
8
9
10
11
func function(arrPtr *[3]int) {   // 用 指针传递 作为 引用传递
arrPtr[0] = 100 // 使用 指针 直接取值
for i, v := range arrPtr { // range 可直接遍历 arrPtr 指向的数组
fmt.Println(i, v)
}
}

func main() {
arr := [3]int{1, 2, 3}
function(&arr)
}

2.11 切片

slice:切片

既是切片,也是动态数组。

  • 基本用法
    • arr[2:6] 表示第 2 位到第 5 位,左闭右开。
    • 前后下标可以缺省。
1
2
3
4
5
6
7
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6}
fmt.Println(arr[2:6]) // [2 3 4 5]
fmt.Println(arr[2:]) // [2 3 4 5 6]
fmt.Println(arr[:6]) // [0 1 2 3 4 5]
fmt.Println(arr[:]) // [0 1 2 3 4 5 6]
}
  • 底层原理

    • slice 底层结构是一个结构体,里面包含了指针、元素数量、容量。
    • 对于切片 s,len(s) 可以取出 slice 中的元素数量,cap(s) 可以取出 slice 的容量。

1
2
3
4
5
type slice struct {
array unsafe.Pointer // 指向数组的指针
len int // slice 中元素的数量
cap int // 容量:从 slice 起始位置到 array 尾部的长度
}
  • 切片做参数
    • go 所有类型都是值类型。slice 传递的也是值,不过这个值是指向数组的指针,可以对数组进行操作。
    • slice 本身是没有数据的,是对底层 array 的一个 view(视图)。
1
2
3
4
5
6
7
8
9
10
func function(arr []int) {   // [] 表示传递的是 slice,[...] 表示传递的是数组
arr[0] = 100
fmt.Println(arr) // [100 3 4 5]
}

func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6}
function(arr[2:6])
fmt.Println(arr) // [0 1 100 3 4 5 6]
}
  • slice 的拷贝
    • s1、s2 都是 arr 的 view。
1
2
3
4
5
6
7
8
9
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6}
s1 := arr[1:6]
fmt.Println(s1) // [1 2 3 4 5]
s2 := s1[1:4]
fmt.Println(s2) // [2 3 4]
s2 = s2[1:]
fmt.Println(s2) // [3 4]
}
  • slice 的扩展
    • slice 可以向后扩展,不能向前扩展。
    • s[i] 不能超过 len(s),向后扩展不能超过 cap(s)
1
2
3
4
5
6
7
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6}
s1 := arr[1:4]
fmt.Println(s1) // [1 2 3]
s2 := s1[2:4]
fmt.Println(s2) // [3 4]
}

  • slice 的添加元素

    • append(slice, elems) 由于值传递的关系,必须接受 append() 的返回值。

      一般 s2 = append(s1, elems)s1 = append(s1, elems)

    • elems 是可变参数,可以加多个。append(slice, elem1, elem2, elem3)

    • slice 的容量足够时,
      • 会通过指针更改原数组的值。
    • 当 slice 的容量不够时,
      • slice 的指针会指向一个新建的数组,并将原数组中的数据拷贝到新数组中。
      • 扩容原则(类似 STL 中的 vector):新数组是原数组容量的两倍,当原数组长度大于 1024 时,将每次按照不小于 25% 的涨幅扩容。
    • 扩容后的 slice 不再指向原数组,无法更改原数组的值。
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() {
arr := [...]int{0, 1, 2}
s1 := arr[1:2] // [1]


// ======slice 的容量够用======
s1 = append(s1, 20) // [1 20]
fmt.Printf("s1 = %d, len(s1) = %d, cap(s1) = %d\n", s1, len(s1), cap(s1))
// s1 = [1 20], len(s1) = 2, cap(s1) = 2


// ======slice 的容量不够用======
s1 = append(s1, 3, 4)
// 此时,s1 = [1 20 3 4], len(s1) = 4, cap(s1) = 4

s1 = append(s1, 5)
// 此时,s1 = [1 20 3 4 5], len(s1) = 5, cap(s1) = 8


// ======扩容后的 s1 不再指向原数组,无法更改原数组的值======
s1[0] = 999
fmt.Println(arr) // [0 1 20]
}
  • slice 作动态数组(定义)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
// 定义空 slice
// 此时 s == nil, len(s) == 0,cap(s) == 0
var s []int

// 初始化 slice 的值
// 此时 s1 == [1 2 3],len(s) == 3,cap(s) == 3
s1 := []int{1, 2, 3}

// 定义空 slice 并设置元素输量
// 此时 s1 == [0 0 0],len(s) == 3,cap(s) == 3
s2 := make([]int, 3)

// 定义空 slice 并设置容量
// 此时 s1 == [0 0 0],len(s) == 3,cap(s) == 16
s3 := make([]int, 3, 16)
}
  • slice 作动态数组(拷贝)
    • 利用 copy() 拷贝时,是拷贝元素值,不是指向数组的指针。(类似于 C++ 的深拷贝)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
s1 := []int{0, 1, 2, 3}
s2 := make([]int, 8, 16)
copy(s2, s1)
fmt.Println(s1) // [0 1 2 3]
fmt.Println(s2) // [0 1 2 3 0 0 0 0]

// ======= 当 len(s2) 小于 len(s1) 时,只能拷贝前一部分 =======
s1 := []int{0, 1, 2, 3}
s2 := make([]int, 2, 3)
copy(s2, s1)
fmt.Println(s1) // [0 1 2 3]
fmt.Println(s2) // [0 1]
}
  • slice 作动态数组(删除某元素)
    • 使用 append(slice, elems) 函数。
      • s1 = append(s1[:2], s1[3:]...)s1[:2] 中添加 s1[3:] 的所有元素。
      • elems 是可变参数,使用 s1[3:]... 可以省去将 s1[3:] 的所有元素都列举出来。
    • 删除头尾极其方便。
1
2
3
4
5
6
7
8
9
10
11
// 删除中间 2 这元素

func main() {
s1 := []int{0, 1, 2, 3}
s1 = append(s1[:2], s1[3:]...)
fmt.Println(s1) // [0 1 3]
}

// 删除头尾元素比较方便
s1 = s1[1:]
s1 = s1[:len(s1) - 1]

2.12 map

  • 创建

    • 使用 make 建立的是空的 map(empty map),使用 var 定义的是 nil,但是二者可以互相运算,无影响。

      But:nil map 不能直接增加元素,需要用 make 函数创建一个非 nil map。

    • 上述二者打印出来就是 map[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
m := map[string]int{
"cpp": 85,
"java": 40,
"python": 60,
"golang": 100,
}


// 创建 empty map
m0 := map[int]bool{} // m0 == empty map
m1 := make(map[int]bool) // m1 == empty map

var m2 map[string]string // m2 == nil // nil map 不能直接赋值

fmt.Println(m, m1, m2) // map[cpp:85 golang:100 java:40 python:60] map[] map[]
}

补充:

  • 声明的是 nil map。
  • nil map 不能直接赋值。
  • 但是 nil map 可以取值,返回类型的默认值。
1
2
3
4
5
6
7
8
// 声明一个 nil map
var m2 map[string]string

// m2["app"] = "APP" // error:nil map 不能直接赋值
value, ok := m2["http"] // 正确:nil map 可以返回默认值。value = “”, ok = false

m2 = make(map[string]string) // 创建一个非 nil map,可以直接赋值
m2["app"] = "APP" // 正确。
  • 遍历元素
    • 使用 range 遍历 key 或 value 或 key-value 对。
    • hash map,没有顺序(每次打印的顺序不一样)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
m := map[string]int{
"cpp": 85,
"java": 40,
"python": 60,
"golang": 100,
}

for k := range m { // 只遍历 key
fmt.Println(k)
}

for _, v := range m { // 只遍历 value
fmt.Println(v)
}

for k, v := range m { // 遍历 key-value 对
fmt.Println(k, v)
}
}
  • 增加元素
1
2
m1 := make(map[string]int)
m1["add"] = 1
  • 获取元素
    • 当 key 不存在时,获得 value 类型的初始值。
      • int 初始值是 0,string 初始值是空串 "",bool 初始值是 false。
    • 获取元素时,可两个返回值:value、ok。
      • 存在 ok == true
      • 不存在 ok == false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
m := map[string]int{
"cpp": 85,
"java": 40,
"python": 60,
"golang": 100,
}

// 获取存在的元素
value1 := m["cpp"]
fmt.Println(value1) // 85

// 获取不存在的元素
value2, ok := m["app"]
fmt.Println(value2, ok) // 0, false
}
  • 删除元素
1
2
3
4
5
6
7
8
9
10
11
func main() {
m := map[string]int{
"cpp": 85,
"java": 40,
"python": 60,
"golang": 100,
}

delete(m, "java")
fmt.Println(m) // map[cpp:85 golang:100 python:60]
}
  • map 中的 key 的类型

    • map 使用哈希表,必须可以比较相等。
    • 除了 slice、map、function 的内建类型都可以作为 key。
    • 当 struct 不包括上述类型,也可以作为 key。
  • 复合 map

    map 的 value 是个 map。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {

// m 的 key 是 string 类型,value 是 map[string]int 类型。
m := map[string]map[string]int{
"China": {
"cpp": 80,
"java": 20,
},
"USA": {
"cpp": 0,
"java": 0,
},
"England": {},
}
fmt.Println(m) // map[China:map[cpp:80 java:20] England:map[] USA:map[cpp:0 java:0]]
}

2.13 rune

  • 字符串存在的问题:索引和汉字数量无法对上,意味着无法通过索引取到中文汉字。
    • s 有 5 个汉字。
    • len(s) = 15,len 只能获得字节长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
s := "民族有希望"

// 存在问题:索引和汉字数量无法对上
for i, ch := range s {
fmt.Printf("(%d, %c) ", i, ch) // (0, 民) (3, 族) (6, 有) (9, 希) (12, 望)
// fmt.Printf("%c", s[2]) // 无法取到想要的汉字
}
fmt.Println()

// 解决问题:使用 rune 对字符串进行类型转换
for i, ch := range []rune(s) {
fmt.Printf("(%d, %c) ", i, ch) // (0, 民) (1, 族) (2, 有) (3, 希) (4, 望)
}
}
  • 出现问题的原因:
    • string 本质上是字节数组,索引的长度大小是字节,而 utf-8 编码是 3 个字节储存一个汉字,所以汉字数量和索引自然对不上。
    • 之所以转 rune 数组可以解决问题,是因为 rune 是 4 字节储存一个字符/汉字,索引的长度大小是 rune。
  • 十六进制打印验证一下

前置知识点:

  • rune 是 4 个字节储存 1 个字符,不使用 UTF-8 编码方式,使用 unicode 字符集一一对照即可。

  • string 使用的是 UTF-8 编码方式。

  • unicode 只是字符集,UTF-8 是 unicode 的实现方式之一,详见阮一峰:字符编码笔记

如果明白了UTF-8、unicode,应该可以很轻松理解以下两个事实:

  • 在 UTF-8 编码模式下,一个汉字需要 3 个字节表示。
  • 使用 4 个字节储存 1 个汉字,使用 unicode 字符集一一对照时,其实只有后两个字节有数据,前两个字节数据均为 0。

再深入一点,都是使用 unicode 字符集,UTF-8 为什么需要更长的字节?

  • UTF-8 编码是变长的编码方式,所以需要用额外的字符标记,这是 3 个字节表示 1 个字符,而不是 3 个字节表示是 3 个字符。
  • 当规定编码方式为 4 个字节表示 1 个字符,就不需要使用额外的字符来标记多少字节储存一个字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
s := "民族有希望"

// s 本质上就是字节数组,为了将其按 Byte 为单位打印出来,需要将 s 转化为字节数组。
// 每个 ch 是 byte,一共循环 15 次,取出 15 个 byte。
// UTF—8 编码中,3 个字节表示 1 个汉字,所以一共表示 5 个汉字。
for _, ch := range []byte(s) {
fmt.Printf("%X ", ch) // E6 B0 91 E6 97 8F E6 9C 89 E5 B8 8C E6 9C 9B
}
fmt.Println()

// 对于 string,索引 i 还是按照 byte 为单位,但是打印是会按照字符来打印。
// 每个 ch 是 rune,一共循环 5 次,取出 5 个 rune,表示 5 个汉字。
// rune 使用 unicode 符号集,只需 2 个字节可表示一个汉字,所以 rune 的 4 字节,只有后面 2 个字节有数据,故只打印出后面两个字节
for i, ch := range s {
fmt.Printf("(%d, %X) ", i, ch) // (0, 6C11) (3, 65CF) (6, 6709) (9, 5E0C) (12, 671B)
}
fmt.Println()
}
  • 获取 string 中的字符数量。

    • 问题:len(s) 只能获得字节长度,无法获取字符数量。
    • 理解:将 string 转化为 rune,取 rune 的数量。
    • 函数:utf8.RuneCount(string)
  • 在 byte 数组中,获取 rune 字符

    • 理解:将 Byte 数组中的二进制,用 UTF-8 编码,转化为字符,以 rune 类型返回字符,size 表示字符含有几个字节。
    • 函数:ch, size := utf8.DecodeRune(bytes)

2.14 string

  • string 和 rune 数组、byte 数组的关系。

    • string 本质上是 []byte,但是 string 的类型是 string,[]byte 的类型是 []uint8。
    • string 和 []byte 可以互相转化。
    • string 和 []rune 可以互相转化,但 rune 使用 4 个字节储存 string 的一个字符/汉字。
    • []rune 不可以和 []byte 互相转化。
  • string 拼接:直接 + 字符串(双引号)即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // string 与 string 拼接
    s := "abc"
    s += "123"

    // string 与 []rune 拼接
    r := []rune("abc")
    s += string(r)

    // string 与 []byte 拼接
    b := []byte("abc")
    s += string(b)

    // string 与 字符 拼接
    c := 'd'
    s += string(c)
    // fmt.Printf("%s", s) // abc123abcabcd
  • []byte、[]rune 拼接不能使用 +,使用 append

    1
    2
    3
    4
    5
    6
    7
    8
    9
    r1, r2 := []rune("abc"), []rune("def")
    r1 = append(r1, r2...)
    // fmt.Printf("%c", r1) // [a b c d e f]


    r1, r2 := []byte("abc"), []byte("def")
    r1 = append(r1, r2...)
    fmt.Printf("%c", r1) // [a b c d e f]
    fmt.Printf("%s", r1) // [a b c d e f]
  • string 和 rune 数组遍历

    string 和数组遍历一样。

    for i, value := range s 或者 for i := 0; i < len(s); i++

    1
    2
    3
    4
    5
    6
    7
    // 二维数组的使用方式
    func destCity(paths [][]string) {
    for _, path := range paths {
    path[0] = 'A'
    fmt.Println(path)
    }
    }
  • string[i] 是 “c” or ‘c’?如何判断 s[i] == ‘-‘

    • string[0] 是 uint8 类型。

    • 判断方式: s[i] == 'c'

      注意:上述判断方式只能与 uint8(byte)类型比较,与 uint32(rune)类型比较时,需要强制转换为 uint8(byte)类型。

  • ch := 'a' ch 是 uint32,也就是 rune 类型。

  • string 切片

    string 可以切片;切片可用 + 拼接。

  • 字符串打印

    rune 、[]rune、byte、[]byte 均为整型,必需先将其转换为string才能用 Println 打印出来,否则打印出来的是一个整数。

2.15 strings.Builder

string 类型也是只读且不可变的。因此,这种拼接字符串的方式会导致大量的string创建、销毁和内存分配。Golang 1.10 之后,推出了 strings.Builder 用于拼接字符串。

实现文件:strings/builder.go

1
2
3
4
func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)
  • 原理

    1
    2
    3
    4
    type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf []byte
    }
    • 既然 string 在构建过程中会不断的被销毁重建,为了尽量避免这个问题,底层使用一个 buf []byte 来存放字符串的内容。
    • 对于写操作,将新内容 append 到 buf 中即可。
    • buf 容量不够就自动扩容。
  • Grow()

    • 扩容的原理:分配新的更大的 slice。

    • strings.BuilderGrow() 方法是通过 current_capacity * 2 + nn 就是你想要扩充的容量)的方式来对内部的 slice 进行扩容的。

    • 当 n < current_capacity 是不会发生扩容的。

      1
      func (b *Builder) Grow(n int)
  • String()

    • strings.Builder 支持使用 String() 来获取最终的字符串结果。

    • unsafe.Pointer,该类型可以表示任意类型且可寻址的指针值,可以在不同的指针类型之间进行转换。

    • 这里直接将buf []byte转换为 string 类型,无需拷贝 buf,节省内存。

      1
      2
      3
      func (b *Builder) String() string {
      return *(*string)(unsafe.Pointer(&b.buf))
      }
  • 不要拷贝

    • strings.Builder 不推荐被拷贝。当你试图拷贝 strings.Builder 并写入的时候,你的程序就会崩溃。

    • 原因:strings.Builder 拷贝是浅拷贝,拷贝前后两个 builder 指向同一个 buf []byte

    • 拷贝后只支持 len()、string(),不支持更改数据的。除非调用 builder.Reset() 方法重置了 builder,才能写入。

2.15 strings 包

不必过分深究 API,现用现学。参考:字符串操作

strings 包内有诸多字符串操作,常用的有:

  • 分割拼接:Fields,Split,Join
  • 查子串:Contains,Index,Count
  • 转换大小写:ToLower,ToUpper
  • 修建:Trim,TrimRight,TrimLeft
  • 字符串比较:Compare

字符串和基本数据类型之间转换:

  • strconv.Itoa:string 转换为十进制有符号整型,实际上内部调用的 ParseInt(s, 10, 0)

    1
    func ParseInt(s string, base int, bitSize int) (i int64, err error)

3.面对对象

  • go 仅支持封装,不支持继承和多态。(面向接口编程)
  • go 没有 class,只有 struct。

3.1 结构体

  • 定义结构体
1
2
3
4
type treeNode struct {
value int
left, right *treeNode
}
  • 创建结构体
    • 无论结构体指针还是结构体,都用 . 来访问成员。
      • C++ 中使用的方式: root.left->right
      • go 中为:root.left.right
    • new 一个变量返回变量地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {

var root treeNode
root = treeNode{value: 3}

root.right = &treeNode{} // right 是指针,需赋值为地址

root.left = &treeNode{5, nil, nil}

root.left.right = new(treeNode)

fmt.Println(root) // {3 0xc0000ac030 0xc0000ac018}
}
  • 创建结构体数组
1
2
3
4
5
6
7
8
 func mian() {
nodes := []treeNode{
{value: 3},
{},
{6, nil, nil},
}
fmt.Println(nodes) // [{3 <nil> <nil>} {0 <nil> <nil>} {6 <nil> <nil>}]
}
  • go 没有构造函数
    • 如果需要控制结构体构造,可以写工厂函数。
    • 在 go 中,可以返回局部变量的地址。
      • (C++ 中局部变量分配在栈上,离开函数立即销毁,局部变量的地址不可返回,需要在堆上 new 出变量,并手动释放。)
      • (java 中所有变量都是分配在堆上的,有垃圾回收机制)
      • go 的变量分配在堆上还是栈上,无需使用者关心,由编译器根据代码来决定。
    • 那么,编译器根据代码如何决定,go 的局部变量分配在堆上还是栈上?
      • 如果局部变量没有取地址,并返回出去,将分配在栈上。
      • 如果需要取地址并返回,将分配在堆上。
      • 当该变量不再被使用,就会被垃圾回收机制回收。
1
2
3
4
5
6
7
8
func mian() {
ptr := createNode(10)
fmt.Println(*ptr)
}

func createNode(value int) *treeNode {
return &treeNode{value: value} // 此处返回局部变量的地址,go 中不会有问题。
}

3.2 结构体方法

  • 定义与使用
    • (node treeNode) :go 中叫接受者,相当于 this 指针,标记了哪个结构体在调用结构体方法。
    • 结构体方法其实就是普通函数,使用接收者是为了便于 root.print() 的写法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type treeNode struct {
value int
left, right *treeNode
}

// 结构体方法
func (node treeNode) print() {
fmt.Println(node.value)
}

// 普通函数
func print2(node treeNode) {
fmt.Println(node.value)
}

func main() {
root := treeNode{6, nil,nil}
root.print() // 使用结构体方法
print2(root) // 使用普通方法
}
  • 接收者类型

    • 接收者类型是结构体,该方法无法对结构体变量进行更改。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    func main() {
    root := treeNode{6, nil, nil}
    root.setValue(600)
    fmt.Println(root) // {6 <nil> <nil>},root 没有被更改。
    }

    func (node treeNode) setValue(value int) { // 接收者是结构体类型
    node.value = value
    }
    • 接收者类型是结构体指针,该方法可以对结构体变量进行更改。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    func main() {
    root := treeNode{6, nil, nil}
    root.setValue(600)
    fmt.Println(root) // {600 <nil> <nil>},root 被更改。
    }

    func (node *treeNode) setValue(value int) { // 接收者是结构体指针类型
    node.value = value
    }
    • 使用值接收者还是指针接收者呢?

      值接收者是 go 特有的,C++ 和 java 都是 this 指针。

      • 建议:最好使用指针接收者。
      • 结构体过大,值传递需要拷贝,推荐使用指针接收者。
      • 要改变结构体内容,必须使用指针接收者。
  • 无论接收者类型是指针还是结构体,都可以接收指针、结构体两种类型。

    • 在 C++ 中,root.get()root_ptr->get()
    • 但在 go 中,root.print()rootPtr.print() 都是用 .
    • 所以 go 中接收者需要同时可以接收这两种类型。本质上都是传递的地址,再根据接收者的需要,取地址还是取结构体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
root := treeNode{6, nil, nil}
rootPtr := &root

root.print()
rootPtr.print()

root.ptrPrint()
rootPtr.ptrPrint()
}

func (node treeNode) print() {
fmt.Println(node.value)
}

func (node *treeNode) ptrPrint() {
fmt.Println(node.value)
}
  • 结构体指针是nil,也可以调用结构体方法
    • 首先,需要是结构体类型的指针,不可以 nil.happy()
    • 其次,接收者应该是指针类型,否则会报错。(空指针有点特殊)
1
2
3
4
5
6
7
8
func main() {
var ptr *treeNode
ptr.happy()
}

func (node *treeNode) happy() {
fmt.Println("happy")
}

PS:空指针可以调用方法,这能有啥用呢?

A:可以省去在递归是对空指针的判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 二叉树的中序遍历
func (node *treeNode) traverse() {
if node == nil {
return
}
node.left.traverse() // 正是因为空指针可以调用方法,所以此处不用判断 node.left 是否为空。
node.print()
node.right.traverse()
}

func (node treeNode) print() {
fmt.Println(node.value)
}

3.3 封装

go 语言,通过函数的命名来进行封装。

  • 函数命名方式:大驼峰、小驼峰。
  • 大驼峰表示 public。
  • 小驼峰表示 private。

本博客所有文章均个人原创,除特别声明外均采用 CC BY-SA 4.0协议,转载请注明出处!