go学习
让我们先对 Go(或称 Golang )做一个小小的介绍。Go 是由谷歌工程师 Robert Griesemer、Rob Pike 和 Ken Thompson
设计的。它是一种静态类型的、编译的语言。第一个版本于 2012 年 3 月作为开源版本发布。在许多编程语言中,有许多方法来解决一个特定的问题。程序员要花很多时间去思考解决它的最佳方法。Go
却相信用较少的功能——只有一种正确的方式来解决问题,这为开发人员节省了时间,并使大型代码库易于维护。 Go 中没有像 maps
和 filters
这样的 “表达性”功能。
- “Go 是一种开源的编程语言,它使人们能够轻松地构建简单、可靠和高效的软件”。- GoLang
- “当你有增加表现力的功能时,通常会增加系统开销”—— Rob Pike
入门
Go 是由 packages(包)组成的。package main 告诉 Go 编译器,该程序被编译为可执行文件,而不是共享库。它是一个应用程序的入口点。package
main 的定义如下:
1 |
|
让我们继续前进,在 Go workspace 创建一个 main.go
文件,编写一个简单的 hello world 例子。
Workspace
Go 中的 workspace 是由环境变量 GOPATH
定义的。
你写的任何代码都要写在 workspace 里面。Go 将搜索 GOPATH
目录内的任何软件包,或者 GOROOT
目录,该目录在安装 Go
时默认设置。GOROOT
是安装 Go 的路径。
设置 GOPATH
到你想要的目录。现在,让我们把它添加到 ~/workspace
文件夹内。
1 |
|
在我们刚刚创建的 workspace 文件夹中创建 main.go
文件,其中包含以下代码。
Hello World
1 |
|
在上面的例子中,fmt
是 Go 中的一个内置包,它实现了用于格式化 I/O 输出的函数。
我们通过使用 import
关键字在 Go 中导入一个包。func main
是代码被执行的主入口点。Println
是包 fmt
中的一个函数,它为我们打印出
“hello world”。
让我们通过运行这个文件来看看。我们有两种方法可以运行 Go 命令。正如我们所知,Go 是一种编译语言,所以我们首先需要在执行之前编译它。
1 |
|
这将创建一个二进制可执行文件main
,现在我们可以运行:
1 |
|
还有一种更简单的方法来运行程序。go run
命令会编译源代码,并直接执行源码中的 main() 函数,不会在当前目录留下可执行文件。你可以简单地运行以下命令来执行该程序。
1 |
|
**注意**:要尝试本博客中提到的代码,你可以使用 https://play.golang.org
变量
Go 中的变量是明确声明的。Go 是一种静态类型的语言。这意味着在声明变量的时候会检查变量的类型。一个变量可以被声明:
1 |
|
在这种情况下,值将被设置为 0。使用下面的语法来声明和初始化一个具有不同值的变量:
1 |
|
这里的变量被自动分配为 int。我们可以对变量的声明使用一个简短定义,即:
1 |
|
我们也可以在同一行中声明多个变量:
1 |
|
数据类型
像其他编程语言一样,Go 支持各种不同的数据结构。让我们来探索其中:
整型、字符串和布尔值
支持的整型包括 int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr(无符号整型,长度跟平台相关,它的长度可以用来保存一个指针地址) 等
字符串类型存储一个字节序列。它用关键字 string
来表示和声明。
布尔值使用关键字 bool
来存储。
Go 也支持复数类型,可以用 complex64
和 complex128
来声明。
1 |
|
数组、切片和 Maps
数组是由相同数据类型的元素组成的一个序列。数组在声明时有一个固定的长度,所以它不能被扩大到超过这个长度。一个数组声明:
1 |
|
数组也可以是多维的。我们可以简单地用以下方式创建它们:
1 |
|
数组会限制数组的值发生变化,当代码运行时。数组也没有提供获取子数组的能力。 为此,Go有一种数据类型,叫做切片(slices)。
切片存储了一连串的元素,并且可以在任何时候扩展。切片声明与数组声明类似——但没有定义容量:
1 |
|
这将创建一个容量为 0、长度为 0 的切片。
也可以用容量和长度来定义切片。我们可以用下面的语法来定义它:
1 |
|
这里,切片的初始长度为 5,容量为 10。
分片是对数组的一种抽象。切片使用一个数组作为底层结构。一个片断包含三个部分:容量、长度和一个指向底层数组的指针,如下图所示:
图片源自: https://blog.golang.org/go-slices-usage-and-internals
一个切片的容量可以通过使用 append 或 copy 函数来增加。append 函数将值添加到数组的末端,如果需要的话也可以增加容量。
1 |
|
另一种增加切片容量的方法是使用 copy 函数。简单地创建另一个容量更大的片断,并将原来的切片复制到新创建的切片上:
1 |
|
我们可以创建一个切片的子切片。这可以通过以下命令简单地完成:
1 |
|
Maps 是 Go 中的一种数据类型,它将键映射到值。我们可以使用以下命令来定义一个 map:
1 |
|
m
是新的 map 变量,它的键是 string
类型, 值是 integers
类型。我们很容易在 map 上添加键值对:
1 |
|
类型转换
一种类型的数据类型可以通过类型转换转换为另一种类型。让我们看看一个简单的类型转换:
1 |
|
不是所有类型的数据类型都可以转换为另一种类型。请确保数据类型与转换的内容相匹配。
条件语句
if else
对于条件性语句,我们可以使用 if-else 语句,如下例所示。请确保大括号与条件语句在同一行。
1 |
|
switch case
Switch cases 有助于组织多个条件语句。下面的例子显示了一个简单的 siwtch 语句:
1 |
|
循环
Go 有一个循环的关键词 for
。for
循环命令用于实现不同种类的循环:
1 |
|
上面的例子类似于 C 语言中的 while 循环。
Go 中的 for 语句也可以用于普通的 for 循环:
1 |
|
Go 中的死循环:
1 |
|
Pointers (指针)
Go提供了指针。指针是用来保存一个值的地址的地方。指针是由 * 定义的。指针是根据数据的类型来定义的,例如:
1 |
|
ap
是指向一个整数类型的指针。&
操作符可以用来获取一个变量的地址。
1 |
|
指针所指向的值可以使用 *
操作符来访问:
1 |
|
在传递结构体作为参数时,或者在为定义的类型声明方法时,通常倾向于使用指针。
- 传递值时,实际上是在复制值,这意味着更多的内存。
- 通过指针,函数改变的值会反映在 方法/函数 调用者身上
例如:
1 |
|
注意:当你在尝试博客中的示例代码时,不要忘记用 package main
包含它,并在需要时导入 fmt 或其他包,如上面第一个 main.go
例子中所示。
Functions (函数)
在 main package 中定义的 main 函数是 go 程序执行的入口。更多的函数可以被定义和使用。让我们来看看一个简单的例子:
1 |
|
在上面的例子中我们可以看到,Go 函数是用 func 关键字来定义的,后面是函数名称。一个函数的 参数
需要根据其数据类型来定义,最后是返回的数据类型。
一个函数的返回值也可以在函数中预先定义:
1 |
|
这里c被定义为返回变量。所以定义的变量c会自动返回,而不需要在最后的返回语句中定义。
你也可以从一个函数中返回多个返回值,用逗号来分隔返回值。
1 |
|
方法、结构体、接口
Go并不是一种完全面向对象的语言,但通过结构体(Struct)、接口(Interface)和方法(Method),它有很多面向对象的支持和感觉。
结构体(Struct)
结构体是一种类型化的、不同字段的集合。结构体用于将数据分组。例如,如果我们想对 Person
类型的数据进行分组,我们可以定义一个人的属性,其中可能包括姓名、年龄、性别。可以使用以下语法来定义一个结构体:
1 |
|
在定义了一个人的类型结构后,现在让我们来创建一个 person:
1 |
|
我们可以很容易地用一个点(.)来访问这些数据。
1 |
|
你也可以用结构的指针直接访问其属性:
1 |
|
方法(Methods)
方法(Method)是一种特殊的函数类型,它有一个 receiver 。receiver 可以是一个值或一个指针。让我们创建一个名为 describe
的方法(Method),它有一个我们在上面的例子中创建的接收器类型的 person:
1 |
|
正如我们在上面的例子中看到的,现在可以使用点运算符来调用该方法,如 pp.describe
。请注意,receiver
是一个指针。使用指针,我们传递的是一个值的引用,所以如果我们在方法中做任何改变,都会反映在 receiver
pp中。它也不会创建一个新的对象的副本,这就节省了内存。
请注意,在上面的例子中,年龄的值被改变了,而名字的值没有改变,因为setName方法是 receiver 类型的,而 setAge 是指针类型的。
接口(Interfaces)
Go 接口(interfaces)是一个方法(methods)的集合。接口有助于将一个类型的属性组合在一起。让我们以一个接口 animal 为例:
1 |
|
animal 是一个接口(interface)类型。现在让我们创建两个不同类型的 animal,它们都实现了 animal 接口类型:
1 |
|
type cat struct {
在主函数中,我们创建一个动物类型的变量 a
。我们给动物分配一个 snake 和一个 cat 的类型,并使用 Println 来打印
a.description。由于我们在两种类型(cat 和 snake)中都以不同的方式实现了 describe 方法,我们得到了打印的动物描述。
包
我们把Go的所有代码都写在一个包里。main package 是程序执行的入口点。Go 中有很多内置包。我们一直在使用的最著名的是fmt
包。
“Go 软件包是 Go 提供的大型编程的主要机制,它们使得将一个大型项目分割成小块成为可能。”
— Robert Griesemer
Installing a package (安装一个包)
1 |
|
我们安装的软件包被保存在 GOPATH 环境变量设置的工作目录。你可以通过进入我们工作目录下的 pkg 文件夹 cd $GOPATH/pkg
来查看这些软件包。
创建自定义包
让我们先创建一个文件夹 custom_package:
1 |
|
要创建一个自定义包,我们需要首先创建一个文件夹,并加上我们需要的包名。比方说,我们要建立一个 person
包。为此,让我们在 custom_package
文件夹中创建一个名为 person
的文件夹:
1 |
|
现在让我们在这个文件夹中创建一个文件 person.go。
1 |
|
我们现在需要安装这个包,以便它可以被导入和使用。因此,让我们来安装它:
1 |
|
现在让我们回到custom_package文件夹,创建一个 main.go 文件
1 |
|
在这里,我们现在可以导入我们创建的包 person
并使用函数 Description。注意,我们在包中创建的函数 secretName
将不能被访问。在
Go 中,没有大写字母开头的方法名称将是私有的。
包文档
Go内置了对包的文档支持。运行以下命令来生成文档:
1 |
|
这将为我们的包 person 里面的描述函数生成文档。要看到这些文档,请使用以下命令运行一个网络服务器:
1 |
|
现在去URL http://localhost:8080/pkg/,看看我们刚刚创建的包的文档。
Go 内置包
fmt
该包实现了格式化的 I/O 函数。我们已经用这个包实现了向 stdout 打印的功能。
json
Go中另一个有用的包是json包。这有助于对JSON进行编码/解码。让我们举个例子,对一些 JSON 进行编码/解码:
编码
1 |
|
解码
1 |
|
当使用 unmarshal 解码 json 字节时,第一个参数是 json 字节,第二个参数是我们希望 json
被映射到的响应类型结构的地址。注意,json: "page"
将页面键映射到结构中的 PageNumber 键。
错误处理
错误是指程序中不想要的和意外的结果。比方说,我们正在对一个外部服务进行 API 调用。这个 API 调用可能是成功的,也可能是失败的。当错误类型出现时,Go
程序中的错误可以被识别。让我们看看这个例子:
1 |
|
在这里,对错误对象的 API 调用可能通过也可能失败。我们可以检查错误是否为零或存在,并相应地处理响应:
1 |
|
从函数返回自定义错误
当我们在编写自己的函数时,有些情况下会出现错误。这些错误可以在错误对象的帮助下返回:
1 |
|
大多数 Go 中内置的包,或者我们使用的外部包,都有一个错误处理的机制。所以我们调用的任何函数都有可能出现错误。这些错误绝不应该被忽视,总是在我们调用这些函数的地方优雅地处理,正如我们在上面的例子中所做的那样。
Panic
Panic 是指在程序执行过程中突然遇到的未被处理的东西。在Go中,Panic 不是处理程序中异常的理想方式。建议使用一个错误对象来代替。当
Panic 发生时,程序的执行会被停止。Panic 发生后被执行的东西是 defer。
1 |
|
Defer
Defer 是指总是在函数的末尾被执行的东西。
在上面的例子中,我们用 panic() 使程序的执行陷入 panic。正如你所注意到的,这里有一个 defer
语句,它将使程序在最后执行这一行。当我们需要在函数结束时执行一些东西时也可以使用 defer,例如关闭一个文件。
并发
Go 是在考虑到并发性的情况下建立的。Go 中的并发性可以通过 Go 协程实现,它是轻量级的线程。
Go 协程
Go 协程是可以与另一个函数并行或同时运行的函数。创建一个 Go 协程非常简单。只需在一个函数前面加上关键字 Go,我们就可以让它并行执行。Go
协程是非常轻量级的,所以我们可以创建成千上万的协程。让我们来看看一个简单的例子:
1 |
|
正如你在上面的例子中所看到的,函数 c 是一个 Go 协程,与 Go 主线程并行执行。有些时候,我们希望在多个线程之间共享资源。Go
倾向于不将一个线程的变量与另一个线程共享,因为这样会增加死锁和资源等待的可能性。还有一种方法可以在 Go 协程之间共享资源:通过
Go channels。
通道
我们可以使用通道在两个 Go 协程之间传递数据。在创建 channel 时,有必要指定该 channel 接收什么样的数据。让我们创建一个简单的字符串类型的
channel,如下所示:
1 |
|
通过这个 channel,我们可以发送字符串类型的数据。我们可以在这个 channel 中发送和接收数据:
1 |
|
接收方 channel 等待,直到发送方发送数据到 channel。
单向通道
有些情况下,我们希望 Go 程序通过 channel 接收数据,但不发送数据,反之亦然。为此,我们也可以创建一个单向 channel
。让我们来看看一个简单的例子:
1 |
|
在上面的例子中,sc
是一个 Go 协程,它只能向通道发送消息,但不能接收消息。
使用 select 为 Go 例程组织多个通道
一个函数可能有多个 channel 在等待。为此,我们可以使用一个选择(select)语句。让我们看一个例子,以了解更清楚的情况:
1 |
|
在上面的例子中,main正在等待两个 channel,c1 和 c2。通过 select case 语句,main 函数打印出,信息从它先收到的 channel 中发送出来。
带缓冲的通道
你可以在go中创建一个缓冲 channel。有了缓冲 channel,如果缓冲区满了,发送到该 channel 的消息就会被阻断。让我们看一下这个例子:
1 |
|
正如我们在上面看到的,一个 channel 接受的信息不超过2条。
为什么 Golang 会成功?
简洁性… — Rob-pike
Great
我们学习了 Go 的一些主要组成部分和特点。
- 变量、数据类型
- 数组 切片 和 maps
- 函数
- 循环和条件语句
- 指针
- 软件包
- 方法、结构体和接口
- 错误处理
- 并发 - Go 协程和通道
恭喜你,你现在对 Go 有了相当的了解。
我最有成效的一天是减少了 1000 行代码。
— Ken Thompson
不要停在这里,继续向前推进,思考一个小的应用并开始创建。
我也在 Milap Neupane
博客发布本文:学习Go,从0到1