Go 语言学习笔记


简介

这里我想先介绍一下我学习 Go 语言的初衷,在 2019 秋招的时候,我接到了很多非常不错的面试机会,但由于我的计算机网络和操作系统的基础有点差(本人在校期间参加 ACM 竞赛,所以数据结构和算法并不是我的弱点),这些机会都从我的手中溜走,面经中的确有很多关于这方面的专题,但不知道为什么,总是说服不了自己去背下来(可能就是懒),在左右思量之后,决定放弃秋招,踏踏实实的补习一下这方面的知识。

之所以借助 Go 语言来深刻理解计算机网络和操作系统,我也是经过多方面考虑的(至于这条道路选的合不合适,我也不知道):

  • 我在找工作时选的是后端方向,语言选的是 C++,但是我对C++ 某些语言特性的理解真的是惨不忍睹(甚至上升到“三观不和”地步),我分析原因:可能是对语言特性所面对的问题场景,没有什么直观的感受。这种问题场景的缺失,在学习时是很致命的,光背概念,抓不到核心问题,费时费力,性价比太低;

  • 在面试的时候,不只一次听到大厂们(腾讯、字节、美团……)或多或少在使用 Go 语言,而且貌似培训机构对于 Go 语言的培训还不是很多,似乎有个空子(陷阱)等我去钻(掉);

  • C++ 关于初学项目的太少太老太难了,Go 语言比较简单(目前来看,且侧重点也正是我所想要学习的操作系统和计算机网络的实战结合),找资料参考时应该比较方便,还有一点,也是最重要的一点,我实在搞不明白、甚至有点排斥 C++ 的内存管理机制,而且对于内存、并发等教程,网上的资料实在不敢恭维(除了针对面经所介绍的,关于具体场景的运用也很少),而 Go 语言直接在其官方文档中写的很清楚。

  • 在这里我还要再啰嗦两句,每种语言都有其各自的特性,所针对解决的问题也各有优劣,我并不是想说 C++ 没有Go 语言好之类的言论,我真正想表达的主旨是经过各方面的考虑之后,我决定以 Go 语言作为现阶段的选择。

Go 语言环境配置

进入官网下载,下载 Linux 环境下的包:https://studygolang.com/dl

例如,我下载的包为go1.10.3.linux-amd64.tar.gz

  1. cd进入你用来存放安装包的目录,然后解压到 /usr/local,会得到go文件夹:

    1
    tar -C /usr/local -zxvf  go1.10.3.linux-amd64.tar.gz
  2. 添加/usr/loacl/go/bin目录到PATH变量中。添加到/etc/profile$HOME/.profile都可以(通过echo $HOME可以得到具体路径)

  1. 使用 vim 打开 /etc/profile,没有的话可以用命令sudo apt-get install vim安装一个

    1
    sudo vim /etc/profile
  1. 进入之后按 i进入插入模式,在文件的最后插入:

    1
    2
    export GOROOT=/usr/local/go
    export PATH=$PATH:$GOROOT/bin

    Esc退出插入模式,并且输入:wq(保存并退出)

  1. 退出 vim 之后,输入:

    1
    source /etc/profile
  2. 执行go version,如果现实版本号,则Go环境安装成功。

    1
    2
    ant@PC:~$ go version
    go version go1.10.3 gccgo (Ubuntu 8.3.0-6ubuntu1~18.04.1) 8.3.0 linux/amd64

语言特性

1.编译型语言

Go 使用编译器来编译代码。编译器将源代码编译成二进制(或字节码)格式;在编译代码时,编译器检查错误、优化性能并输出可在不同平台上运行的二进制文件。要创建并运行 Go 程序,程序员必须执行如下步骤。

  1. 使用文本编辑器创建 Go 程序;
  2. 保存文件;
  3. 编译程序;
  4. 运行编译得到的可执行文件。

Go 自带了编译器,无须单独安装编译器。

