0、HelloWorld

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}

这就是go语言,现在来逐行认识一下它

  • 第1行:我们的Go程序是由包——package构成的,包的声明形式为:package <包名>。该行的意思是:当前HelloWorld.go文件属于main包。

  • 第3行:如果你使用过Java或Python,那你对import肯定不陌生。该行的意思是:导入一个名为fmt的包。如果需要导入多个包,有两种写法:

    1
    2
    3
    //第一种
    import "fmt"
    import "math"

    或者使用分组形式同时导入多个包

    1
    2
    3
    4
    5
    //第二种
    import (
    "fmt"
    "math/rand"
    )

    显然第二种使用括号,以分组的形式导入更加方便。

  • 第5行:我们使用func关键字来声明一个函数,在这个例子中,我们声明的是main函数。如果你有过其他语言的编程经验,肯定熟悉main函数的作用:程序的入口。(注意本行的左括号 { ,按照go语法标准,不能像C语言那样子单独占一行)

    1
    2
    3
    4
    func main()
    { // 错误写法,原因: go语言函数定义时 `{` 不可以单独占一行
    fmt.Println("Hello, World!")
    }
  • 第6行:我们的函数内容放在函数的{}中,在该例中,调用了fmt包中的打印方法,由此可见,Golang语言调用函数的方法和Java、Python一样使用小数点:<包名>.<函数名>

看完这几行简单代码,我们发现:在Go语言中,并不需要分号;来结束语句。

1、数据结构

1.1、基本数据类型

1.1.1、布尔类型

布尔类型为bool,值可取truefalse,默认值为false

1.1.2、字符串类型

字符串类型为string,默认为空字符串""

1.1.3、数值类型

整数类型分为:

  • 有符号数:intint8int16int32 (rune)int64

  • 无符号数:uintuint8 (byte)uint16uint32uint64

其中intuint的两种类型的长度相同,取决于具体的编译器,比如在32位系统上通常为32位,在64位系统上通常为64位。

int8uint8这些类型则是Go语言直接定义好位数的类型。runebyteint32uint8的别名。

当我们需要使用整数时,应当尽量使用int类型。当然,如果你有特殊的理由使用其他整数类型,便另当他论。

浮点数类型有两种:float32float64,注意没有所谓的float类型

复数类型也有两种:complex64complex128

1.1.4、指针

指针是用来指向地址的引用类型。其本身也有地址, 会套娃。 不开玩笑了, 不懂c语言的此部分可绕开。

C 和 Go 都是有指针概念的语言,此部分主要借这两者之间的异同来加深对 Go 指针的理解和使用。

运算符

C 和 Go 都相同:

  • & 运算符取出变量所在的内存地址

  • * 运算符取出指针变量所指向的内存地址里面的值,也叫 “ 解引用

C 语言版示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main()
{
    int bar = 1;
    // 声明一个指向 int 类型的值的指针
    int *ptr;
    // 通过 & 取出 bar 变量所在的内存地址并赋值给 ptr 指针
    ptr = &bar;
    // 打印 ptr 的值(为地址),*prt 表示取出指针变量所指向的内存地址里面的值
    printf("%p %d\n", ptr, *ptr);
    return (0);
}

//////////////////////////////////////////////
// 输出结果:
// 0x7ffd5471ee54 1

Go 语言版示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
 bar := 1
 // 声明一个指向 int 类型的值的指针
 var ptr *int
 // 通过 & 取出 bar 变量所在的内存地址并赋值给 ptr 指针
 ptr = &bar
 // 打印 ptr 变量储存的指针地址,*prt 表示取出指针变量所指向的内存地址里面的值
 fmt.Printf("%p %d\n", ptr, *ptr)
}

//////////////////////////////////////////////
// 输出结果:
// 0xc000086020 1

Go 还可以使用 new 关键字来分配内存创建指定类型的指针。

1
2
3
4
5
 // 声明一个指向 int 类型的值的指针
 // var ptr *int
 ptr := new(int)
 // 通过 & 取出 bar 变量所在的内存地址并赋值给 ptr 指针
 ptr = &bar

数组名和数组首地址

对于一个数组

1
2
3
4
5
// C
int arr[5] = {12345};
// Go
// 需要指定长度,否则类型为切片
arr := [5]int{12345}

在 C 中,数组名 arr 代表的是数组首元素的地址,相当于 &arr[0]

&arr 代表的是整个数组 arr 的首地址

1
2
3
4
5
6
7
8
9
10
11
12
// C
// arr 数组名代表数组首元素的地址
printf("arr -> %p\n", arr);
// &arr[0] 代表数组首元素的地址
printf("&arr[0] -> %p\n", &arr[0]);
// &arr 代表整个数组 arr 的首地址
printf("&arr -> %p\n", &arr);

// 输出结果:
// arr -> 0061FF0C
// &arr[0] -> 0061FF0C
// &arr -> 0061FF0C

运行程序可以发现 arr&arr 的输出值是相同的,但是它们的意义完全不同。

首先数组名 arr 作为一个标识符,是 arr[0] 的地址,从 &arr[0] 的角度去看就是一个指向 int 类型的值的指针。

&arr 是一个指向 int[5] 类型的值的指针。

可以进一步对其进行指针偏移验证

1
2
3
4
5
6
7
8
// C
// 指针偏移
printf("arr+1 -> %p\n", arr + 1);
printf("&arr+1 -> %p\n", &arr + 1);

// 输出结果:
// arr+1 -> 0061FF10
// &arr+1 -> 0061FF20

这里涉及到偏移量的知识:一个类型为 T 的指针的移动,是以 sizeof(T) 为移动单位的。

  • arr+1 : arr 是一个指向 int 类型的值的指针,因此偏移量为 1*sizeof(int)

  • &arr+1 : &arr 是一个指向 int[5] 的指针,它的偏移量为 1*sizeof(int)*5

到这里相信你应该可以理解 C 语言中的 arr&arr 的区别了吧,接下来看看 Go 语言

1
2
3
4
5
6
7
8
9
// 尝试将数组名 arr 作为地址输出
fmt.Printf("arr -> %p\n", arr)
fmt.Printf("&arr[0] -> %p\n", &arr[0])
fmt.Printf("&arr -> %p\n", &arr)

// 输出结果:
// arr -> %!p([5]int=[1 2 3 4 5])
// &arr[0] -> 0xc00000c300
// &arr -> 0xc00000c300

&arr[0]&arr 与 C 语言一致。

但是数组名 arr 在 Go 中已经不是数组首元素的地址了,代表的是整个数组的值,所以输出时会提示 %!p([5]int=[1 2 3 4 5])

指针运算

指针本质上就是一个无符号整数,代表了内存地址。

指针和整数值可以进行加减法运算,比如上文的指针偏移例子:

  • n : 一个类型为 T 的指针,以 n*sizeof(T) 为单位向高位移动。

  • n : 一个类型为 T 的指针,以 n*sizeof(T) 为单位向低位移动。

其中 sizeof(T) 代表的是数据类型占据的字节,比如 int 在 32 位环境下为 4 字节,64 位环境下为 8 字节

C 语言示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main()
{
    int arr[] = {12345};
    // ptr 是一个指针,为 arr 数组的第一个元素地址
    int *ptr = arr;
    printf("%p %d\n", ptr, *ptr);

    // ptr 指针向高位移动一个单位,移向到 arr 数组第二个元素地址
    ptr++;
    printf("%p %d\n", ptr, *ptr);
    return (0);
}

// 输出结果:
// 0061FF08 1
// 0061FF0C 2

在这里 ptr++0061FF08 移动了 sizeof(int) = 4 个字节到 0061FF0C ,指向了下一个数组元素的地址

Go 语言示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
 arr := [5]uint32{12345}

 // ptr 是一个指针,为 arr 数组的第一个元素地址
 ptr := &arr[0]
 fmt.Println(ptr, *ptr)

 // ptr 指针向高位移动一个单位,移向到 arr 数组第二个元素地址
 ptr++
 fmt.Println(ptr, *ptr)
}

// 输出结果:
// 编译报错:
// .\main.go:13:5: invalid operation: ptr++ (non-numeric type *uint32)

编译报错 *uint32 非数字类型,不支持运算,说明 Go 是不支持指针运算的。

这个为什么不支持, 官方有解答GO FAQ

因此, Go 有指针但不支持指针运算。

另辟蹊径

那还有其他办法吗?答案当然是有的。

在 Go 标准库中提供了一个 unsafe 包用于编译阶段绕过 Go 语言的类型系统,直接操作内存。

我们可以利用 unsafe 包来实现指针运算。

1
2
3
4
5
6
7
8
func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
type ArbitraryType
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
type IntegerType
type Pointer
func Add(ptr Pointer, len IntegerType) Pointer

核心介绍:

  • uintptr : Go 的内置类型。是一个无符号整数,用来存储地址,支持数学运算。常与 unsafe.Pointer 配合做指针运算

  • unsafe.Pointer : 表示指向任意类型的指针,可以和任何类型的指针互相转换(类似 C 语言中的 void* 类型的指针),也可以和 uintptr 互相转换

  • unsafe.Sizeof : 返回操作数在内存中的字节大小,参数可以是任意类型的表达式,例如 fmt.Println(unsafe.Sizeof(uint32(0))) 的结果为4

  • unsafe.Offsetof : 函数的参数必须是一个字段 x.f,然后返回 f 字段相对于 x 起始地址的偏移量,用于计算结构体成员的偏移量

原理:

Go 的 uintptr 类型存储的是地址,且支持数学运算

*T (任意指针类型) 和 unsafe.Pointer 不能运算,但是 unsafe.Pointer 可以和 *Tuintptr 互相转换

因此,将 *T 转换为 unsafe.Pointer 后再转换为 uintptruintptr 进行运算之后重新转换为 unsafe.Pointer => *T 即可

代码实现:

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
package main

import (
 "fmt"
 "unsafe"
)

func main() {
 arr := [5]uint32{12345}

 ptr := &arr[0]

 // ptr(*uint32类型) => one(unsafe.Pointer类型)
 one := unsafe.Pointer(ptr)
 // one(unsafe.Pointer类型) => *uint32
 fmt.Println(one, *(*uint32)(one))

 // one(unsafe.Pointer类型) => one(uintptr类型) 后向高位移动 unsafe.Sizeof(arr[0]) = 4 字节
 // twoUintptr := uintptr(one) + unsafe.Sizeof(arr[0])
 // !!twoUintptr 不能作为临时变量
 // uintptr 类型的临时变量只是一个无符号整数,并不知道它是一个指针地址,可能被 GC
 // 运算完成后应该直接转换回 unsafe.Pointer :
 two := unsafe.Pointer(uintptr(one) + unsafe.Sizeof(arr[0]))
 fmt.Println(two, *(*uint32)(two))
}

// 输出结果:
// 0xc000012150 1
// 0xc000012154 2

甚至还可以更改结构体的私有成员:

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
// model/model.go

package model

import (
 "fmt"
)

type M struct {
 foo uint32
 bar uint32
}

func (m M) Print() {
 fmt.Println(m.foo, m.bar)
}

// main.go

package main

import (
 "example/model"
 "unsafe"
)

func main() {
 m := model.M{}
 m.Print()

 foo := unsafe.Pointer(&m)
 *(*uint32)(foo) = 1
 bar := unsafe.Pointer(uintptr(foo) + 4)
 *(*uint32)(bar) = 2

 m.Print()
}

// 输出结果:
// 0 0
// 1 2

小Tips

Go的底层slice切片源码就使用了unsafe

1
2
3
4
5
6
7
8
9
// slice 切片的底层结构
type slice struct {
 // 底层是一个数组指针
 array unsafe.Pointer
 // 长度
 len int
 // 容量
 cap int
}

总结

  • Go 可以使用 & 运算符取地址,也可以使用 new 创建指针

  • Go 的数组名不是首元素地址

  • Go 的指针不支持运算

  • Go 可以使用 unsafe 包打破安全机制来操控指针,但对我们开发者而言,是 “unsafe” 不安全的

1.2、自定义数据类型

1.2.1、数组、切片type关键字

往往用来自定义一些可以存储相同数据类型的集合类型结构。

切片可以说是专门用来指向此种数据类型结构的指针。

1
2
3
4
5
6
7
// C
int arr[5] = {1, 2, 3, 4, 5}; //这句是拿C语言格式做对比用
// Go
// 数组: 需要指定长度,否则类型为切片
arr := [5]int{1, 2, 3, 4, 5}
// 切片: 相当与有动态数组功能的数组指针
arr := []int{1, 2, 3, 4, 5}

还是没概念的, 可查看&5、数组$6、切片, 后继续观看。

我们可以通过type关键子, 来自定义类型:

1
2
3
4
5
const n = 30

type custom_type [n]byte //byte为uint8的别名,占 8Bit 内存


1.2.2、结构体

往往用来自定义一些可以存储不同数据类型的集合类型结构。

1.2.2.1、定义规范

结构体定义需要使用 type 和 struct 语句。

1
2
3
4
5
6
type typeName struct {
member_1 definition_1 // definition可以是原有数据类型以及自定义数据类型
member_2 definition_2
...
member_n definition_n
}

实例如下:

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
package main

import "fmt"

type Books struct {
title string
author string
subject string
book_id int
}


func main() {

// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "aaa", "Go 语言教程", 6495407})

// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "aaa", subject: "Go 语言教程", book_id: 6495407})

// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "aaa"})
}

//////////////////////////////////////////////////
// 运行结果
{Go 语言 aaa Go 语言教程 6495407}
{Go 语言 aaa Go 语言教程 6495407}
{Go 语言 aaa 0}

1.2.2.2、访问结构体成员

结构体变量名.成员名

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
package main

import "fmt"

type Books struct {
title string
author string
subject string
book_id int
}

func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */

/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "aaa"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407

/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "aaa"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700

/* 打印 Book1 信息 */
fmt.Printf( "Book 1 title : %s\n", Book1.title)
fmt.Printf( "Book 1 author : %s\n", Book1.author)
fmt.Printf( "Book 1 subject : %s\n", Book1.subject)
fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id)

/* 打印 Book2 信息 */
fmt.Printf( "Book 2 title : %s\n", Book2.title)
fmt.Printf( "Book 2 author : %s\n", Book2.author)
fmt.Printf( "Book 2 subject : %s\n", Book2.subject)
fmt.Printf( "Book 2 book_id : %d\n", Book2.book_id)

////////////////////////////////////////////////
// 运行结果
Book 1 title : Go 语言
Book 1 author : aaa
Book 1 subject : Go 语言教程
Book 1 book_id : 6495407
Book 2 title : Python 教程
Book 2 author : aaa
Book 2 subject : Python 语言教程
Book 2 book_id : 6495700

1.2.2.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
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
package main

import "fmt"

type Books struct {
title string
author string
subject string
book_id int
}

func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */

/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "aaa"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407

/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "aaa"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700

/* 打印 Book1 信息 */
printBook(Book1)

/* 打印 Book2 信息 */
printBook(Book2)
}

func printBook( book Books ) {
fmt.Printf( "Book title : %s\n", book.title)
fmt.Printf( "Book author : %s\n", book.author)
fmt.Printf( "Book subject : %s\n", book.subject)
fmt.Printf( "Book book_id : %d\n", book.book_id)
}

///////////////////////////////////////////////////////
// 运行结果
Book title : Go 语言
Book author : aaa
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : aaa
Book subject : Python 语言教程
Book book_id : 6495700

1.2.2.4、结构体指针

  • & 运算符取出变量所在的内存地址

  • * 运算符取出指针变量所指向的内存地址里面的值,也叫 “ 解引用

接下来让我们使用结构体指针重写以上实例,代码如下:

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
package main

import "fmt"

type Books struct {
title string
author string
subject string
book_id int
}

func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */

/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "aaa"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407

/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "aaa"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700

/* 打印 Book1 信息 */
printBook(&Book1)

/* 打印 Book2 信息 */
printBook(&Book2)
}
func printBook( book *Books ) {
fmt.Printf( "Book title : %s\n", book.title)
fmt.Printf( "Book author : %s\n", book.author)
fmt.Printf( "Book subject : %s\n", book.subject)
fmt.Printf( "Book book_id : %d\n", book.book_id)
}

///////////////////////////////////////////////////
// 运行结果
Book title : Go 语言
Book author : aaa
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : aaa
Book subject : Python 语言教程
Book book_id : 6495700

1.2.2.5、结构体成员函数

我们做过c语言开发的朋友都清楚, c语言也是可以面向对象编程的; 比如我就常用 “结构体 + 函数指针” 的组合 来实现最基本的面向对象。

如没有其他语言基础, 请跳过本节 或 先移步阅读2.1、变量3、函数小节后, 再继续阅读。

当然, go语言作为最接近c的语言, 同样是支持面向对象编程的, 通过go的设计, 在实现成员函数时,简化了类似c语言中 结构体+函数指针 这个自定义组合, 提供了更易用的语法, 规范如下:

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
package main

import "fmt"

type typeName struct {
a int
// ...
}

func (typeName) FunName_1(a int) (b float32) {
b = float32(a) + 2.1
return //这里return默认返回b
}


func (tne typeName) FunName_2(a int) (b float32) {
b = float32(a) + 2.1 + float32(tne.a)
return
}

func main() {
var a typeName = typeName{1}
fmt.Println(a.a)
fmt.Println(a.FunName_1(2))
fmt.Println(a.FunName_2(2))
}

//////////////////////////////////////
// 运行结果
1
4.1
5.1

成员函数的声明 与 普通函数的声明类似,区别在于方法的声明在函数名字前加了其所属的结构体名(例子中为typeNametne typeName)

  • 这个tne, 只是我们随便取的名字(可以自定义,我尝试用this或self来命名);
    • 此参数变量作用: 是让我们在成员函数内部可以访问此结构体内部的成员

      自行定义此处的参数变量名, 此变量在成员函数内部的做用: 相当于C++的this指针, 用于调用对象结构体内部所包含的所有成员。

这种普通的结构体成员函数 相当于实现了空接口; 如想了解接口的使用, 可继续前往7、接口小节进行阅读, 以更顺手的姿势在go语言中运用你的面向对象思维。

1.2.2.6、结构体标签

1.2.2.6.1、将结构体标签配合json来使用

Tag是结构体中某个字段别名, 可以定义多个, 空格分隔[7]

1
2
3
type Student struct {
Name string `ak:"av" bk:"bv" ck:"cv"`
}

使用空格来区分多个tag,所以格式要尤为注意

tag的作用相当于该字段的一个属性标签, 在Go语言中, 一些包通过tag来做相应的判断

举个例子, 比如我们有一个结构体

1
2
3
type Student struct {
Name string
}

然后我们将一个该结构体实例化一个 s1

1
2
3
s1 := Student{
Name: "s1",
}

再将 s1 序列化

1
2
3
4
5
v, err := json.Marshal(s1) // json.Marshal方法,json序列化,返回值和报错信息
if err != nil { // 不为nil代表报错
fmt.Println(err)
}
fmt.Println(string(v)) // []byte转string, json

此时 string(v) 为

1
2
3
{
"Name": "s1"
}

因为在 Go 语言中, 结构体字段要想为外部所用就必须首字母大写, 但是如果这个 s1 是返回给前端的, 那每个字段都首字母大写就很怪, 此时我们可以给 Student 加tag解决

结构体修改为

1
2
3
type Student struct {
Name string`json:"name"`
}

序列化时, 会自己找到名为 json 的tag, 根据值来进行json后的赋值

因此,此时的 string(v) 为

1
2
3
{
"name": "s1"
}

下面是一个完整的例子:

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
package main

import (
"encoding/json"
"fmt"
)

type Movie_0 struct {
Title string
Year int
Price int
Actors []string
}