2.优势

  • Go语言在:编译速度、执行效率、开发难度,这 3 个条件之间做到了最佳的平衡。

  • Go语言支持交叉编译,你可以在运行 Linux 系统的计算机上开发可以在 Windows 上运行的应用程序。

  • Go 语言是第一门完全支持 UTF-8 的编程语言,这不仅体现在它可以处理使用 UTF-8 编码的字符串,就连它的源码文件格式都是使用的 UTF-8 编码。

  • Go语言从本质上(程序和结构方面) 来实现并发编程。

  • Go语言没有类和继承的概念,它通过接口(interface)的概念来实现多态性。

  • Go语言有一个清晰易懂的轻量级类型系统, 在类型之间也没有层级之说。 因此可以说Go语言是一门混合型的语言。

  • Go语言的语法规则严谨,没有歧义,更没什么花里胡哨的用法。

  • 并发模型:Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。

  • 内存分配:Go 选择了 tcmalloc,它本就是为并发而设计的高性能内存分配组件。

  • 垃圾回收:当前版本的垃圾回收算法也只能说堪用,离好用尚有不少距离。

  • 静态链接…

  • 标准库:Go 标准库虽称不得完全覆盖,但也算极为丰富。

  • 工具链:完整的工具链对于日常开发极为重要。Go 在此做得相当不错,无论是编译、格式化、错误检查、帮助文档,还是第三方包下载、更新都有对应的工具。其功能未必完善,但起码算得上简单易用。

3.相关项目

  • Docker

    Docker 是一种操作系统层面的虚拟化技术,可以在操作系统和应用程序之间进行隔离,也可以称之为容器。Docker 可以在一台物理服务器上快速运行一个或多个实例。例如,启动一个 CentOS 操作系统,并在其内部命令行执行指令后结束,整个过程就像自己在操作系统一样高效。

    项目链接:https://github.com/docker/docker

  • Go语言

    Go语言自己的早期源码使用C语言和汇编语言写成。从 Go 1.5 版本后,完全使用Go语言自身进行编写。

    项目链接:https://github.com/golang/go

  • beego

    beego 是一个类似 Python 的 Tornado 框架,采用了 RESTFul 的设计思路,使用Go语言编写的一个极轻量级、高可伸缩性和高性能的 Web 应用框架。

    项目链接:https://github.com/astaxie/beego

4.Go语言适合做什么

目前国外很多云平台都是采用Go语言开发的。

  • 服务器编程,以前大家如果使用 C 或者 C++ 做的那些事情,用 Go 来做也很合适,例如处理日志、数据打包、虚拟机处理、文件系统等。
  • 分布式系统、数据库代理器、中间件等,例如 Etcd。
  • 网络编程,这一块目前应用最广,包括 Web 应用、API 应用、下载应用,而且 Go 内置的 net/http 包基本上把我们平常用到的网络功能都实现了。
  • 数据库操作
  • 开发云平台,目前国外很多云平台在采用 Go 开发

使用Go语言原生开发项目的出现。

  • 云计算基础设施领域,代表项目:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储等。
  • 基础软件,代表项目:tidb、influxdb、cockroachdb 等。
  • 微服务,代表项目:go-kit、micro、monzo bank 的 typhon、bilibili 等。
  • 互联网基础设施,代表项目:以太坊、hyperledger 等。

5.Go语言四个编译阶段

Go的编译器在逻辑上可以被分成四个阶段:

  • 词法与语法分析
  • 类型检查
  • 中间代码生成
  • 机器代码生成

6.Go语言工程结构

Go语言是一门推崇软件工程理念的编程语言,它为开发周期的每个环节都提供了完备的工具和支持。Go语言高度强调代码和项目的规范和统一,这集中体现在工程结构或者说代码体制的细节之处。接下来我们来详述Go语言的工程结构。

(1) 工作区

一般情况下,Go语言的源码文件必须放在工作区中。对于命令源码文件来说,这不是必需的。

工作区其实就是一个对应于特定工程的目录,它应包含 3 个子目录:src 目录pkg 目录bin 目录

(2) GOPATH

(3) 源码文件

(4) 包的概念、导入与可见性

你必须在源文件中的第一行(非注释)来指明这个文件属于哪个包,如:package main

所有包名都应该使用小写字母。

package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main的包。

一个应用程序可以包含不同的包,而且即时你只使用 main包也不必把所有的代码都写在一个巨大的文件里:你可以用一些较小的文件,并且在每个文件中的第一行都使用 package main来指明这些文件都属于 main 包。

如果你打算编译包名不是 main的源文件,如pack1,编译后产生的对象文件将会是pack1.a而不是可执行程序。

实用语法查询

Go语言语法类似于C语言,C语言的有些语法会让代码可读性降低甚至发生歧义。Go语言在C语言的基础上取其精华,弃其糟粕,将C语言中较为容易发生错误的写法进行调整,做出相应的编译提示。

  • 去掉表达式冗余括号

  • 强制的代码风格

    Go语言中,左括号必须紧接着语句不换行。其他样式的括号将被视为代码编译错误。这个特性刚开始会使开发者有一些不习惯,但随着对Go语言的不断熟悉,开发者就会发现风格统一让大家在阅读代码时把注意力集中到了解决问题上,而不是代码风格上。

    同时Go语言也提供了一套格式化工具。一些Go语言的开发环境或者编辑器在保存时,都会使用格式化工具对代码进行格式化,让代码提交时已经是统一格式的代码。

0.标准库

下表列出了Go语言标准库中常见的包及其功能。

Go语言标准库包名 功 能
bufio 带缓冲的 I/O 操作
bytes 实现字节操作
container 封装堆、列表和环形列表等容器
crypto 加密算法
database 数据库驱动和接口
debug 各种调试文件格式访问及调试功能
encoding 常见算法如 JSON、XML、Base64 等
flag 命令行解析
fmt 格式化操作
go Go语言的词法、语法树、类型等。可通过这个包进行代码信息提取和修改
html HTML 转义及模板系统
image 常见图形格式的访问及生成
io 实现 I/O 原始访问接口及访问封装
math 数学库
net 网络库,支持 Socket、HTTP、邮件、RPC、SMTP 等
os 操作系统平台不依赖平台操作封装
path 兼容各操作系统的路径操作实用函数
plugin Go 1.7 加入的插件系统。支持将代码编译为插件,按需加载
reflect 语言反射支持。可以动态获得代码中的类型信息,获取和修改变量的值
regexp 正则表达式封装
runtime 运行时接口
sort 排序接口
strings 字符串转换、解析及实用函数
time 时间接口
text 文本模板及 Token 词法器

1.命名规则

<0> 文件名

Go 的源文件以 .go 为后缀名存储在计算机中,这些文件名 均由小写字母 组成,如 test.go 。如果文件名由多个部分组成,则使用下划线 _ 对它们进行分隔,如 scanner_test.go 。文件名不包含空格或其他特殊字符。

<1> 标识符(变量名)

有效的标识符必须以字母(可以使用任何 UTF-8 编码的字符或 _)开头,然后紧跟着 0 个或多个字符或 Unicode 数字,如:X56、group1、_x23、i、өԑ12。

2.基本类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool     // 布尔类型

string // 字符串类型

int int8 int16 int32 int64 // 整数型
uint uint8 uint16 uint32 uint64 uintptr

float32 float64 // 浮点型

byte // uint8 的别名

rune // int32 的别名
// 表示一个 Unicode 码点

complex64 complex128 // 复数类型

Go语言和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。这样做的好处就是可以避免像C语言中那样含糊不清的声明形式,例如:int* a, b; 。其中只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写。而在 Go 中,则可以和轻松地将它们都声明为指针类型:var a, b *int没有歧义。

<0> 初始化

当一个变量被声明之后,所有的内存在 Go 中都是经过初始化的,系统自动赋予它该类型的零值:

  • int 为 0
  • float 为 0.0
  • bool 为 false
  • string 为空字符串
  • 指针、切片、函数变量的默认值为 nil
标准初始化格式