type Movie_1 struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:"actors"`
}

func main() {
movie_0 := Movie_0{"功夫", 2003, 10, []string{"周星驰", "火云邪神"}}
movie_1 := Movie_1{"功夫", 2003, 10, []string{"周星驰", "火云邪神"}}

// 编码的过程 结构体---> json
jsonStr_0, err := json.Marshal(movie_0)
if err != nil {
fmt.Println("json marshal error", err)
return
}
jsonStr_1, err := json.Marshal(movie_1)
if err != nil {
fmt.Println("json marshal error", err)
return
}

fmt.Printf("jsonStr_0 = %s\n", jsonStr_0)
fmt.Printf("jsonStr_1 = %s\n", jsonStr_1)

// 解码的过程
my_Movie_0 := Movie_0{}
my_Movie_1 := Movie_1{}

err = json.Unmarshal(jsonStr_0, &my_Movie_0)
if err != nil{
fmt.Println("json unmarshal error", err)
return
}
err = json.Unmarshal(jsonStr_1, &my_Movie_1)
if err != nil{
fmt.Println("json unmarshal error", err)
return
}

fmt.Printf("%v\n", my_Movie_0)
fmt.Printf("%v\n", my_Movie_1)
}

//////////////////////////////////////////////
// 运行结果
jsonStr_0 = {"Title":"功夫","Year":2003,"Price":10,"Actors":["周星驰","火云邪神"]}
jsonStr_1 = {"title":"功夫","year":2003,"rmb":10,"actors":["周星驰","火云邪神"]}
{功夫 2003 10 [周星驰 火云邪神]}
{功夫 2003 10 [周星驰 火云邪神]}

常用tag记录:

1
2
3
4
json            json序列化或反序列化时字段的名称
db sqlx模块中对应的数据库字段名
form gin框架中对应的前端的数据字段名
binding 搭配 form 使用, 默认如果没查找到结构体中的某个字段则不报错值为空, binding为 required 代表没找到返回错误给前端
1.2.2.6.2、将结构体标签配合反射来使用

下方是一个例子:

Tips: 如不了解Go的反射机制,可先阅读<9、反射>章节

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
package main

import (
"fmt"
"reflect"
)

type resume struct {
Name string `json:"name" doc:"我的名字"`
}

func findDoc(stru interface{}) map[string]string {
t := reflect.TypeOf(stru).Elem()
doc := make(map[string]string)

for i := 0; i < t.NumField(); i++ {
//获取json字段的值 作为map的key, 获取doc字段的值 作为map的value
doc[t.Field(i).Tag.Get("json")] = t.Field(i).Tag.Get("doc")
}

return doc

}

func main() {
var stru resume
doc := findDoc(&stru)
fmt.Printf("name字段为:%s\n", doc["name"])
}

////////////////////
// 运行结果
name字段为:我的名字

1.2.2.7、 结构体的继承

虽然Go结构体没有继承, 但是我们可以通过在某个结构体中写入其他结构体的名字来达到继承目的。

Go中的继承, 没有像C++中所谓的保护公有私有等一系列机制。由于接口的概念, 子类(结构体)也是可以重写父类(结构体)方法的。

1
2
3
4
5
6
7
8
type Father struct{
// ...
}

type Son struct{
Father // Son 继承了 Father 的所有字段属性和方法 , 并且可以重写这些方法
// ...
}

1.3、其他数据结构

1.3.1、枚举

1.3.1.1、定义规范

iota,常常结合常量来用于枚举。

1
2
3
4
5
const (
a = iota
b
c
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时(后续的iota可省略不写),它的值都会自动加 1;所以 a=0, b=1, c=2 。

1.3.1.2、用法

例子1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}

///////////////////////////////////////
// 运行结果
0 1 2 ha ha 100 100 7 8

例子2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"
const (
i=1<<iota
j=3<<iota
k
l
)

func main() {
fmt.Println("i=",i)
fmt.Println("j=",j)
fmt.Println("k=",k)
fmt.Println("l=",l)
}

///////////////////////////////////////
// 运行结果
i= 1
j= 6
k= 12
l= 24

iota 表示从 0 开始自动加 1,所以 i=1<<0, j=3<<1(<< 表示左移的意思),即:i=1, j=6,这没问题,关键在 k 和 l,从输出结果看 k=3<<2l=3<<3

简单表述:

  • i=1:左移 0 位,不变仍为 1。
  • j=3:左移 1 位,变为二进制 110,即 6。
  • k=3:左移 2 位,变为二进制 1100,即 12。
  • l=3:左移 3 位,变为二进制 11000,即 24。

1.3.2、Map

1.3.2.1、介绍

作用相当于C++中stl库里的map, python中的字典。

在go中, map时一种无序的键值对集合。可以向迭代数组和切片那样迭代它。不过,map是无序的,我们无法决定他的返回顺序,这是因为map是使用hash表来实现的。

1.3.2.2、定义规范

仅定义时(不初始化), 如果不用make给其分配内存地址, 后续将无法正常使用, 规范如下例:

1
2
3
4
5
6
7
8
9
10
11
12
var mapName map[keyType]valueType //声明一个map变量,此时只是一个nil指针,还需要初始化才能使用

mapName := make(map[keyType]valueType) //给其分配内存空间, 到此才算定义成功, 可以投入后续的使用

// 当然以上也可以将以上两句, 合一起写, 来完成一个map变量的定义
// var mapName map[keyType]valueType = make(map[keyType]valueType)

// 开始初始化前面定义好的mapName
mapName [ "key_1" ] = "value_1"
mapName [ "key_2" ] = "value_2"
......
mapName [ "key_n" ] = "value_n"

如想避免使用make, 可直接在定义时顺便进行初始化,规范如下例:

1
2
// 定义并初始化
mapName := map[keyType]valueType{"key_1": "value_1", "key_2": "value_2", ...... , "key_n": "value_n"}

1.3.2.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
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import "fmt"

func main() {
var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string) // 不能省略

/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap [ "France" ] = "巴黎"
countryCapitalMap [ "Italy" ] = "罗马"
countryCapitalMap [ "Japan" ] = "东京"
countryCapitalMap [ "India " ] = "新德里"

/*使用键输出地图值 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [country])
}

/*查看元素在集合中是否存在 */
capital, ok := countryCapitalMap [ "American" ] /*如果确定是真实的,则存在,否则不存在 */
/*fmt.Println(capital) */
/*fmt.Println(ok) */
if (ok) {
fmt.Println("American 的首都是", capital)
} else {
fmt.Println("American 的首都不存在")
}
}

//////////////////////////////////////////////////////////
// 运行结果
France 首都是 巴黎
Italy 首都是 罗马
Japan 首都是 东京
India 首都是 新德里
American 的首都不存在

delete()函数用于删除集合的元素, 实例如下:

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
package main

import "fmt"

func main() {
/* 创建map */
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}

fmt.Println("原始地图")

/* 打印地图 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}

/*删除元素*/
delete(countryCapitalMap, "France")

fmt.Println("删除元素后地图")

/*打印地图*/
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
}

///////////////////////////////////////////////////
// 运行结果
原始地图
Italy 首都是 Rome
Japan 首都是 Tokyo
India 首都是 New delhi
France 首都是 Paris
删除元素后地图
Italy 首都是 Rome
Japan 首都是 Tokyo
India 首都是 New delhi

1.4、”空值”、默认值

虽然Golang中没有空值, 但本小节可以基于此概念来展开叙述。

go在编译器安全规则中, 对指针概念做了深度封装, 把c/c++指针的常用场景, 区分后封装为对应语法使得编程新手也能写出内存安全且相对漂亮的代码

因此下文中很多概念, 其实只是在C语言中一个指针走天下时的常用组合的深度封装; 虽是万变不离其宗, 但在封装时出于内存访问的安全考虑, 还是做了很多限制的(很多在c语言中的允许用法在此处会被编译器违规报错), 不过正是基于这些规范, 语法整体才做到了更规范易用, 使得没经验的新手也能写出漂亮的代码,使得编程越来越亲民。

go中所谓的”空值”, 可以是:

  • nil
    • 指针、channel、func、interface、map、slice(切片)类型的变量, 未初始化时的默认值nil

      tips2: 初始值为nilmap无法正常使用(因为map使用时需要初始化 -> 若不想初始化则需要通过make给其分配内存地址,才能不影响后续使用)
      tips3: go 语言中的结构体变量, 未初始化时的默认值为空接口(即必须是有地址的interface实现)(**不能直接通过 nil对其赋值(编译规则不允许), 只能通过new(结构体类型名)对其分配内存,来作为结构体的”空值”)**\

      int、string、float32、float64、struct...等 ,都实现了空接口(interface{}) ; 因此,空接口(interface{})可作为万能类型 来引用 任意数据类型

    • nil可以赋值给 指针、channel、func、interface、map、slice类型的变量

      tips1: nil不能赋值给string的变量编译器会报错。
      tips2: nil不能赋值给自定义结构体类型的变量编译器会报错。

  • ""
    • string字符串类型的变量, 未初始化时的默认值为""
  • 0
    • slice(切片)类型的变量, “0值”指的是是长度容量都为0的空切片, 这个空切片不为nil。
    • 其他类型的变量, 未初始化时的默认值为0
  • 在go中, nil不一定等于nil。

2、变量和常量

2.1、变量

2.1.1、规范

Go语言要求标识符可以为字母或下划线_以及他们的组合, 且不能使用关键字作为标识符。

区分大小写,变量num和变量Num是两个变量。

2.1.2、变量声明

变量声明创建一个或多个变量,将相应的标识符绑定到它们,并给每个变量一个类型和一个初始值。

1
2
3
4
5
6
7
8
9
10
var i int
var U, V, W float64
var k = 0
var x, y float32 = -1, -2
var (
i int
u, v, s = 2.0, 3.0, "bar"
)
var re, im = complexSqrt(-1)
var _, found = entries[name] // 映射匹配值, 此时我们只需要"found", 舍弃 "_" 对应的值映射

如果给出了表达式列表,则使用遵循赋值语句规则的表达式初始化变量。否则,每个变量都被初始化为零值。

如果存在类型,则为每个变量赋予该类型。否则,每个变量都被赋予对应的初始化值的类型。如果该值是无类型常量,则首先将其隐式转换为其默认类型;如果它是一个无类型的布尔值,它首先被隐式转换为布尔类型。预声明的值 nil 不能用于初始化没有明确类型的变量。

1
2
3
4
var d = math.Sin(0.5)  // d is float64
var i = 42 // i is int
var t, ok = true // t is T, ok is bool
var n = nil // 不合规定

如果一个变量声明了但没有使用, 请在编译器编译过程中报错时检查此变量, 若确定多余, 请删除。(对于导入的包同样有此约束规范,即: 导入的包必须使用)

2.1.3、简短变量声明

在本章的 $ 2.2 小节中我们介绍了变量声明相关的语法规范; 其实除此之外,规范中还有一种更为简洁方便的变量声明方式,经常被使用:

  • 它进行声明及初始化时省去了部分关键字(只能在函数内部使用)。

    1
    2
    3
    4
    5
    i, j := 0, 10
    f := func() int { return 7 }
    ch := make(chan int)
    r, w, _ := os.Pipe() // os.Pipe() 返回一对连接的文件描述符和一个错误,如果有的话
    _, y, _ := coord(p) // coord() 有三个返回值;但只对y坐标感兴趣

    如上方代码块, 当我们使用:=声明变量时,不用写var也不用写类型。

在某些情况下,如 “if”、”for “或 “switch “语句初始化时,用来声明本地临时变量。

2.1.4、变量的类型转换

不同类型的变量之间不能直接进行赋值或其他运算。

但也不是没有办法, 比如我们可以先进行类型转换: 比如类型分别为int8int的变量, 可将int8类型转换为int类型,这样就可以间接地进行赋值和其他运算。

使用表达式T(v)将变量v的值的类型转换为T。注意是转换的是变量的值,变量本身的类型不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

var (
a int = 1
b int8 = 2
c uint
d int64
)

func main() {
c = uint(b) //将变量b的值2的类型从int8转换为uint
d = int64(a) + int64(b)

fmt.Printf("c(%T):%v = b(%T):%v\n", c, c, b, b)
fmt.Printf("a(%T):%v + b(%T):%v = d(%T):%v\n", a, a, b, b, d, d)
}

////////////////////////////////////////////
// 运行结果
c(uint):2 = b(int8):2
a(int):1 + b(int8):2 = d(int64):3

注意:Go语言中的类型转换是显示的,表达式T()是必须的,不能省略。

2.1.4.1、string和int类型的相互转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// string转成int:

int, err := strconv.Atoi(string)

// string转成int64:

int64, err := strconv.ParseInt(string, 10, 64)

// int转成string:

string := strconv.Itoa(int)

// int64转成string:

string := strconv.FormatInt(int64,10)

2.1.4.2、int 和 []byte之间的转换

我们都知道, 字符串[]byte,只需要 str_test := []byte("哈哈") 即可;
那么如果是字符串形式的端口号,如"443"端口, 如何转成对应只占 2byte[]byte呢?

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 先转成int类型
c, _ := strconv.Atoi("443")

// 向内存申请符合要求的 **2byte** 的 `[]byte`
d := make([]byte, 2)

// 使用 binary 包(即package名, 是可以小写使用的), 将其转化为**2byte** 的 `[]byte`, 并赋值给d
binary.BigEndian.PutUint16(d, uint16(c))

/////////////////////////////////
// 结果是
d = [0x01, 0xbb] // 443

如果需要反向解析, 则如下示例:

1
2
3
4
5
6
7
c = int(binary.BigEndian.Uint16(d)),

fmt.Println(string(c))

//////////////////
// 结果是
443

2.1.4.3、 按base64协议编解码的[]byte与string

在go语言中, 一般 string 字符串 转换为 []byte, 或是[]byte转换为string时, 我们通常直接 []byte(string) 或是 string([]byte) 即可。

但在某些场景(如网络传输等), 需要针对二进制格式的文件(根证书, 电子邮件附件…)做序列化与反序列化以进行网络传输, 我们的一般流程是:

先按照 base64 协议把这些不可见字节数据(调用编码API时需要传入的参数)编码为可见字节数据(编码过程封装在API内部),进而能够转为字符串类型(调用编码API得到结果字符串),接着将获得的字符串通过 json 进行序列化后, 编码为字节流通过网络发送出去, 直到完全发送。 而对方收到数据后,要先把字节流解码,然后再反序列化为所需字符串(调用解码API时需要传入的参数),接着转为可见字节类型数据(解码过程封装在API内部),最后再使用 base64 解码还原成原始字节数据(调用解码API时得到的结果字节数据)。

关于base64协议介绍, 可查看 Base64 介绍

go语言中, 常用的base64编解码API示例如下:

1
2
3
4
5
//编码    把不可见字节数据,编码为, 可见的字符串类型
passwordString := base64.StdEncoding.EncodeToString([]byte)

//解码 把可见的字符串类型,解码, 还原为原始字节数据 ( 此API参数中的TrimSpace是为了消除字符串两边的空格 )
bs, err := base64.StdEncoding.DecodeString(strings.TrimSpace(passwordString))

2.2、常量

常量是固定的值,值在程序运行期间不会改变。

常量可以定义为数值、字符串、布尔类型

常量的声明方式和变量差不多,区别在于常量需要用const关键字修饰,不能使用:=进行声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

const num int = 555 // 常量
var a int = 1 // 变量

func main() {

const world = "世界" // 常量
const truth = true // 常量

fmt.Println("Hello,", world)
fmt.Println("num = ", num)
fmt.Println("a = ", a)
fmt.Println("对吗?", truth)
}

常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过:

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

import "unsafe"
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)

func main(){
println(a, b, c)
}

//////////////////////////////////////////
// 运行结果
abc 3 16

常量配合iota可以用作枚举, 已经在1.3.1、枚举已经提到过了, 此处不再过多赘述。

3、函数

如果你之前学习过C或者Java等语言,肯定已经对函数(方法)有了一定的认识。

简单地来说,函数是对能完成某个功能的部分代码的抽象。当以后再需要该功能,我们只需要调用其对用的函数即可,不必再重复编写代码。

3.1、函数的声明

已介绍过了, 声明函数的关键字func

1
2
3
func func_name() {

}

3.2、函数的 “参数” “返回值” 定义规范

Go语言中,函数可以有0个或多个参数, 并且还可以有0个或多个返回值。与C语言不同的是,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
package main

import "fmt"

func printName(name string, age int) int{ // 有参,有返回值; 有两个参数, 一个int类型的返回值
fmt.Println("我叫", name, ", 今年", age, "岁了")
a := age + 1
return a
}

func sumAndDiff(x, y int) (int, int) { // 有参,有返回值; 多个类型相同的参数可简写, 而多个类型相同的返回值不可简写
sum := x + y
diff := x - y
return sum, diff //两个返回值
}

func sayHello() { // 无参,无返回值; 无返回值的函数,无需标注返回值类型(与此相比之下 C 语言中则需要有void关键字)
fmt.Println("小豪说:“你好”")
}

func main() {
num := printName("小豪", 1)
c, d := sumAndDiff(5, 1)
sayHello()
}

go语言规范还提供了另一种函数返回的方式: 命名返回值。

顾名思义,我们通过给返回值进行命名,使用空return语句,这样会直接返回已命名的返回值。如上例的sumAndDiff函数可以写为:

1
2
3
4
5
func sumAndDiff(x, y int) (sum int, diff int) {//提前命名返回值
sum = x + y
diff = x - y //返回值在函数中被初始化
return //返回值已经初始化了,不需要再在return语句中写变量了
}

3.3、总结

符合go函数规范的有两种写法

1.一般写法

1
2
3
4
5
6
7
// 一般写法
func functionName(input1, input11 type1, input2 type2 ...) (type1, type11, type2 ...){

//函数体

return value1, value11, value2 ...
}

2.命名返回值写法

1
2
3
4
5
6
7
8
9
10
// 命名返回值写法
func functionName(input1, input11 type1, input2 type2 ...) (output1 type1, output11 type11, output2 type2 ...){

//函数体
output1 = ...
output11 = ...
output2 = ...
...
return
}

3.4、函数的递归

Go 语言支持递归。但我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中。

递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。

3.4.1、用递归实现阶乘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func Factorial(n uint64)(result uint64) {
if (n > 0) {
result = n * Factorial(n-1)
return result // 函数退出, 并返回阶乘的递归结果
}
return 1 // 0的阶乘为1, 所以 如果为n为0 就直接返回1
}

func main() {
var i int = 15
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}

/////////////////////////////////////////////////
// 运行结果
15 的阶乘是 1307674368000

3.4.2、用递归实现斐波那契数列

数学上对该数列的的定义: F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

// 通过递归实现获得此数列数组的函数, 形参n为数列数组的下标
func fibonacci(n int) int {
if n < 2 {
return n
}
return fibonacci(n-2) + fibonacci(n-1)
}

func main() {
var i int
for i = 0; i < 10; i++ { // 比如我们需要用到数列数组的前10个成员, 直接遍历这个递归函数来获取
fmt.Printf("%d\t", fibonacci(i))
}
}

/////////////////////////////////////////////////
// 运行结果
0 1 1 2 3 5 8 13 21 34

3.5、函数导出名

前面我们已经使用了import 导入功能,比如improt "fmt",该行代码可以让我们在本包中使用其他包里的函数。

那么我们如何让其他包能够使用到本包的方法或变量呢?答案是:将方法或变量导出

在Go语言中,如果一个名字以大写字母开头,那么它就是已导出的,这意味着别的包可以使用它。(相当于Java中的public的作用)

比如我们常用的打印函数fmt.Println(...),可以看到Println()的首字母是大写的,所以我们能够导入fmt包后使用该方法。

3.6、init()函数

init()函数就像main函数一样,不带任何参数也不返回任何东西每个包(package)中都存在此函数,并且在初始化包(import)时将调用此函数该函数是隐式声明的因此您不能从任何地方引用它,并且可以在同一程序中创建多个init()函数,并且它们将按照创建顺序执行

您可以在程序中的任何位置创建init()函数,并且它们以词汇文件名顺序(字母顺序)调用。 并允许在init()函数中放置语句,但始终记住init()函数执行的时间点是”在main()函数调用之前”,因此它不依赖于main()函数init()函数的主要目的是初始化无法在全局上下文中初始化的全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main 

import "fmt"

//多个init()函数
func init() {
fmt.Println("Welcome to init() function")
}

func init() {
fmt.Println("Hello! init() function")
}

func main() {
fmt.Println("Welcome to main() function")
}

//////////////////////////////////////////////
// 运行结果
Welcome to init() function
Hello! init() function
Welcome to main() function

此例子是单文件内的, 涉及多个文件时init函数会以词汇文件名顺序(字母顺序)调用, 此处不进行演示。

4、控制语句: “条件” “循环”等

4.1、if语句

if语句是条件判断语句,用来判断是否满足某种条件,如果满足,则执行某段代码;如果不满足,则不执行。

1
2
3
4
5
6
7
if ... {
//代码
} else if ... {
//代码
} else {
//代码
}

注意格式:和c语言不同,go语言的条件判断语句 不需要使用小括号()

下面是几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if a > 0 {//如果满足a>0,则打印Hello, World
fmt.Println("Hello, World")
}
****
if a > 0 {//如果满足a>0,则打印 Hello, World
fmt.Println("Hello, World!")
} else {//否则(即不满足a>0),则打印 你好,世界!
fmt.Println("你好,世界!")
}

if a > 5 {//如果满足a>5,则打印 Hello, World
fmt.Println("Hello, World!")
} else if a <= 5 && a > 0 {//如果满足0<a<=5,则打印 好好学习,天天向上
fmt.Println("好好学习,天天向上")
} else {//否则(即上面的条件都不满足),则打印 你好,世界!
fmt.Println("你好,世界!")
}

Go语言的if语句有一个特性:可以在条件表达式前执行一个简单的语句。下面是一个例子:

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

import "fmt"

func sum(x, y int) int {
return x + y
}

func main () {
if i := sum(1, 2); i > 0 {
fmt.Println("i = ", i)//作用域内,能打印i
}
//fmt.Println("i = ", i)//作用域外,不能打印i, 会在编译阶段直接报错
}

在if语句中,使用sum(x, y int)函数计算出i的值,再进行判断。注意:变量i的作用域只在if语句中有效。

4.2、for语句

for语句是Go语言中的循环控制语句。它有几种形式:

(一)基本形式

1
2
3
for 初始化语句; 条件表达式; 后置语句 {
//循环体代码
}
  • 初始化语句:在第一次循环前之前,且只执行这一次
  • 条件表达式:每次循环都会计算该表达式,如果满足(值为true)则继续循环;如果不满足(值为false),则跳出循环
  • 后置语句:每次循环执行完都会执行该语句
    下面是一个例子,循环打印5次”Hello,World”
1
2
3
for i := 0; i < 5; i++ {
fmt.Println("Hello, World!", i)
}

(二)省略形式

for循环中的初始化语句和后置语句是可以省略的。

1
2
3
4
5
// 省略初始化语句
i := 0
for ; i < 5; i++ {
fmt.Println("Hello, World!", i)
}
1
2
3
4
5
6
// 省略后置语句
i := 0
for ; i < 5; {
fmt.Println("Hello, World!", i)
i++
}

从某种意义上来讲,上面两个例子并没有省略初始化语句或后置语句,只是改变了位置。

(三)while形式

诸如C、Java等语言中都有while循环,但是Go语言中没有while循环,但是我们可以使用for循环来实现“while循环”。

其实(二)省略形式中的第二个for循环例子就已经可以看做是while循环了。我们再稍做改进:

1
2
3
4
5
i := 0
for i < 5 {//去掉两个分号,只写条件表达式
fmt.Println("Hello, World!", i)
i++
}

(四)无限循环形式

1
2
3
4
//打印无限多个Hello, World!
for {
fmt.Println("Hello, World!")
}

4.3、break和continue

上面提到的循环语句只有当条件表达式的值为false时,才会停止循环。但实际开发中,我们可能在条件表达式的值为true的情况下,需要退出循环。这种时候,就需要使用breakcontinue语句。

break语句用来跳出当前循环,continue语句用来跳过本次循环。

下面是两个实例(改进上面循环打印5次”Hello,World!”的例子):

  • 实例1:增加需求,当打印完第2遍Hello,World!时,停止打印

    1
    2
    3
    4
    5
    6
    for i := 0; i < 5; i++ {
    if i == 2 {
    break
    }
    fmt.Println("Hello, World!", i)
    }
  • 实例2:增加需求,不打印第3遍Hello,World!

    1
    2
    3
    4
    5
    6
    for i := 0; i < 5; i++ {
    if i == 2 {
    continue
    }
    fmt.Println("Hello, World!", i)
    }

4.4、switch语句

我们可以使用if…else if…else if…进行一连串的条件判断,但是这样过于繁杂。switch语句就是用来简化这个问题的。

1
2
3
4
5
6
7
8
9
10
11
12
switch 变量 {
case 选项1 :
//操作1代码
case 选项2 :
//操作2代码
case 选项3 :
//操作3代码
case 选项n:
//操作n代码
default :
//默认操作
}

switch语句中有许多case和一个default,只有当变量和case的选项相匹配时,才会执行对应的操作代码。如果没有case的选项可以匹配,则默认执行default的代码。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func sum(x, y int) int {
return x + y
}

func main () {
result := sum(3, 2)
switch result {
case 1 :
fmt.Println("结果为1")
case 2, 3, 4: //多种情况聚合在一起
fmt.Println("结果为2或3或4")
case sum(1, 4): //支持表达式
fmt.Println("结果为5")
default:
fmt.Println("其他结果")
}
}

从上面的例子可以看出,Go语言中switchcase支持常量(不必为整数)、表达式、多个值聚合。注意:不论是常量、表达式,还是多个值聚合,都要保证常量、表达式的值、聚合多个值的类型和switch做判断的变量相同。

switch语句的匹配顺序是自上到下。Go语言自动为每个case提供了break语句,所以在众多选项中只能执行1个casedefault,然后结束,剩余的不再执行。

但是可以使用fallthrough强制执行之后的case(直接强制执行,无需判断case是否匹配,不执行前面已经运行按流程case过了的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
result := sum(1, 1)
switch result {
case 1 :
fmt.Println("结果为1")
fallthrough
case 2, 3, 4:
fmt.Println("结果为2或3或4")
fallthrough
case sum(1, 4):
fmt.Println("结果为5")
fallthrough
default:
fmt.Println("其他结果")
}

//运行结果
结果为234
结果为5
其他结果

4.5、select语句

请先了解go语言的并发 | 8、并发编程后, 再继续阅读select使用 | 8.2.5、select

5、数组

5.1、规范

数组和前文提到的结构体一样,用于自定义数据类型(所定义出的数据类型中,存在多个类型相同的变量)。数组中每个变量称为数组的元素,每个元素都有一个数字编号——数组下标,该下标从0开始,用于区别各个元素。数组中可容纳的元素个数称为数组的长度。

数组的长度是其类型的一部分(即数组不能改变长度)。如下例:

1
2
3
var a [4]int //将变量a声明为拥有4个整数的数组

var b [5]int //将变量b声明为拥有5个整数的数组

变量a和b 的类型分别为[4]int和[5]int,是不同的类型。

5.2、数组声明

1
var arr_name [length]type

var:不必多说,声明变量时都会用到该关键字。

arr_name:数组名称,本质是个变量

length:数组的长度

type:数组的类型

[]:通过它来进行对数组元素的读取、赋值

下面是一个例子:

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

import "fmt"

func main() {
var a [2]string //声明一个长度为2的string数组
a[0] = "我是" //赋值
a[1] = "小豪"
fmt.Println(a[0], a[1]) //获取元素
fmt.Println(a)
}

//////////////////////
//运行结果
我是 小豪
[我是 小豪]

声明的数组,如果不初始化,则数组中的所有元素值都为“零值”。如下例:

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

import "fmt"

func main() {
var a [3]int
var b [3]string
var c [3]bool

fmt.Println(a) //[0 0 0]
fmt.Println(b) //[ ]
fmt.Println(c) //[false false false]
}

对数组元素进行初始化时,若只初始化了部分元素,则剩余的仍是零值。:

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
var a = [5]int {1, 2, 3}
fmt.Println(a) //[1 2 3 0 0]
}

在声明数组时同时初始时,可以通过...代替具体长度指定,来让go编译器自动计算数组长度:

1
var a = [...]int {1, 2, 3} //初始化,数组长度为3

5.3、简短数组声明

数组声明时可以采用:= 来省略var关键字,而且与一般声明方式比, 通过:=声明的方式如果不想初始化, 则必须带上{},如下例:

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

import "fmt"

func main() {
a := [5]int {1, 2, 3} //初始化
b := [3]int {} //声明但未初始化(注意此时比喻带`{}`)
c := [...]int {1, 2, 3}

fmt.Println(a) //[1 2 3 0 0]
fmt.Println(b) //[0 0 0]
fmt.Println(c) //[1 2 3]
}

5.3、二维数组

二维数组当中的元素仍是数组:

1
2
3
var ab = [2][4]int {[4]int {1, 2, 3, 4}, [4]int {4, 5, 6, 7}}

ab := [2][4]int {[4]int {1, 2, 3, 4}, [4]int {4, 5, 6, 7}}

可以省去数组元素的类型:

1
2
3
var ab = [2][4]int {{1, 2, 3, 4}, {4, 5, 6, 7}}

ab := [2][4]int {{1, 2, 3, 4}, {4, 5, 6, 7}}

5.4、遍历数组

1.使用数组长度

  • 可以使用len(array)函数获取数组长度,然后遍历。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arr := [5]string {"a", "b", "c", "d", "e"}

bc := [2][4]int {
{1, 2, 3, 4},
{5, 6, 7, 8},
}

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

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

2.使用range关键字(常用)

  • range关键字用于for循环中遍历数组时,每次迭代都会返回两个值,第一个值为当前元素的下标,第二值为该下标所对应的元素值。如果这两个值的其中一个你不需要,只需使用下划线_代替即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arr := [5]string {"a", "b", "c", "d", "e"}

bc := [2][4]int {
{1, 2, 3, 4},
{5, 6, 7, 8},
}

for i, v := range arr {//遍历一维数组
fmt.Println(i, v)
}

for i := range arr {//遍历时只获取下标
fmt.Println(i)
}

for _, v := range arr{//遍历时只获取元素值
fmt.Println(v)
}

for _, v := range bc {//遍历二维数组
for _, w := range v{
fmt.Println(w)
}
}

tip: 两种遍历方式同样适用于后文的遍历切片

6、切片 (slice)

6.1、概念

前面提到:Go中的数组的长度是固定的。这样就会在实际应用中带来不方便,因为很多时候在声明数组前并不明确该数组要存储多少个元素。声明太多,浪费;声明太少,不够用。

Go中的切片解决了这个问题, 即为我们提供了类似C++stl库中“动态数组”的功能。

6.2、切片声明

声明切片和声明数组类似,但是不指定长度

1
var sli_name []type

比如,声明一个元素集合为int的切片类型、并将其变量名定义为a

1
var a []int

可以在声明它的时候直接初始化:

1
var a = []int {1, 2, 3, 4}

可以从一个已有的数组或者已有的切片中获取切片(常用)。

获取切片的方式是通过两个下标来获取,即开始下标(startIndex)和结束下标(endIndex),二者以冒号分隔。包括startIndex,不包括endIndex

1
a[startIndex : endIndex]

如下例:

1
2
3
4
5
6
7
8
a := [5]string {"a", "b", "c", "d", "e"}    //声明并初始化一个数组
b := []int {1, 2, 3, 4} //声明并初始化一个切片

sliA := a[2:4] // 声明并初始化一个数组,直接从已有数组获取
sliB := b[1:3] // 声明并初始化一个切片,直接从已有切片获取

fmt.Println(sliA) //[c d]
fmt.Println(sliB) //[2 3]

6.3、简短切片声明

支持 -> 在6.2最后的例子中就是用的:=, 注意事项同数组(若仅声明不初始化则必须加{})。

6.4、切片的本质

前面提到:切片为我们提供了“动态数组”。但“动态数组”只是我们从表面来看的结果。更准确的说,从本质来看它更接近c语言中的”数组指针”,并且是对此概念高度封装后的开箱即用产物,因此他的本质是指针,是一个引用类型。

可以说切片即兼顾了c数组指针使用时的性能、内存安全等问题,避免了使用的繁琐; 又适配了c++动态数组的功能和易用性, 避免了概念太多所面临的学习与使用门槛提高。

tips: 由此可见,go对c/c++中常用的一些抽象概念, 重新设计后在语言层级封装好, 还自动gc的一系列做法, 确实可以做到让编程语言的使用难度直线下滑(后续的语言层并发更是重量级), 同时也会吸引更多的开发者使用go;

切片并不存储任何数据,它只是一个引用类型,切片总是指向一个底层的数组,描述这个底层数组的一段。

所以我们在声明数组时需要指定长度,而声明切片时不需要:

1
2
3
var arr = [4]int {1, 2, 3, 4} //声明数组

var slice = []int {1, 2, 3, 4} //声明切片

由于切片的底层引用的是数组,所以更改切片中的元素会修改其底层数组中对应的元素,如果还有其他切片也引用了该底层数组,那么这些切片也能观测到这些修改。如图:

下面是一个例子:

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
package main

import "fmt"

func main() {
array := [5]string {"aa", "bb", "cc", "dd", "ee"} //数组
fmt.Println(array) //[aa bb cc dd ee]

slice1 := array[0:2] //切片1
slice2 := array[1:3] //切片2
slice3 := array[2:5] //切片3

fmt.Println(slice1) //[aa bb]
fmt.Println(slice2) //[bb cc]
fmt.Println(slice3) //[cc dd ee]

slice1[0] = "xx" //修改切片1中的值
slice2[1] = "yy" //修改切片2中的值
slice3[2] = "zz" ////修改切片3中的值

fmt.Println(array) //[xx bb yy dd zz]
fmt.Println(slice1) //[xx bb]
fmt.Println(slice2) //[bb yy]
fmt.Println(slice3) //[yy dd zz]
}

6.5、切片的相关操作

6.5.1、长度

切片的长度指切片所包含的元素个数。通过函数len(slice)获取切片slice的长度。

6.5.2、容量

切片的容量指切片的第一个元素到其底层数组的最后一个元素的个数。通过函数cap(slice)获取切片slice的容量。

下面是一个例子:

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

import "fmt"

func main() {
arr := [10]string {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
s := arr[2:5] //创建切片s

fmt.Println(arr) //[a b c d e f g h i j]
fmt.Println(s) //[c d e]

fmt.Println(len(s)) //3
fmt.Println(cap(s)) //8
}

下面是长度和容量的示意图:

有了容量这个概念,我们就可以通过重新切片来改变切片的长度:

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
package main

import "fmt"

func main() {
arr := [10]string {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}

s := arr[2:5]
fmt.Println(s)
fmt.Printf("s的长度为%d,s的容量为%d\n", len(s), cap(s))

s = s[2:8]
fmt.Println(s)
fmt.Printf("s的长度为%d,s的容量为%d\n", len(s), cap(s))

s = s[0:2]
fmt.Println(s)
fmt.Printf("s的长度为%d,s的容量为%d\n", len(s), cap(s))
}

//////////////////////////////////////////////////////////////
// 运行结果
[c d e]
s的长度为3,s的容量为8
[e f g h i j]
s的长度为6,s的容量为6
[e f]
s的长度为2,s的容量为6

6.5.3、追加元素

使用func append(slice []Type, elems ...Type) []Type可以向切片slice的末尾追加类型为Type的元素elems

该函数的结果是一个包含原切片所有元素加上新添加元素的切片。由于改变切片内容了,所以底层数组也会被改变。

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

import "fmt"

func main() {
s := []string {"a", "b", "c", "d"}
s = append(s, "e") //追加1个
s = append(s, "f", "g", "h") //追加3个
fmt.Println(s)
}

当切片中容量已经用完时(len(s) == cap(s)),也即底层数组容纳不了追加的元素时,Go会分配一个更大的底层数组,返回的切片指向这个新分配的数组,原数组的内容不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
arr := [5]string {"a", "b", "c", "d", "e"}
slice1 := arr[0:2]
fmt.Println(slice1) //[a b]
//追加3个元素,slice1的容量已满
slice1 = append(slice1, "1", "2", "3")
fmt.Println(slice1) //[a b 1 2 3]
//底层数组跟着改变
fmt.Println(arr) //[a b 1 2 3]
//继续追加
slice1 = append(slice1, "4", "5")
//指向新的底层数组
fmt.Println(slice1) //[a b 1 2 3 4 5]
//原底层数组不变
fmt.Println(arr) //[a b 1 2 3]
}

6.5.4、复制切片

func copy(dst []Type, src []Type) int

dst是目标切片,src是源切片,该函数会将src中的元素复制到dst中,并返回复制的元素个数(该返回值是两个切片长度中的小值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
slice1 := []string {"a", "b"}
slice2 := []string {"1", "2", "3"}
length := copy(slice2, slice1)
//length := copy(slice1, slice2)
fmt.Println(length)
fmt.Println(slice1)
fmt.Println(slice2)
}

////////////////////////
// 运行结果
2
[a b]
[a b 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
slice1 := []string {"a", "b"}
slice2 := []string {"1", "2", "3"}
// length := copy(slice2, slice1)
length := copy(slice1, slice2)
fmt.Println(length)
fmt.Println(slice1)
fmt.Println(slice2)
}

//////////////////////
// 运行结果
2
[1 2]
[1 2 3]

6.5.5、切片默认行为

切片的默认开始下标是0,默认结束下标是切片的长度。

而且在使用时,也会用此默认值来代替未明确表示的值, 举个例子, 如下:

1
2
3
4
5
6
7
8
// 对于数组a
var a [10]int

//下面几个切片式引用是等价的:
//// a[0:10]
//// a[:10]
//// a[0:]
//// a[:]

6.6、特殊切片

6.6.1、nil切片

切片的零值是 nil,当声明一个切片,但不出初始化它,该切片便为nil切片。nil切片的长度和容量为0且没有底层数组。( 此处的nil证实了之前对应切片本质的说明)

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("s切片是nil切片")
}
}

/////////////////////////////////////////
// 运行结果
[] 0 0
s切片是nil切片

6.6.2、切片的切片

切片中的元素可以是切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
ss := [][]int {
[]int {1, 2, 3},
[]int {4, 5, 6},
[]int {7, 8, 9},
}

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

///////////////////////////////////////////
// 运行结果
[1 2 3]
[4 5 6]
[7 8 9]

6.7、使用make函数创建切片

使用make函数可以在创建切片时指定长度和容量。make函数会分配一个元素为零值的数组并返回一个引用了它的切片

该函数接受三个参数,分别用来指定切片的类型、长度、容量。当不传入容量参数时,容量默认和长度相同。容量参数不能小于长度参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
a := make([]int, 5)
fmt.Println(a, len(a), cap(a)) //[0 0 0 0 0] 5 5

b := make([]int, 5, 6)
fmt.Println(b, len(b), cap(b)) //[0 0 0 0 0] 5 6

//c := make([]int, 5, 4)
//fmt.Println(c, len(c), cap(c))//报错:len larger than cap in make([]int)
}

///////////////////////////////////////////////
// 运行结果
[0 0 0 0 0] 5 5
[0 0 0 0 0] 5 6

6.8、遍历切片

因为切片是对数组的引用,所以遍历切片也就是在遍历数组

7、接口

7.1、接口的定义与使用

7.1.1、普通使用

Go语言中的接口定义使用 interface 关键字,定义形式与结构体类似。

接口的实现是”隐式”的, 没有关键词, 可以先看个例子后,再进行解释, 规范如下:

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
package main

import "fmt"

type interfaceName interface { // 定义名为 interfaceName 的接口, 里面有一系列未实现的函数

// 若此接口成员方法为我们设计中多个对象中都需要的方法, 就可以通过接口写出; 随后由需要此方法的结构体来实际定义具体其功能(是不是想到了c++的多态)。
testFun()
}

type typeName_1 struct {
a int
// ...
}

type typeName_2 struct {
a int
}

func (typeName_1) FunName(a int) (b float32) { // 定义了普通的结构体的成员函数
b = float32(a) + 2.1
return //这里return默认返回b
}

func (typeName_1) testFun(a int) (b float32) { // 在结构体typeName_1中 ,实现了接口 interfaceName 中的testFun()方法
b = float32(a) + 5.5
fmt.Println("我是小小工厂typeName_1")
return
}

func (typeName_2) testFun(a int) (b float32) { // 在结构体typeName_2中, 实现了接口 interfaceName 中的testFun()方法
b = float32(a) + 9.5
fmt.Println("我是小小工厂typeName_2")
return
}

func main() {
var a typeName_1 = typeName_1{1}
var b typeName_2 = typeName_2{1}
fmt.Println(a.a)
fmt.Println(a.FunName(2))

// 使用接口中的成员函数
fmt.Println(a.testFun(2))
fmt.Println(b.testFun(2))
}

///////////////////////////////////////////////////////////////
// 运行结果
1
4.1
我是小小工厂typeName_1
7.5
我是小小工厂typeName_2
11.5

功能与C#中接口一样, 或着 说跟 c++中 “虚函数+多态” 组合方式的 功能一样, 可容易写出通用的代码框架, 实现设计模式(如工厂模式)。

重写实现接口定义采用的方式不同于C#的继承方式, 因为go中没有继承。

go语言 是通过 结构体定义成员函数的方式 来直接实现对接口中所含虚函数的定义。故前面说接口的实现是”隐式”的。

将 定义结构体成员函数 时的 普通成员”自定义函数名” 改为 接口中某个已有的”虚成员的函数名” 来实现接口中成员方法的实际定义

7.1.2、多态使用

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import (
"fmt"
)

type Phone interface {
call()
}

type NokiaPhone struct {
}

func (NokiaPhone) call() { // 用法与上个例子一样,没有用的结构体内其他成员
fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
value string // 如未初始化, 默认值为空字符串""
num int // 如未初始化, 默认值为0
}

func (iPhone IPhone) call() { // 如果想使用结构体对象中的其他成员, 同定义普通结构体成员函数一样, 给出一个具体的形参即可(形参名自行定义)
fmt.Println("I am iPhone, I can call you!")
fmt.Println("IPhone value = ", iPhone.value)
fmt.Println("IPhone num = ", iPhone.num)

}

func main() {
var phone Phone // 用于演示接口的多态性质
var a IPhone
a.value = "500元"
a.num = 123
a.call()

fmt.Println("-------------------------------------------------------------------")

phone = new(NokiaPhone) // 多态 NokiaPhone对象
phone.call()

fmt.Println("-------------------------------------------------------------------")

phone = new(IPhone) // 多态 IPhone对象
phone.call() // 涉及到未初始化的结构体成员, 会按默认值处理
}

/////////////////////////////////////////////////////////////////////////
// 运行结果
I am iPhone, I can call you!
IPhone value = 500
IPhone num = 123
-------------------------------------------------------------------
I am Nokia, I can call you!
-------------------------------------------------------------------
I am iPhone, I can call you!
IPhone value =
IPhone num = 0

7.1.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
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
package main

import "fmt"

type I interface {
I2 // 继承了I2
}

type I2 interface {
Rrrrr(name string) int
}

type A struct {
I // 继承了I
name string
}

type B struct {
age int
}

func (b B) Rrrrr(name string) int {
fmt.Println("Rrrrr", name, b.age)
return 3
}

func (b B) Yrrrr(name string) int {
fmt.Println("Yrrrr", name, b.age)
return 3
}

func main() {
b := B{age: 80}
a := A{I: b, name: "xiao a"}
fmt.Println(a.Rrrrr(a.name))
// fmt.Println(a.I.Rrrrr(a.name)) // 使用时, 建议使用完整路径, 使得代码更易读
// fmt.Println(a.Yrrrr(a.name)) // 虽然将 b 赋值给了 I , 但是 a 中依旧是没有Yrrrr这个函数的。因为 接口 I 中并没有Yrrrr

fmt.Println(b.Rrrrr(a.name))
fmt.Println(b.Yrrrr(a.name))
}

////////////////////////////////////////////
// 运行结果
Rrrrr xiao a 80
3
Rrrrr xiao a 80
3
Yrrrr xiao a 80
3

7.2、空接口与类型断言

如果一个接口里面没有定义任何方法, 那么他就是空接口, 前面我们提到过结构体成员函数就是通过空接口实现的,换句话说:任意结构体都隐式的实现了空接口

什么是隐式实现? 即 Go语言为了避免用户重复定义很多空接口,它自己内置了空接口:interface{}

空接口里面没有方法,因此不具备任何能力,但是它的作用在于 可以容纳任意对象,它是一个万能容器。例如实现一个字典,字典的key是字符串,但是希望value可以容纳 任意类型的对象,这时就可以使用空接口来实现, 规范如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
var user map[string]interface{} = map[string]interface{}{
"name": "小豪",
"age": 26,
"isman": true,
} //通过空接口可突破单个 map 中 value 类型的限制

fmt.Println(user)

// 这里用到了interface{}类型转换, 请注意写法; 此写法也可作类型断言来使用。
fmt.Println(user["name"].(string), user["age"].(int), user["isman"])
// 虽然, 手动转换可省略不写如 user["isman"]; 但根据我自身习惯, 本人还是建议平时别嫌麻烦, 统一地把转换语句给用上。
}

///////////////////////////////////////////////////////////////
// 运行结果
map[age:26 isman:true name:小豪]
小豪 26 true

tips: int、string、float32、float64、struct...等都实现了空接口(interface{});因此 空接口(interface{}) 可以作为万能类型 来引用 任意数据类型(即相当于指针)(若以接口做形参即指针做形参,调用时需所需数据类型变量的地址进去); 空接口形参在函数内使用时,也时常会用类型断言机制来判断具体类型。

下面是一个空接口与类型断言结合使用的案例:

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
package main

import "fmt"

type Book struct {
auth string
}

func myFunc(arg interface{}) {
fmt.Println("myFunc is called...")
fmt.Println(arg)

// interface{} 该如何区此处引用的底层数据类型呢?

// 可以通过"类型断言"的机制,来区分数据类型(if 或 switch均可)

value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
fmt.Println("")
} else {
fmt.Println("arg is string type, value = ", value)

fmt.Printf("value type is: %T \n ", value)
fmt.Println("")
}
}

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

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

///////////////////////////////////////////////
// 运行结果
myFunc is called...
{Golang}
arg is not string type

myFunc is called...
100
arg is not string type

myFunc is called...
abc
arg is string type, value = abc
value type is: string

myFunc is called...
3.14
arg is not string type

8、并发编程

协程并发

协程:coroutine。也叫轻量级线程。比如lua语言就支持协程。

与传统的系统级线程和进程相比,协程最大的优势在于“轻量级”。可以轻松创建上万个而不会导致系统资源衰竭。而线程和进程通常很难超过1万个。这也是协程别称“轻量级线程”的原因。

一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源

多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能也并不完整,比如仅仅提供协程的创建、销毁与切换等能力。如果在这样的轻量级线程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,从而无法真正达到轻量级线程本身期望达到的目标。

在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少!但能达到进程、线程并发相同的效果。

在一次并发任务中,进程、线程、协程均可以实现。从系统资源消耗的角度出发来看,进程相当多,线程次之,协程最少。

Go并发

Go 在语言级别支持协程,叫goroutine。Go 语言标准库提供的所有系统调用操作(包括所有同步IO操作),都会出让CPU给其他goroutine。这让轻量级线程的切换管理不依赖于系统的线程和进程,也不需要依赖于CPU的核心数量。

有人把Go比作21世纪的C语言。第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持并发。同时,并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制。

Go语言为并发编程而内置的上层API基于顺序通信进程模型CSP(communicating sequential processes)。这就意味着显式锁都是可以避免的,因为Go通过相对安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。

Go语言中的并发程序主要使用两种手段来实现。goroutine和channel。

什么是Goroutine

goroutine是Go语言并行设计的核心,有人称之为go程。 Goroutine从量级上看很像协程,它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

一般情况下,一个普通计算机跑几十个线程就有点负载过大了,但是同样的机器却可以轻松地让成百上千个goroutine进行资源竞争。

8.1、go程(goroutine)

Go是直接语言层支持并发的, Go的并发需要用到 go 关键字。

go程(goroutine) 可看作是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的, 在go程种的代码可以和其他代码并发执行。

为方便解释, go程这个词在本文的某些位置依旧由线程这个词来表达

goroutine 语法格式:

go 函数名( 参数列表 ) -> 使用 go 语句开启一个新的运行期go程(即goroutine) 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。

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
package main

import (
"fmt"
"time"
)

func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
go say("world")
say("hello")
}

//////////////////////////////////////////////////////
// 运行结果 (第一种结果)
world
hello
hello
world
world
hello
world
hello
hello
world

// 运行结果 (第二种结果)
hello
world
world
hello
hello
world
world
hello
world
hello

通过两种不同的运行结果, 可以看出输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行

8.2、通道(channel)

8.2.1、并发概念与锁的介绍

通道(channel)用来传递数据的一个数据结构,用于数据交互。而且通道本身保证了多线程场景下绝对安全的数据交互

  • 绝对安全指的是: c/c++/c#等语言, 在多线程场景下容易造成的 各种死锁越权访问临界区设置不够精确而浪费掉部分性能… 等问题, 在用通道来实现数据交互时,统统不存在。正是这一机制, 使得新手程序员也能实现内存安全且高性能的代码, 这也是为什么我前面说go是语言层支持并发的。
  • go写起来有多方便,我来举个例子: 我们写c/c++代码时, 出于性能考虑, 一般会采用多线程, 那么就不可避免地要面临”临界区如何设定范围, 以及对各种锁的选择问题”, 同时这些问题地处理,往往是很大的性能优化点, 有着很大地可优化空间。 (比如,临界区把范围全包的话,还不如单线程,简直浪费资源; 当同时有多个读操作时,需要读写锁,可一旦使用,过程中也就避免不了对正在访问数据以及如何访问的关注)。 使用了通道后,很多场景下的此类情况,也就没有必要再过多地考虑了; 与此同时,这个可优化空间也已被Go通道给榨干了, 直接在语言层解决基于此类问题地所有优化项。
  • 如果你的应用场景的某些复杂全局项需要用到锁(如一些全局的非通道的,需并发使用的变量,还是离不开锁的), 或是应用场景过于简单时,不想使用高封装化的通道传递数据, 即想针对简单场景做性能优化。那么Go中同样也提供了sync.Mutexsync.RWMutex等基本锁功能。(其中sync.Mutex锁, 其实不光有LockUnlock方法,也提供了RLockRLock方法,这里的R代表Read。)

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向(即发送或接收)。如果未指定方向,则为双向通道

1
2
3
ch <- v    // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:

ch := make(chan int)

8.2.2、无缓冲区的通道

注意: 默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

下面是一个例子:

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
package main

import (
"fmt"
"time"
)

func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
fmt.Print("数据传入, 并阻塞当前线程等待接收方接收\n")
//通道c, 没有缓冲区, 故也同样可以说是: 因"通道已满"无法写入
//而阻塞等待。(反正没缓存空间, 通道实刻都是满的, 只能实时等
//待对方接收来释放空间。)
c <- sum // 把 sum 发送到通道 c
fmt.Println("阻塞解除,接收方已接收数值", sum)
}

func main() {
s := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)
go sum(s[:len(s)/2], c) // 注意,通道默认不带缓冲区, 必须时发时收的, 因此需要并发执行
go sum(s[len(s)/2:], c) // 注意,通道默认不带缓冲区, 必须时发时收的, 因此需要并发执行

time.Sleep(100000)
fmt.Println("接收倒计时: 3")
time.Sleep(100000)
fmt.Println("接收倒计时: 2")
time.Sleep(100000)
fmt.Println("接收倒计时: 1")
time.Sleep(100000)
fmt.Println("开始接收")

x, y := <-c, <-c // 从通道 通道 中实时接收值

fmt.Printf("...\n...\n")
fmt.Println("接收完毕, 下方是接收者x,y变量值的打印信息")
fmt.Println(x, y)
time.Sleep(100000) // 主线程短暂延时, 保证其他线程先退出
}

/////////////////////////////////////////////////////////////////
// 运行结果 (其中的任意一种)
数据传入, 并阻塞当前线程等待接收方接收
接收倒计时: 3
数据传入, 并阻塞当前线程等待接收方接收
接收倒计时: 2
接收倒计时: 1
开始接收
...
...
接收完毕, 下方是接收者x,y变量值的打印信息
-5 17
阻塞解除,接收方已接收数值 17
阻塞解除,接收方已接收数值 -5

运行结果种数太多, 文章只给出一个,其他结果不再列举, 这里直接说结果:

  • 现象:
    • 此例子中使用了无缓存通道, 因此必须通过协程并发来保证 “发送同时,必须有接受者”, 否则报错。
    • 此例子中两个输入者协程一定阻塞在 , “开始接收”之前
    • 此例子中两个输入者协程一定在”接受完毕后””解除阻塞”,继续执行其剩余代码
  • 结论: 通道是自带锁机制的(比如互斥锁,同步锁,读写锁等…)。

    因此, 如果本例子中没有采用并发的形式,当执行到向通道发送值的代码行实,直接就因阻塞卡死了。 编译器会提前报错来避免这种现象的。

8.2.3、带缓冲区的通道

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

ch := make(chan int, 100)

前面我们说了, 通道是自带互斥锁的, 那若是我们的通道有了缓冲区后, 其临界区会如何呢,如下所述:

  • 带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态

    就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

  • 如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内
  • 如果缓冲区没有数据, 接收方在有值可以接收之前会一直阻塞。
  • 缓冲区的大小是有限的, 如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)

// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2

// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}

/////////////////////////////////////////////////
// 运行结果
1
2

8.2.4、通道的遍历与关闭

可以通过关闭通道close(channelName), 从而实现有限通道的遍历, 代码如下:

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
package main

import (
"fmt"
)

func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c) // 关闭通道
}

func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}

上面只是简单举个例子, 至于 channel关闭 的相关特点(注意事项), 有如下几条:

  • channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;

  • 关闭channel后,无法向channel 再发送数据(若违规发送,将引发 panic 错误后导致接收立即返回零值);

  • 关闭channel后,可以继续从channel接收数据;

  • 对于nil channel,无论收发都会被阻塞。(因此使用时一定要通过make(chan type, 缓冲大小值)来定义通道)

8.2.5、select

select 是 Go 中的一个控制结构,类似于对C语言中的select与epoll这种I/O多路复用的轮询机制的模仿, 不过Go中select是用来轮询通道(channel)是否可用的, 常配合一个无限的for循环来使用。(若对C不了解, 可以把go的select类比为用于通信的 switch 语句。每个 case 必须是一个IO通道操作,要么是发送要么是接收), 大致结构如下:

1
2
3
4
5
6
7
8
9
10
for {
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
}

select可等待多个通道操作。将goroutinechannelselect结合是Go语言的一个强大功能。

也就是说, 一般单流程下(即 单go程下)只能监控一个channel的状态; 而用了select后可以完成用一个go程来监控多个channel的状态

示例代码:

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
package main

import (
"fmt"
)

// 斐波那契数列
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}

func main() {
c := make(chan int)
quit := make(chan int)

// 获取数列中的前6个元素
go func() {
// 循环结束后,会造成通道c的阻塞
for i := 0; i < 6; i++ {
fmt.Println(<-c)
}
// 此时像阻塞的通道quit写入值
quit <- 0
}()

fibonacci(c, quit)
}

///////////////////////////////////////
// 运行结果
1
1
2
3
5
8
quit

在 golang 中,谁也无法保证某些情况下的 select 是否会永久阻塞。很多时候都需要设置一下 select 的超时时间,可以借助 time 包的 After() 实现select超时时间的设置。参考自< Golang time after >

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
"fmt"
"time"
)

var c chan int

func handle(int) {
fmt.println("123456")
}

func main() {
select {
case m := <-c:
handle(m)
case <-time.After(10 * time.Second):
fmt.Println("timed out")
}
}

//////////////////////////////////////////
// 运行结果
timed out
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
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
// 激活一个goroutine,但5秒之后才发送数据
go func() {
time.Sleep(5 * time.Second)
ch1 <- "put value into ch1"
}()
select {
case val := <-ch1:
fmt.Println("recv value from ch1:",val)
return
// 只等待3秒,然后就结束
case <-time.After(3 * time.Second):
fmt.Println("3 second over, timeover")
}
}

//////////////////////////////////////////
// 运行结果
3 second over, timeover

9、反射

本小节(以及其他小节中的部分内容)转载自刘丹冰原创文章<8小时转职Golang工程师>[10]

可返回本文前面的小节中<1.2.2.6.2、将’结构体标签’配合’反射’来使用>,查看反射结构体标签配合使用的示例。

9.1、编程语言中反射的概念

在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。

每种语言的反射模型都不同,并且有些语言根本不支持反射。Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的只要包含这个包就可以使用

多插一句,Golang的gRPC也是通过反射实现的。

9.2、interface 和 反射

在讲反射之前,先来看看Golang关于类型设计的一些原则

  • 变量包括(type, value)两部分
  • type 包括 static typeconcrete type. 简单来说 static type是你在编码是看见的类型(如intstring),concrete typeruntime系统看见的类型
  • 类型断言能否成功,取决于变量的concrete type,而不是static type. 因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.

接下来要讲的反射,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说

在Golang的实现中,每个interface变量都有一个对应pairpair中记录了实际变量的类型和值:

1
<type, value>    // pair

type是实际变量的类型, value是实际变量值。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。

例如,创建类型为*os.File的变量,然后将其赋给一个接口变量r

1
2
3
4
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

var r io.Reader
r = tty

接口变量rpair中将记录如下信息:<value: tty, type: *os.File>,这个pair在接口变量的连续赋值过程中是不变的,将接口变量r赋给另一个接口变量w:

1
2
var w io.Writer
w = r.(io.Writer)

接口变量wpairrpair相同,都是:<value: tty, type: *os.File>,即使w是空接口类型,pair也是不变的。

interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"io"
"os"
)


func main() {
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) // "/dev/tty" 代表终端
if err != nil {
fmt.Println("open file error", err)
return
}

var r io.Reader
r = tty

var w io.Writer
w = r.(io.Writer)
w.Write([]byte("HELLO THIS IS A TEST!!!\n")) // 这里实际调用的是"/dev/tty"终端的Write函数
}

再比如:

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
package main

import "fmt"

type Reader interface {
ReadBook()
}

type Writer interface {
WriteBook()
}

//具体类型
type Book struct {
}

func (this *Book) ReadBook() {
fmt.Println("Read a book.")
}

func (this *Book) WriteBook() {
fmt.Println("Write a book.")
}

func main() {
// b: pair<type:Book, value:book{}地址>
b := &Book{}

var r Reader
// r: pair<type:Book, value:book{}地址>
r = b

r.ReadBook()

var w Writer
// w: pair<type:Book, value:book{}地址>
w = r.(Writer) // 此处可以断言成功。 是因为 w r 具体的type是一致的
w.WriteBook() // 此处调用的 是 Book 的WriteBook
}

9.3、reflect

reflect的基本功能TypeOf和ValueOf

既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解释

1
2
3
4
5
6
7
8
9
10
11
12
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero
func ValueOf(i interface{}) Value {...}

//ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

//TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"reflect"
)

func main() {
var num float64 = 1.2345

fmt.Println("type: ", reflect.TypeOf(num))
fmt.Println("value: ", reflect.ValueOf(num))
}

运行结果:
type: float64
value: 1.2345

说明

  1. reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
  2. reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1 “Allen.Wu” 25} 这样的结构体struct的值
  3. 也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种

从relfect.Value中获取接口interface的信息

当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。

已知原有类型【进行“强制转换”】

已知类型后转换为其对应的类型的做法如下,直接通过Interface方法然后强制转换,如下:

1
realValue := value.Interface().(已知的类型)

示例如下:

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
package main

import (
"fmt"
"reflect"
)

func main() {
var num float64 = 1.2345

pointer := reflect.ValueOf(&num)
value := reflect.ValueOf(num)

// 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic
// Golang 对类型要求非常严格,类型一定要完全符合
// 如下两个,一个是*float64,一个是float64,如果弄混,则会panic
convertPointer := pointer.Interface().(*float64)
convertValue := value.Interface().(float64)

fmt.Println(convertPointer)
fmt.Println(convertValue)
}

运行结果:
0xc42000e238
1.2345

说明

  1. 转换的时候,如果转换的类型不完全符合,则直接panic,类型要求非常严格!
  2. 转换的时候,要区分是指针还是指
  3. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”

未知原有类型【遍历探测其Filed】

很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其Filed来得知,示例如下:

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
package main

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

func (u User) ReflectCallFunc() {
fmt.Println("Allen.Wu ReflectCallFunc")
}

func main() {

user := User{1, "Allen.Wu", 25}

DoFiledAndMethod(user)

}

// 通过接口来获取任意参数,然后一一揭晓
func DoFiledAndMethod(input interface{}) {

getType := reflect.TypeOf(input)
fmt.Println("get Type is :", getType.Name())

getValue := reflect.ValueOf(input)
fmt.Println("get all Fields is:", getValue)

// 获取方法字段
// 1. 先获取interface的reflect.Type,然后通过NumField进行遍历
// 2. 再通过reflect.Type的Field获取其Field
// 3. 最后通过Field的Interface()得到对应的value
for i := 0; i < getType.NumField(); i++ {
field := getType.Field(i)
value := getValue.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}

// 获取方法
// 1. 先获取interface的reflect.Type,然后通过.NumMethod进行遍历
for i := 0; i < getType.NumMethod(); i++ {
m := getType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}

运行结果:
get Type is : User
get all Fields is: {1 Allen.Wu 25}
Id: int = 1
Name: string = Allen.Wu
Age: int = 25
ReflectCallFunc: func(main.User)

说明

通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:

  1. 先获取interface的reflect.Type,然后通过NumField进行遍历
  2. 再通过reflect.Type的Field获取其Field
  3. 最后通过Field的Interface()得到对应的value

通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:

  1. 先获取interface的reflect.Type,然后通过NumMethod进行遍历
  2. 再分别通过reflect.Type的Method获取对应的真实的方法(函数)
  3. 最后对结果取其Name和Type得知具体的方法名
  4. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
  5. struct 或者 struct 的嵌套都是一样的判断处理方式

9.3.1、通过reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

示例如下:

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
package main

import (
"fmt"
"reflect"
)

func main() {

var num float64 = 1.2345
fmt.Println("old value of pointer:", num)

// 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
pointer := reflect.ValueOf(&num)
newValue := pointer.Elem()

fmt.Println("type of pointer:", newValue.Type())
fmt.Println("settability of pointer:", newValue.CanSet())

// 重新赋值
newValue.SetFloat(77)
fmt.Println("new value of pointer:", num)

////////////////////
// 如果reflect.ValueOf的参数不是指针,会如何?
pointer = reflect.ValueOf(num)
//newValue = pointer.Elem() // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}

运行结果:
old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77

说明

  1. 需要传入的参数是* float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针。
  2. 如果传入的参数不是指针,而是变量,那么
    ○ 通过Elem获取原始值对应的对象则直接panic
    ○ 通过CanSet方法查询是否可以设置返回false
  3. newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
  4. reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的
  5. 也就是说如果要修改反射类型对象,其值必须是“addressable”【对应的要传入的是指针,同时要通过Elem方法获取原始值对应的反射对象】
  6. struct 或者 struct 的嵌套都是一样的判断处理方式

9.3.2、通过reflect.ValueOf来进行方法的调用

这算是一个高级用法了,前面我们只说到对类型、变量的几种反射的用法,包括如何获取其值、其类型、如果重新设置新值。但是在工程应用中,另外一个常用并且属于高级的用法,就是通过reflect来进行方法【函数】的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定

示例如下:

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
package main

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

func (u User) ReflectCallFuncHasArgs(name string, age int) {
fmt.Println("ReflectCallFuncHasArgs name: ", name, ", age:", age, "and origal User.Name:", u.Name)
}

func (u User) ReflectCallFuncNoArgs() {
fmt.Println("ReflectCallFuncNoArgs")
}

// 如何通过反射来进行方法的调用?
// 本来可以用u.ReflectCallFuncXXX直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调动mv.Call

func main() {
user := User{1, "Allen.Wu", 25}

// 1. 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
getValue := reflect.ValueOf(user)

// 一定要指定参数为正确的方法名
// 2. 先看看带有参数的调用方法
methodValue := getValue.MethodByName("ReflectCallFuncHasArgs")
args := []reflect.Value{reflect.ValueOf("wudebao"), reflect.ValueOf(30)}
methodValue.Call(args)

// 一定要指定参数为正确的方法名
// 3. 再看看无参数的调用方法
methodValue = getValue.MethodByName("ReflectCallFuncNoArgs")
args = make([]reflect.Value, 0)
methodValue.Call(args)
}


运行结果:
ReflectCallFuncHasArgs name: wudebao , age: 30 and origal User.Name: Allen.Wu
ReflectCallFuncNoArgs

说明

  1. 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
  2. reflect.Value.MethodByName这.MethodByName,需要指定准确真实的方法名字,如果错误将直接panic,MethodByName返回一个函数值对应的reflect.Value方法的名字。
  3. []reflect.Value,这个是最终需要调用的方法的参数,可以没有或者一个或者多个,根据实际参数来定。
  4. reflect.Value的 Call 这个方法,这个方法将最终调用真实的方法,参数务必保持一致,如果reflect.Value’Kind不是一个方法,那么将直接panic。
  5. 本来可以用u.ReflectCallFuncXXX直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调用methodValue.Call

9.4、Golang的反射reflect性能

Golang的反射很慢,这个和它的API设计有关。在 java 里面,我们一般使用反射都是这样来弄的。

1
2
3
Field field = clazz.getField("hello");
field.get(obj1);
field.get(obj2);

这个取得的反射对象类型是 java.lang.reflect.Field。它是可以复用的。只要传入不同的obj,就可以取得这个obj上对应的 field。

但是Golang的反射不是这样设计的:

1
2
type_ := reflect.TypeOf(obj)
field, _ := type_.FieldByName("hello")

这里取出来的 field 对象是 reflect.StructField 类型,但是它没有办法用来取得对应对象上的值。如果要取值,得用另外一套对object,而不是type的反射

1
2
type_ := reflect.ValueOf(obj)
fieldValue := type_.FieldByName("hello")

这里取出来的 fieldValue 类型是 reflect.Value,它是一个具体的值,而不是一个可复用的反射对象了,每次反射都需要malloc这个reflect.Value结构体,并且还涉及到GC。

Golang reflect慢主要有两个原因

  1. 涉及到内存分配以及后续的GC;
  2. reflect实现里面有大量的枚举,也就是for循环,比如类型之类的.

9.5、总结

上述详细说明了Golang的反射reflect的各种功能和用法,都附带有相应的示例,相信能够在工程应用中进行相应实践,总结一下就是:

  • 反射可以大大提高程序的灵活性,使得interface{}有更大的发挥余地
    • 反射必须结合interface才玩得转
    • 变量的type要是concrete type的(也就是interface变量)才有反射一说
  • 反射可以将“接口类型变量”转换为“反射类型对象”
    • 反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
  • 反射可以将“反射类型对象”转换为“接口类型变量
    • reflect.value.Interface().(已知的类型)
    • 遍历reflect.Type的Field获取其Field
  • 反射可以修改反射类型对象,但是其值必须是“addressable”
    • 想要利用反射修改对象状态,前提是 interface.data 是 settable,即 pointer-interface
  • 通过反射可以“动态”调用方法
  • 因为Golang本身不支持模板,因此在以往需要使用模板的场景下往往就需要使用反射(reflect)来实现

9.6、反射的基本原理

10、其他

10.1、defer关键字

defer关键字通常用于清洁目的。 defer关键字将函数或语句的执行推迟到调用函数的结尾。 [8]

实际作用再封闭函数在右花括号}后面,晚于return执行。如果在执行封闭功能期间发生错误,也同样不影响该命令最终的执行。

总结:

  • 如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。

  • defer会在return之后执行。

  • 如果在执行封闭功能期间发生错误,也同样不影响该命令最终的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
)

func print1(s string) {
fmt.Println(s)
}

func print2(s string) {
fmt.Println(s)
}

func main() {
defer print1("Hi...")
print2("there")
}

//////////////////////////////////////
// 运行结果
there
Hi...

10.2、错误处理

10.2.1、fmt.Errorf( )

fmt.Errorf()[官方] 根据格式说明符进行格式化,并将字符串作为满足错误的值返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
const name, id = "bueller", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)
fmt.Println(err.Error())
aa := fmt.Errorf("user %s (id %s) not found", name, err)
fmt.Println(aa.Error())
os.Exit(1)// 常与此函数共用(参数为0代表成功, 非0则终止进程),用于发生错误时手动结束进程
}

//////////////////////////////////////////////////////
// 运行结果
user "bueller" (id 17) not found
user bueller (id user "bueller" (id 17) not found) not found

如果想在报错时直接退出程序, 不如直接使用 log.Fatalf()

10.2.2、log.Fatalf( )

log.Fatalf[官方] 等价于 Print(),然后调用 os.Exit(1)。

10.2.3、os.IsExist 与 os.IsNotExist

os.IsNotExist 不等于 !os.IsExist[9]; 虽是乍一看好像是互补的两个函数, 但是它们的功能完全不同, 看下面解释即可了解全貌:

假设您正在尝试创建一个文件。您期望的行为是文件不存在,因此如果文件存在,则会引发相应的错误,您可以使用 os.IsExist 来检查它。

相反,os.IsNotExist 检查文件不存在时可能发生的错误。

10.3、Go modules

10.3.1、介绍

Go modules 是 Go 语言的依赖解决方案,发布于 Go1.11,成长于 Go1.12,丰富于 Go1.13,正式于 Go1.14 推荐在生产上使用。

Go moudles 目前集成在 Go 的工具链中,只要安装了 Go,自然而然也就可以使用 Go moudles 了,而 Go modules 的出现也解决了在 Go1.11 前的几个常见争议问题:

  1. Go 语言长久以来的依赖管理问题。
  2. “淘汰”现有的 GOPATH 的使用模式。
  3. 统一社区中的其它的依赖管理工具(提供迁移功能)。

GOPATH已被Gomodules淘汰,故本文不再对其做过多解释: 只需要知道, GOPATH时代常用的go get 命令在GoModules的加持下,变得更加好用了(即 若项目是通过go mod init初始化后的, 则在执行go get时,会自动配置两个文件go.mod和go.sum文件的内容, 这两个文件(go.mod和go.sum文件)用于管理当前项目的所有依赖)。

可以说,现在如果使用Go语言(从1.14版本起), 就离不开GoModules。

10.3.2、go mod 命令

命令 作用
go mod init 生成 go.mod 文件
go mod download 下载 go.mod 文件中指明的所有依赖
go mod tidy 整理现有的依赖, 添加新的依赖, 删除未使用的依赖
go mod graph 查看现有的依赖结构
go mod edit 编辑 go.mod 文件
go mod vendor 导出项目所有的依赖到vendor目录
go mod verify 校验一个模块是否被篡改过
go mod why 查看为什么需要依赖某模块

其中用到最多的就是go mod init这个命令了。其次是go mod tidy。同时我们可以使用go help mod调出上述表格内容, 查看帮助信息。

10.3.3、go mod的相关环境变量

可以通过go env命令来进行查看

1
2
3
4
5
6
7
8
$ go env
GO111MODULE="auto"
GOPROXY="https://proxy.golang.org,direct"
GONOPROXY=""
GOSUMDB="sum.golang.org"
GONOSUMDB=""
GOPRIVATE=""
...

对于每个变量的作用不再进行详细的说明, 这里只介绍我们需要做的设置以及为什么这样设置:

  • GO111MODULE -> 是否开启go modules模式

    • 建议go V1.14之后, 都设置成on
      • 命令go env -w GO111MODULE=on
      • 还可以在用户脚本中该配置文件, 在文件底部添加export GO111MODULE=on
  • GOPROXY -> 项目的第三方依赖库的下载源地址,默认是https://proxy.golang.org,direct(需代理)

    • 建议设置为国内地址
      • 七牛云 -> https://goproxy.cn,direct
      • 阿里云 -> https://mirrors.aliyun.com/goproxy/,direct
    • direct -> 此参数用于指示Go在设置地址中找不到模块时,自动回源到模块版本的源地址去抓取(比如 GitHub 等)
  • GOSUMDB -> 用来校验拉取的第三方库是否是完整的

    • 默认也是国外的网站, 如果设置了GOPROXY, 此项就不用设置了
  • GONOPROXY -> 代表私有依赖库的下载地址, 通过设置GOPRIVATE即可。

  • GONOSUMDB -> 代表私有依赖库的校验地址, 通过设置GOPRIVATE即可。

  • GOPRIVATE -> 代表私有依赖库地址,”对于用到私有依赖库的项目,配置此项即可”(不用去配置GONOSUMDBGONOPROXY)

    • go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"
      • 依赖库的值是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个
        • 即 git.example.com 和 github.com/eddycjy/mquote 是我们设置的两个私有仓库; 故这两个私有的依赖不会进行GOPROXY地址的下载和校验
      • 如果不想每次都重新设置,我们也可以利用通配符,例如: go env -w GOPRIVATE="*.example.com"

10.3.4、初始化项目

命令:

go mod init 包名称

“包名称”用于项目代码文件的import使用 -> 当前项目中所有本地”package名”, 需要文件之间互相依赖调用时,需要在书写代码前这样引入模块 import "包名称"/"package名"

  • 如果你的项目是开源项目,一般将”包名称”设置成当前项目所在的github仓库地址, 如 github.com/用户名/仓库名
  • 如果你的项目是私有的, 一般将”包名称”设置成当前项目的”自定义项目名称”
  • 如果你的项目名称没有想好, 可随意设置”包名称”, 一般同项目根目录名。 可在想好名称后,删除原有的go.mod以及so.sum文件, 重新使用go mod init 包名称设置项目名。

go mod init命令会在项目中生成go.mod文件来自动管理项目依赖。(自动管理依赖的文件有两个go.modgo.sum,第二个在初次go get时生成)

已初始化的项目可以使用 go get 命令自动配置相关依赖。 -> 使用后, 会自动对go.mod文件以及go.sum文件做相应配置。(如果没有go.sum文件,会自动生成并配置)

10.4、网络编程

10.4.1、介绍

有网络编程基础的同志直接跳过即可

主要的形式为套接字(socket)编程, 当前的网络时代,可以说网络中的进程通信无处不在”一切皆socket”。

常用的 Socket 类型有两种:流式Socket(SOCK_STREAM)和 数据报式 Socket(SOCK_DGRAM)。

无论那种语言, 都会去遵从协议标准来实现这两种主流的套接字(Socket)通信, “其中TCP所属类型为面向连接的流式套接字“, “UDP所属类型为无连接的数据报式套接字“。

不管是TCP还是UDP, 在现有的体系下, 往往被狭义分地为客户端(Client)和服务端(Server),就是我们常说的C/S。近些年,基于Http协议的B/S架构服务器已渐渐成为主流, 而Http协议就是基于TCP协议之上的应用层协议

有兴趣的朋友可以按照协议标准,自己动手基于TCP实现一个HTTP协议的web服务端框架,来作编程训练哈。 现阶段,无论哪种编程语言, 都已经有非常成熟的HTTP框架供大家使用了。

浏览器(一个承载http协议的TCP客户端)的 前端(web端),即为大前端( 随着硬件性能瓶颈的突破, 其浏览器自身的天然跨平台优势发挥的淋漓尽致, 除去少数对客户端性能有要求的场景, 几乎一切场景都可以简单的通过web端来满足, 而伴随着chrome浏览器内核魔改的nodejs发布,js也可以写服务端代码了,甚至这种通过浏览器内核映射的部分宿主操作系统API,使得js也可以直接打包客户端程序, 总之这一系列技术支撑, 使得个人全栈门槛大大降低, 也正式拉开了大前端的时代序幕 )

  • TCP由于需要建立连接, 因此Server端往往需要在绑定Listen后先建立连接, 成功accept连接后, 开开始真正地收发通信数据, 随后地通信过程没有必要时刻包含用于回发的addr信息

    • server端, 需要有固定的服务的Listen来实时监听将要到来的连接请求, 并在按照协议完成三次握手后,accept这个连接 ( c语言中会返回代表连接通信可以收发数据的文件描述符”fd”< type: int >, go语言中会返回代表连接通信可以收发数据的”conn”< type: net.Conn > )
    • client端, 只需要提供需要请求的服务器addr信息, 即可选择对应协议的socket来发起连接请求, 获得 “conn” < type: net.Conn >
  • UDP由于无连接地数据报特性,因此Server端往往在绑定addr后直接Listen监听通信数据, 其直接Listen得到的 “conn” < type: net.Conn >并非一个固定连接的”conn”, 因此其中包含有对端(Client)的addr信息, 我们可以基于此信息回发数据。

    • server端, 需要有固定的服务的Listen来实时监听将要到来的数据信息, 并返回承载此信息的 “conn”< type: net.UDPConn > ), 此信息中含有对端(Client)的addr信息,用于回发
    • client端, 只需要提供需要请求的服务器addr信息, 即可选择对应协议的socket来发起连接请求, 获得 “conn” < type: net.UDPConn >

10.4.2、go语言网络编程示例

go语言的套接字通信中(无论是TCP还是UDP), 拿到conn后, 就可以去处理相关业务逻辑了。

这些业务逻辑的代码, 在TCP中, 常常被我们工程师自定义封装到一个业务处理函数”handler(conn net.Conn)”中, 交给其他线程(go程)去并发处理(往往新建一个go程, 或直接扔给已有线程池, 需具体问题具体抉择)。

一般为了降低新go程的创建与GC损耗, 高并发的场景中常常利用对应的池化复用技术来解决(线程池)

c语言中往往需要自己写线程池,或是找到其他人分享的开源线程池代码, 而go中有一个转移对象池类,官方的解决了这个需求

  • TCP 拿到的conn 是一个个固定连接的conn, 通过并发机制来面向此连接收发数据即可。

    • server TCP

      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(){
      listener, err := net.Listen("tcp", "ip:port")
      for{
      conn, err := listener.Accept()
      go process(conn)
      }
      }


      func process(conn net.Conn) {
      defer conn.Close()
      for {
      if n, err := conn.Read(buf[:]); err != nil{
      fmt.Printf("read from connect failed, err: %v\n", err)
      break
      }
      // ...
      // 业务代码
      // ...
      if _,err = conn.Write([]byte("Send From Server")); err != nil{
      fmt.Printf("write to client failed, err: %v\n", err)
      break
      }
      }
      }

      // 当然,TCP的 conn 也是可以获取对端地址信息的。
      Addr := conn.RemoteAddr().String()

    • client TCP

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // 下面是伪代码

      conn, err := net.Dial("tcp", "ServerIP:port")
      defer conn.Close()
      for{
      if _, err = conn.Write([]byte("要发送的数据")); err != nil {
      fmt.Printf("write failed , err : %v\n", err)
      break

      var recvData = make([]byte, 1024)
      if _, err = conn.Read(recvData); err != nil{
      fmt.Printf("Read failed , err : %v\n", err)
      break
      }
      }
      }
  • UDP 拿到的conn 是一个无固定连接的conn, 但伴随其绑定端口接收的数据中, 会带有发送者的addr信息, 回发时填入对应的addr信息即可针对性回发。对于短消息,数据报机制天生无需考虑并发,单个循环足够; 若消息处理需要涉及时间过长的运算,就会造成回发前的阻塞时间过长, 此时可以考虑建立多个UDPConn,来并发处理,或直接将此业务改用TCP。

    • server UDP

      udp常用server

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 下面是伪代码

      addr, err := net.ResolveUDPAddr("udp", "Ip:Port")
      conn, err := net.ListenUDP("udp", addr) // 第二个参数为, 上一步解析出的addr, 返回的conn为单个带有addr的 net.UDPAddr 数据报, 处理此绑定端口下的所有消息。
      defer conn.Close()
      for{
      data := make([]byte, 1024)
      _, rAddr, err := conn.ReadFromUDP(data)
      //...
      // 业务代码
      //...
      _, err = conn.WriteToUDP([]byte("回发的数据"), rAddr)
      }

      udp多线程并发server不作举例, 建议直接上Tcp

    • client UDP

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // 下面是伪代码

      conn, err := net.Dial("udp", "serverIP:Port")
      defer conn.Close()
      for{
      if _, err := conn.Write([]byte("需要发送给服务端的数据")); err != nil{
      fmt.Println("Wrtie err =", err)
      return
      }
      data := make([]byte, 1024)
      if _, err = conn.Read(data); err != nil{
      fmt.Println("Read err =", err)
      return
      }
      }

10.5、go语言安装

10.5.1、 Linux系统中如何安装

  • 下载

  • 通过WinScp(或其它ftp工具), 将文件移动到 Linux 服务器中。

  • 进入对应目录, 使用此命令解压 tar -zxvf go1.21.6.linux-amd64.tar.gz -C /usr/local/

    -C 后跟的是解压到的目录

  • 在 家目录 下, 编辑 .bashrc 文件, 在其末尾添加如下信息:

    1
    2
    3
    4
    5
    6
    7
    # 设置Go语言的路径
    export GOROOT=/usr/local/go
    export GOPATH=$HOME/go
    export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

    # 启动Go Modules
    export GO111MODULE=on
  • 使用 source .bashrc 来刷新配置

  • 使用 go version 检查是否配置成功

  • 使用 go env -w GOPROXY=”https://goproxy.cn,direct" 将包代理配置成国内的七牛云。

自此, go语言就安装完毕了

10.5.1、 Win系统中如何安装以及更新

安装: 直接在官网下载安装包即可, 全部按照默认安装完毕即可。

更新: 直接在官网下载最新版本(或你需要版本)的安装包即可, 安装过程中会提示你选择卸载已有的go版本, 选择卸载就好, 之后依旧是全部选择默认。

10.6、 go语言中获取环境变量

在 Go 语言中,os.Getenv 函数会按照以下顺序查找环境变量:

  1. 首先,它会检查当前进程的环境变量。如果在当前进程中设置了该环境变量(即通过os.Setenv设置的值),os.Getenv 将返回该值。
  2. 如果当前进程中没有设置该环境变量,它会继续检查用户级别的环境变量。如果在用户级别设置了该环境变量,os.Getenv 将返回该值。
  3. 如果在用户级别也没有设置该环境变量,它会继续检查系统级别的环境变量。如果在系统级别设置了该环境变量,os.Getenv 将返回该值。
  4. 如果在系统级别和用户级别都没有设置该环境变量,os.Getenv 将返回一个空字符串。

因此,如果你在 Go 项目中使用了 .env 文件来存储环境变量,那么它的优先级会低于 os.Setenv 函数设置的环境变量,但高于系统级别和用户级别的环境变量。

注意 .env 文件需要使用 godotenv 这个项目库的Load()函数来加载使用(“github.com/joho/godotenv” 项目 已验证安全)

1
2
3
4
5
6
7
8
import "github.com/joho/godotenv"

// 从 .env 文件中读取环境变量
err := godotenv.Load()
if err != nil {
fmt.Println("无法加载 .env 文件:", err)
return
}

10.7、 go语言中的日志库

在go1.21之前, 我们往往使用zap 或 zerolog 等第三方日志库, 虽然不是官方的, 但是它们也是足够优秀的。
如zerolog在性能方面无语伦比, 甚至1.21版本后的go官方slog库, 都没有zerolog的性能高, zerolog要快slog大约2倍。
如slog库, 主要就是借鉴zap来开发的, 旨在比zap更简洁的api, 以及更高的性能, 并且slog也确实做到了这两点。

目前, 我的项目已经全部改用slog, 下面说下使用。

引入

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
// logger/logger.go
package logger

import (
"log/slog"
"os"
)

// 仅输出到 标准输出
// var Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))

// 仅输出到 文件
// var Logger *slog.Logger

// func init() {
// // 打开一个文件用于写入日志
// file, err := os.OpenFile("log.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
// if err != nil {
// slog.Error("Failed to open log file", "err", err)
// os.Exit(1)
// }

// // 创建一个新的 Handler,将其输出重定向到文件
// handler := slog.NewJSONHandler(file, nil)

// // 使用新的 Handler 创建 Logger
// Logger = slog.New(handler)
// }

// 通过环境变量LOG_MODE来判断, 输出到 标准输出 还是 文件
var Logger *slog.Logger

func init() {
var handler slog.Handler

// 打开一个文件用于写入日志
file, err := os.OpenFile("log.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
slog.Error("Failed to open log file", "err", err)
os.Exit(1)
}

// 根据环境变量的值来决定日志的输出位置
// (debug release两个选项可选,默认只要不显式指定相关环境变量为release, 就默认为debug模式--即debug是默认不需要显式指定的)
if os.Getenv("LOG_MODE") == "release" {
// 生产环境:日志只输出到文件
handler = slog.NewJSONHandler(file, nil)
} else {
// 开发环境:日志输出到标准输出和文件
handler = slog.NewJSONHandler(os.Stdout, nil)
}

Logger = slog.New(handler)
}

使用

1
2
3
4
5
6
7
8
9
// otherfile.go
package main

import "myproject/logger"

func otherFunction() {
// 在 otherFunction 中使用全局的 Logger
logger.Logger.Info("Info message from otherFunction")
}

带 文件 以及 行数等详细信息的使用方式如下:

可能带来一定的性能损耗, 但是这个相比于带来的好处完全可以忽略不计:

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

import (
"log/slog"
"runtime"
)

func main() {
_, file, line, _ := runtime.Caller(0)
slog.Info("Info message", "file", file, "line", line)
}

因此, 我们可以自定义我们的slog输出如下:

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
// 自定义的 Debug 函数
func Debug(msg string, keyvals ...interface{}) {
_, file, line, _ := runtime.Caller(1)
keyvals = append([]interface{}{"file", file, "line", line}, keyvals...)
Logger.Debug(msg, keyvals...)
}

// 自定义的 Info 函数
func Info(msg string, keyvals ...interface{}) {
_, file, line, _ := runtime.Caller(1)
keyvals = append([]interface{}{"file", file, "line", line}, keyvals...)
Logger.Info(msg, keyvals...)
}

// 自定义的 Warn 函数
func Warn(msg string, keyvals ...interface{}) {
_, file, line, _ := runtime.Caller(1)
keyvals = append([]interface{}{"file", file, "line", line}, keyvals...)
Logger.Warn(msg, keyvals...)
}

// 自定义的 Error 函数
func Error(msg string, keyvals ...interface{}) {
_, file, line, _ := runtime.Caller(1)
keyvals = append([]interface{}{"file", file, "line", line}, keyvals...)
Logger.Error(msg, keyvals...)
}

如果对应Info和Debug, 你不想让其在生产环境中打印的信息太详细, 可以进一步通过环境变量来控制

本人不在乎这点性能损耗, 更想要详细的信息。因此一般不怎么用这种方式。

1
2
3
4
5
6
7
8
9
10
// 自定义的 Info 函数
func Info(msg string, keyvals ...interface{}) {
if os.Getenv("LOG_MODE") != "release" {
_, file, line, _ := runtime.Caller(1)
keyvals = append([]interface{}{"file", file, "line", line}, keyvals...)
}
Logger.Info(msg, keyvals...)
}
// 自定义的 Debug 函数
// ...

在 slog 中,日志级别主要有以下几种:

请注意,slog 默认的日志级别是 Info,也就是说,默认情况下,Debug 级别的日志信息是不会被输出的。如果你想改变日志级别,你需要自定义 Handler 来实现日志级别的判断。

此外,slog 不支持 Fatal API,也就是说,你不能使用 slog 来终止进程。 比如, 可以将其配合 os.Exit(1)来实现Error输出后, 直接终止进程这一需求。

  • Debug:用于输出调试信息。这是最低的日志级别,会输出所有日志信息。
  • Info:用于输出一般信息。这是默认的日志级别,只会输出 Info 级别及以上的日志信息。
  • Warn:用于输出警告信息。会输出 Warn 级别及以上的日志信息。
  • Error:用于输出错误信息。会输出 Error 级别及以上的日志信息。

我们可以自行指定所需要的日志级别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// logger.go
package main

import (
"log/slog"
"os"
)

func main() {
var programLevel = new(slog.LevelVar)
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})
slog.SetDefault(slog.New(h))
slog.Debug("hello", "标题", "路多辛的博客")

programLevel.Set(slog.LevelDebug)
slog.Debug("hello2", "标题2", "路多辛的博客")
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// logger.go
package main

import (
"log/slog"
"os"
)

var ProgramLevel *slog.LevelVar

func main() {
ProgramLevel = new(slog.LevelVar)
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})
slog.SetDefault(slog.New(h))
slog.Debug("hello", "标题", "路多辛的博客")

programLevel.Set(slog.LevelDebug)
slog.Debug("hello2", "标题2", "路多辛的博客")
}

11、go开发零碎

11.1、引用第三方库时, 若遇到bug, 如何使用pr中未被作者合入主分支的修复

当我们做开发时, 难免引入一些第三方库, 那么当这些库的维护者最近刚好再忙, 而你刚好发现这个库的一些问题, 且这个问题刚好被其它人通过pr修复了, 只是还未合入主分支的情况下。我们如何在其合入主分支前, 利用这个修复bug的pr, 来从本地解决遇到的问题呢。

如果仅是测试这个pr或是暂时使用, 没想要协助维护反馈上游的话, 没必要在github上创建此库的Fork仓库,我们只需要将这个仓库克隆下来, 本地使用即可。

本地解决

步骤如下:

  1. 克隆存储库
    如果你还没有克隆存储库,可以使用以下命令:

    1
    2
    git clone <repository_url>
    cd <repository_directory>
  2. 获取PR的分支

    找到PR的分支名称,然后切换到该分支。你可以在GitHub上的PR页面找到分支名称。使用以下命令切换到PR分支:

    1
    2
    git fetch origin pull/<PR_number>/head:<branch_name>
    git checkout <branch_name>
  3. 在你的项目中使用本地库

    在你的项目中,修改go.mod文件,将依赖指向你本地的库(一般添加在go.mod文件的末尾)。例如:

    1
    replace github.com/username/repository => path_to_local_repository

    注意:

    • 此路径最好用系统的绝对路径。
    • linux和win上, 对于路径的使用格式在此处需要区分。

      如在win上, /d/safe/beep/beep是错误的路径, 正确的路径应该使用 D:/safe/beep/beep

  4. 更新依赖

    在你的项目根目录下运行以下命令,以确保你的项目使用的是本地库的最新版本:

    1
    go mod tidy
  5. 测试更改

    运行你的项目,看看PR的更改是否解决了你的问题。你可以运行项目中的测试,或者手动检查功能是否正常。

  6. 提交反馈

    如果PR解决了你的问题,可以在GitHub上给PR作者提供反馈,感谢他们的帮助。如果还有问题,可以在PR页面上继续讨论。

当项目方的主分支完成合并后, 重新依赖项目方仓库

是的,你的import文件不需要修改,只需要修改go.mod文件即可。

当作者合并了这个PR后,你可以按照以下步骤切换回官方主分支:

  1. 移除replace指令

    将这一行删除。 或注释掉。

  2. 更新依赖

    在你的项目根目录下运行以下命令,以确保你的项目使用的是官方主分支的最新版本:

    1
    go mod tidy
  3. 验证更改
    运行你的项目,确保一切正常。

这样,你的项目就会使用官方主分支的最新版本。

参考

声明: 本篇是多篇文章的转载的整合文, 无法单一标明链接。 故本站按奉行协议, 在此标明所有的内容来源链接。

在本篇的整合过程中,所发现不完美的地方,均会按自认更深层次的理解及叙述对其做出修复重构。整合之外,也有一些自己的原创内容(通篇占比不高于30%)。

非商业化转载,请附上本文链接 及下方的 参考链接。商业话转载, 请自行自行考究下方参考链接中,内容的原始出处, 并与对应内容的原作者沟通版权授予义务。

[1] Go语言编程规范

[2] Go语言入门系列(二)之基础语法总结

[3] go的指针跟c的指针区别

[4] Go语言入门系列(三)之数组和切片

[5] Go 语言教程 | 菜鸟教程

[6] Go 语言教程 | CodingDict

[7] Go 结构体标签

[8] Go 基础教程 | 无涯教程

[9] Golang Error Checkers: os.IsExist vs os.IsNotExist

[10] 8小时转职Golang工程师

[11] Go 语言教程 | 嗨客网