变量声明可以包含初始值,每个变量对应一个。

1
var i, j int = 1, 2

如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。

1
var c, python, java = true, false, "no!"

<1> 声明变量

  • 标准格式:声明变量的一般形式是使用 var 关键字:

    1
    var name type

    其中,var 是声明变量的关键字,name 是变量名,type 是变量的类型,行尾无须分号。

  • 批量格式:使用关键字 var 和括号,可以将一组变量定义放在一起。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var (
    a int
    b string
    c []float32
    d func() bool
    e struct {
    x int
    }
    )
  • 简短格式赋值:可使用更加简短的变量定义和初始化语法

    1
    2
    名字 := 表达式
    i, j := 0, 1
    1
    2
    3
    4
    func main() {
    x:=100
    a,s:=1, "abc"
    }

    需要注意的是,简短模式有以下限制:

    • 定义变量,同时显式初始化
    • 不能提供数据类型
    • 只能用在函数内部

<2> 匿名变量

为了增加代码的灵活性,而产生了匿名变量。

匿名变量的特点是一个下画线__本身就是一个特殊的标识符,被称为空白标识符。

匿名变量不占用内存空间,不会分配内存。

任何类型都可以赋值给它,但任何赋给这个标识符的值都将被抛弃,从某种角度上可以说,它起到了一个占位符的功能。

匿名变量与匿名变量之间也不会因为多次声明而无法使用。

<3> 常量

定义常量的标准语法格式:

1
const name [type] = value

type 可写可不写;例如可以直接写为:

1
2
const str = "hello world"
int num = 1

可在全局环境中定义;

常量不能用 :=语法;

常量可以是字符、字符串、布尔值或数值。

<4> 指针

Go 拥有指针。指针保存了值的内存地址。

类型 *T 是指向 T 类型值的指针。其零值为 nil

1
var p *int

& 操作符会生成一个指向其操作数的指针。

1
2
i := 42
p = &i

* 操作符表示指针指向的底层值。

1
2
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i

与 C 不同,Go 没有指针运算,可以进行简单的指针操作,亲测之后,可以使用多重指针。

<5> 结构体

别说话,照着这样写/用,就对了

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

import "fmt"

type Pointer struct {
X int
Y int
}

func main() {
position := Pointer{1, 2}

pointer := &position

fmt.Println(position.X)
fmt.Println(pointer.Y)
}

链表的创建和遍历

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"

type node struct{
val int
next *node
}


func Build_List(head *node) {
var father, son *node
father = head;

for i := 0; i <=5 ; i++ {
son = &node{val:i}
father.next = son
father = son
}
}

func Trav_List(head *node) {
var q *node
q = head.next

for q != nil {
fmt.Printf("%d ", q.val)
q = q.next
}
}


func main() {
head := &node{}
Build_List(head)
Trav_List(head)
}

<6> 数组

类型 [n]T 表示拥有 nT 类型的值的数组。

表达式

1
var a [10]int

会将变量 a 声明为拥有 10 个整数的数组。

demo

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

import "fmt"

func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)

primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}

<7> 切片

在这里,我要着重提一下,虽然目前为止我还没有想到切片的具体应用场景(除了可能在处理报文段时我能过觉得会用到),但我觉得还是不要给自己挖坑,仔细探讨一下切片和数组的区别。

切片的主要功能:通过两个下标来选择截取的范围,范围为 [low,high),包括第一个元素,但排除最后一个元素。

1
a[low : high]

对于数组,我们理论上来说只需要知道这个数组的起始地址、数据间隔、终止地址 就可以对它进行任何操作。在明白了这三个基本要素后,后面就会很好理解,切片的做法并不是直接拷贝一段数据到另一块内存区域中,而是直接使用数组的内存地址进行操作,这和引用很类似。切片会在数组的基础上根据要求自己定义起始地址终止地址,但还会保存数组的终止地址,切片可以直接访问自己起始地址终止地址范围的值,但在进行切片的切片时它的访问范围会扩大至切片的起始地址到数组的终止地址。

切片有有个基本属性:长度(len)容量(cap)

长度的计算方式是:(切片的终止地址 - 切片的起始地址)/ 数据间隔

容量的计算方式是:(切片的终止地址 - 数组的起始地址)/ 数据间隔

例如:一个数组 :

1
2
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(arr, len(arr), cap(arr))

[0 1 2 3 4 5 6 7 8 9] 10 10

这个数组的长度为 10, 容量为 10;接下来我们将其进行切片;

1
2
s := arr[2:7]
fmt.Println(s, len(s), cap(s))

[2 3 4 5 6] 5 8

该切片的长度为 5, 容量为 8;

1
fmt.Printf("%p -- %p\n", &arr[2], &s[0])

0xc4200143d0 – 0xc4200143d0

但要注意 s 和 s[0] 的地址并不相同:

1
fmt.Printf("%p -- %p\n", &s, &s[0])

0xc420016080 – 0xc4200143d0

使用切片进行数值更改,更改的同时底层数组中的值也会改变;

切片可以当做一个数组使用,但比数组要灵活很多,这个就不细说了。

备注:切片文法

切片文法类似于没有长度的数组文法。

这是一个数组文法:

1
[3]bool{true, true, false}

下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:

1
[]bool{true, true, false}

<8> 用make创建切片

通过以上的了解之后,大概明白了切片确实比数组好用,但还有一个不方便之处是每次还要创建一个数组。

切片可以用内建函数 make 来创建,这也是创建动态数组的方式。

make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:

1
a := make([]int, 5)    // len(a)=5

要指定它的容量,需向 make 传入第三个参数:

1
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

<9> 向切片中追加元素

使用 append 函数可以对切片的进行元素追加。追加的元素会添加在切片的尾部,当然,这会直接改变底层数组的值,当的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组,即在底层数组够大的情况下,其引用的是底层数组(及指针是相同的),如果超出范围,会另开一个(指针用的不是同一套)。

例如:

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 main() {
arr := []int{9, 9, 9, 9, 9}

s := arr[1:2]
printSlice(s, arr)

// 添加一个空切片
s = append(s, 0)
printSlice(s, arr)

// 这个切片会按需增长
s = append(s, 1)
printSlice(s, arr)

// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s, arr)
}

func printSlice(s , arr []int) {
fmt.Printf("len=%d cap=%d %v %v\n", len(s), cap(s), s, arr)
}

len=1 cap=4 [9] [9 9 9 9 9]
len=2 cap=4 [9 0] [9 9 0 9 9]
len=3 cap=4 [9 0 1] [9 9 0 1 9]
len=6 cap=8 [9 0 1 2 3 4] [9 9 0 1 9]

<10> 映射 map

几乎每种编程语言都存在这一种数据结构:key - value 对,通过唯一的 key 找到相对应的 value。

创建:

map 的创建方式比较方便的有两种

方法一:

1
m := make(map[string]int)

例如:

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

import "fmt"

func main(){

m := make(map[string]int)

m["one"] = 1
m["two"] = 2
m["three"] = 3

fmt.Println(m)
}

map[one:1 two:2 three:3]

方法二:这种方法可以定义全局变量,但是必须两行都得写,有点麻烦

1
2
var m map[string]int
m = map[string]int{}

例如:

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

import "fmt"

var m map[string]int

func main(){

m = map[string]int{}

m["one"] = 1
m["two"] = 2
m["three"] = 3

fmt.Println(m)
}

map[two:2 three:3 one:1]

扩展:创建一个 key 为 structvalue 为 struct 的map。

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

import "fmt"

type date struct {
val int
str string
}

func main(){

m := make(map[date]date)

m[date{1,"one"}] = date{100, "Zzz..."}

fmt.Println(m)
}

map[{1 one}:{100 Zzz…}]

备注:

map 的默认排序是随机的。

删除:
1
delete(map_name, map_key)

例子:

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

import "fmt"

func main(){

m := make(map[string]int)

m["one"] = 1
m["two"] = 2
m["three"] = 3

fmt.Println(m)

delete(m, "one")

fmt.Println(m)
}

map[one:1 two:2 three:3]
map[two:2 three:3]

检测:

通过双赋值检测某个键是否存在:

1
elem, ok = m[key]

keym 中,oktrue ;否则,okfalse

key 不在映射中,那么 elem 是该映射元素类型的零值。

同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。

:若 elemok 还未声明,你可以使用短变量声明:

1
elem, ok := m[key]

例如:

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(){

m := make(map[string]int)

m["one"] = 1
m["two"] = 2
m["three"] = 3

elem, ok := m["one"]

fmt.Println(elem, ok)

elem, ok = m["four"]

fmt.Println(elem, ok)
}

1 true
0 false

<11> 列表 list

虽然目前为止,列表在实际中几乎没用过,但我觉得还是要写一下,万一以后要查着用呢?

引用的包:
1
import "container/list"
创建方式:

方式1:

1
变量名 := list.New()

方式2:

1
var 变量名 list.List
添加元素:

所添加的元素并不要求类型一致。

1
2
3
4
// 将 fist 字符串插入到列表的尾部
变量名.PushBack("fist")
// 将数值 67 放入列表
变量名.PushFront(67)
删除/插入元素:

删除函数

1
变量名.Remove(句柄)

插入函数

句柄之后插入元素

1
变量名.InsertAfter(内容, 句柄)

句柄之前插入元素

1
变量名.InsertBefore(内容, 句柄)

在链表中部删除或插入某个元素时需要获取元素句柄。具体操作,请看这个例子:

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 "container/list"

func main() {
l := list.New()

// 尾部添加
l.PushBack("canon")

// 头部添加
l.PushFront(67)

// 尾部添加后保存元素句柄
element := l.PushBack("fist")

// 在fist之后添加high
l.InsertAfter("high", element)

// 在fist之前添加noon
l.InsertBefore("noon", element)

// 使用 Remove()
l.Remove(element)
}

下表中展示了每次操作后列表的实际元素情况:

操作内容 列表元素
l.PushBack(“canon”) canon
l.PushFront(67) 67, canon
element := l.PushBack(“fist”) 67, canon, fist
l.InsertAfter(“high”, element) 67, canon, fist, high
l.InsertBefore(“noon”, element) 67, canon, noon, fist, high
l.Remove(element) 67, canon, noon, high
遍历列表:

方法:

1
2
3
for i := 列表名.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}

例子:

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

import "container/list"
import "fmt"

func main() {
l := list.New()


l.PushBack("one")
l.PushBack("two")
l.PushBack("three")

for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
}

one
two
three

3.常用语法

<1> for 循环

for 两边的括号被去掉,int 声明被简化为:=,直接通过编译器右值推导获得 a 的变量类型并声明。

注意:和 C/C++ ……之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的

1
2
3
for a := 0; a<10; a++ {
// 循环代码
}

初始化语句和后置语句是可选的。

1
2
3
4
5
sum := 1
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum)

Go 语言中没有 “while” ,但可以使用以下语法代替 “while”

1
2
3
4
5
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)

无限循环

1
2
3
4
5
6
package main

func main() {
for {
}
}

<2> if 判断

在Go语言中,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。代码如下:

1
2
3
if 表达式 {
// 表达式成立
}

if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内。

1
2
3
if v := math.Pow(x, n); v < lim {
// 表达式成立
}

if else

1
2
3
4
5
if v := math.Pow(x, n); v < lim {
// 表达式成立1
} else {
// 表达式成立2
}

<3> i++ 和 ++i

Go语言中自增只有一种写法:

1
i++

如果写成前置自增++i,或者赋值后自增a=i++都将导致编译错误。

<4> 函数

(1)基本操作

在本例中,add 接受两个 int 类型的参数。

注意类型在变量名 之后

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

import "fmt"

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

func main() {
fmt.Println(add(42, 13))
}

当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。

在本例中:

1
x int, y int

被缩写为

1
x, y int
(2)多值返回

函数可以返回任意数量的返回值。swap 函数返回了两个字符串。

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

import "fmt"

func swap(x, y string) (string, string) {
return y, x
}

func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
(3)函数值

在 go 语言中,函数也是值,这里指的值不是说返回值,而是可以将函数作为参数或将其整个函数作为值的意思。具体理解看样例:

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"
"math"
)

func compute(func_temp func(float64, float64) float64) float64 {
return func_temp(3, 4)
}

func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}

fmt.Println(hypot(5, 12))

fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}

13
5
81

在这个样例中,hypotmath.Pow 的参数类型和返回值类型都是相同的,所以可以作为 compute函数的参数,而且可以看出在 Go 语言中函数在创建时是可以没有名字的。

(4)函数闭包

对不起,我暂时对于函数闭包的理解还不是很清楚,主要的不是很理解名字和用法环境。

官方解释:

Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。

但是我的理解是:可以实现 C++ 静态局部变量的功能

例子:用函数闭包实现斐波那契数列

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"

// 返回一个“返回int的函数”
func fibonacci() func() int {
a := -1
b := 1
return func() int {
c := a + b
a = b
b = c
return c
}
}

func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}

0
1
1
2
3
5
8
13
21
34

<5> Range

for 循环的 range 形式可遍历切片或映射。

当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

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

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
1
2
3
4
5
6
7
8
9
> 2**0 = 1
> 2**1 = 2
> 2**2 = 4
> 2**3 = 8
> 2**4 = 16
> 2**5 = 32
> 2**6 = 64
> 2**7 = 128
>

<6> switch

switch 是编写一连串 if - else 语句的简便方法。它运行第一个值等于条件表达式的 case 语句。

Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。

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"
"runtime"
)

func main() {
fmt.Print("Go runs on ")
os := runtime.GOOS
switch os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}

Go runs on Linux.

<7> 方法&接口

在 Go 语言中,也有对 Java、C++ ……中类的一些功能的替代。 在 Go 中,结构体(struct)就是用来存放数据类型,没有在其内部写个方法之类的操作,但可以实现只有某种结构体才能使用的该方法的操作,本质上和类里的方法没什么不同。

以下是个简单的举例,模仿着写就行了。

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

import "fmt"

type node struct {
a, b int
}

func (temp node) add() int {
return temp.a + temp.b
}

func main() {
Zzz := node{5, 9}
fmt.Println(Zzz.add())
}

14

接口(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
package main

import "fmt"

type speaker interface{
speak()
}

type cat struct {}

type dog struct {}

func (c cat) speak() {
fmt.Println("miao~~")
}

func (d dog) speak() {
fmt.Println("wang~~")
}

func hit(x speaker) {
x.speak()
}

func main() {
var c1 cat
var d1 dog

hit(c1)
hit(d1)
}

miao~
wang

上述代码,定义了两个结构体:cat、dog 。虽然没有什么内容,但都定义了两个属于各自结构体的方法,名字都叫 speak(),现在面临的问题是,我想值用一个函数hit()就可以输出各自的叫声,但由于两个结构不同,且 Go 语言没有多态这个说法,所以实现不了。

解决上述问题就是创建一个接口(interface)类型,接口类型中包含方法的名称、参数以及返回值,但并不包含函数的具体实现。当一个结构体类型实现了某个接口中定义的所有方法后,它同时也是该接口类型的数值了。也就是说一个结构体类型既是该结构体类型也可能是一个或多个接口类型。

也许上述的代码例子与实际有些牵强,这里再举一个实际开发中会遇到的情况。一个开发项目中可能根据不同情况用到多种不同的数据库,但是对于增删改查这种同一类操作,我需要写n个针对不同数据库的函数接口就太过麻烦了。这里使用接口就会好很多。

当然,方法和接口还有很多其他的用法与细节,我在这里就不细说了,以后遇到在进行补充。

<8> defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
defer fmt.Println("world")

fmt.Println("hello")
}

hello

world

defer 栈

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

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

import "fmt"

func main() {
fmt.Println("counting")

for i := 0; i < 10; i++ {
defer fmt.Println(i)
}

fmt.Println("done")
}

counting
done
9
8
7
6
5
4
3
2
1
0

4.Go 并发

1.Go程-goroutine

Go 语言通过编译器运行时(runtime),从语言上支持了并发的特性。Go 语言的并发通过 goroutine 特性完成。goroutine 类似于线程,但是可以根据需要创建多个 goroutine 并发工作。goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。

如果单纯的往主程序中加 Go 程的话,不一定会得到你想要得到答案,例如下面的例子:

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

import "fmt"

func say(temp string, n int) {

for i := 0; i < n; i++ {
fmt.Println(i, ":", temp)
}

}

func main() {

go say("hello", 100)
say("world", 10)
}

0 : world
1 : world
2 : world
3 : world
4 : world
5 : world
0 : hello
1 : hello
2 : hello
3 : hello
4 : hello
5 : hello
6 : hello
7 : hello
8 : hello
9 : hello
10 : hello
11 : hello
12 : hello
13 : hello
6 : world
7 : world
8 : world
9 : world

如果主函数中的语句运行网络,这整个程序就会都结束,即使并行进程中的语句没有执行完也会结束。

2.信道-channel

Go 语言还提供 信道 - channel 在多个 goroutine 间进行通信。

信道是带有类型的管道,信道在使用前必须创建:

1
ch := make(chan int)

你可以通过它用信道操作符 <- 来发送或者接收值。

1
2
ch <- v     // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。

(“箭头”就是数据流的方向。)

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 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
package main

import "fmt"

func sum(arr []int, c chan int) {
sum := 0

for _, v := range arr {
sum += v
}

c <- sum
}


func main() {
arr := make([]int, 10000)
arr[0] = 1

c := make(chan int)

go sum(arr[:len(arr)/2], c)
go sum(arr[len(arr)/2:], c)

x, y := <-c, <-c

fmt.Println(x, y)
}

0 1

1 0

3.信道的缓冲

信道可以是 带缓冲 的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:

1
ch := make(chan int, 100)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

例子:

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

import "fmt"

func main() {

ch := make(chan int, 2)

ch <- 1
ch <- 2

fmt.Println(<-ch)
fmt.Println(<-ch)

}

1
2

对于以上的实例,可以理解为信道相当于一个有固定容量的队列,每次执行<-ch这类操作都是弹出队首的元素。超过这个容量或队列为空的话,都会报错。

4.range 和 close

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。

1
close(ch)

接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

1
v, ok := <-ch

之后 ok 会被设置为 false

循环 for i := range c 会不断从信道接收值,直到它被关闭。

注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

例子:

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 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)
for i := range c {
fmt.Println(i)
}
}

0
1
1
2
3
5
8
13
21
34

5.select 语句

select 语句使一个 Go 程可以等待多个通信操作。

select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

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

import "fmt"

func fibonacci(ch_1, ch_2 chan int) {
x, y := 0, 1

for {
select {
case ch_1 <- x:
x, y = y, x+y
case <- ch_2:
fmt.Println("quit")
return
}

}

}

func temp_func (ch_1, ch_2 chan int) {

for i := 0; i < 10; i++ {
fmt.Println(<-ch_1)
}

ch_2 <- 0

}

func main() {
ch_1 := make(chan int)
ch_2 := make(chan int)

go temp_func(ch_1, ch_2)

fibonacci(ch_1, ch_2)
}

0
1
1
2
3
5
8
13
21
34
quit

备注:

select 中的其它分支都没有准备好时,default 分支就会执行。

为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:

1
2
3
4
5
6
select {
case i := <-c:
// 使用 i
default:
// 从 c 中接收会阻塞时执行
}

6.互斥锁

但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?

这里涉及的概念叫做 互斥(mutual*exclusion) ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  • Lock
  • Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。

我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。

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"
"sync"
"time"
)

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}

func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}

time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}

1000