Go 标准库秘籍(全)
原文:
zh.annas-archive.org/md5/F3FFC94069815F41B53B3D7D6E774406
译者:飞龙
前言
感谢您给予本书机会!本书是一本指南,带您了解 Go 标准库的可能性,其中包含了许多开箱即用的功能和解决方案。请注意,本书涵盖的解决方案主要是对标准库实现的简单演示以及其使用方式的说明。这些示例旨在为您提供解决特定问题的起点,而不是完全解决问题。
本书适合对象
这本书适用于那些想要加强基础并揭示 Go 标准库隐藏部分的人。本书希望读者具有 Go 的基本知识。对于一些示例,了解 HTML、操作系统和网络将有所帮助。
本书内容
第一章,与环境交互,探讨了您的代码如何与操作系统环境交互。还涵盖了使用命令行标志和参数、消耗信号以及与子进程一起工作。
第二章,字符串和其他内容,介绍了对字符串的常见操作,从简单的子字符串搜索到文本格式化为制表符。
第三章,处理数字,介绍了基本转换和数字格式化选项。还涵盖了大数字的操作以及在输出消息中正确使用复数形式。
第四章,从前从前,对时间包进行了详细讨论,包括格式化、算术运算以及给定时间段内或延迟一定时间后的代码运行。
第五章,输入和输出,涵盖了利用标准 Go 接口进行的 I/O 操作。除了基本的 I/O 外,本章还涵盖了一些有用的序列化格式以及如何处理它们。
第六章,发现文件系统,讨论了与文件系统的工作,包括列出文件夹、读取和更改文件属性,以及对比文件。
第七章,连接网络,展示了连接 TCP 和 UDP 服务器的客户端实现,以及 SMTP、HTTP 和 JSON-RPC 的使用。
第八章,与数据库工作,专注于常见的数据库任务,如数据选择和提取、事务处理和执行,以及存储过程的缺点。
第九章,来到服务器端,从服务器的角度提供了对网络的视角。介绍了 TCP、UDP 和 HTTP 服务器的基础知识。
第十章,并发乐趣,涉及同步机制和对资源的并发访问。
第十一章,技巧与窍门,提供了有用的测试和改进 HTTP 服务器实现的技巧,并展示了 HTTP/2 推送的好处。
为了充分利用本书
尽管 Go 编程平台是跨平台的,但本书中的示例通常假定使用基于 Unix 的操作系统,或者至少可以执行一些常见的 Unix 实用程序。对于 Windows 用户,Cygwin 或 GitBash 实用程序可能会有所帮助。示例代码最适合这种设置:
-
基于 Unix 的环境
-
大于或等于 1.9.2 的 Go 版本
-
互联网连接
-
在将创建和执行示例代码的文件夹上具有读取、写入和执行权限
下载示例代码文件
您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接将文件发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
登录或注册www.packtpub.com。
-
选择“支持”选项卡。
-
单击“代码下载和勘误”。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
文件下载完成后,请确保您使用最新版本的解压缩软件解压或提取文件夹:
-
Windows 的 WinRAR/7-Zip
-
Mac 的 Zipeg/iZip/UnRarX
-
Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Go-Standard-Library-Cookbook
。如果代码有更新,将在现有的 GitHub 存储库上进行更新。
我们还有来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/
上找到!快去看看吧!
使用的约定
本书中使用了许多文本约定。
CodeInText
:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“验证您的GOPATH
和GOROOT
环境变量是否设置正确。”
代码块设置如下:
package main
import (
"log"
"runtime"
)
当我们希望引起您对代码块的特定部分的注意时,相关的行或项目会以粗体显示:
package main
import (
"log"
"runtime"
)
粗体:表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。
警告或重要说明看起来像这样。
提示和技巧看起来像这样。
章节
在本书中,您会经常看到几个标题(准备工作、如何做、它是如何工作的、还有更多和另请参阅)。
为了清晰地说明如何完成一个食谱,使用以下各节:
准备工作
本节告诉您食谱中会有什么,并描述如何设置食谱所需的任何软件或任何初步设置。
如何做…
本节包含了遵循食谱所需的步骤。
它是如何工作的…
本节通常包括对前一节发生的事情的详细解释。
还有更多…
本节包括有关食谱的其他信息,以使您对食谱更加了解。
另请参阅
本节为食谱提供了其他有用信息的链接。
第一章:与环境交互
在本章中,将涵盖以下配方:
-
检索 Golang 版本
-
访问程序参数
-
使用 flag 包创建程序接口
-
获取并设置带有默认值的环境变量
-
检索当前工作目录
-
获取当前进程 PID
-
处理操作系统信号
-
调用外部进程
-
检索子进程信息
-
从子进程读取/写入
-
优雅地关闭应用程序
-
使用功能选项进行文件配置
介绍
每个程序一旦被执行,就存在于操作系统的环境中。程序接收输入并向该环境提供输出。操作系统还需要与程序通信,让程序知道外部发生了什么。最后,程序需要做出适当的响应。
本章将带您了解系统环境的发现基础知识,通过程序参数对程序进行参数化,以及操作系统信号的概念。您还将学习如何执行和与子进程通信。
检索 Golang 版本
在构建程序时,最好记录环境设置、构建版本和运行时版本,特别是如果您的应用程序更复杂。这有助于您分析问题,以防出现故障。
除了构建版本和例如环境变量之外,编译二进制文件的 Go 版本可以包含在日志中。以下的步骤将向您展示如何将 Go 运行时版本包含在程序信息中。
准备就绪
安装并验证 Go 安装。以下步骤可能有所帮助:
-
在您的计算机上下载并安装 Go。
-
验证您的
GOPATH
和GOROOT
环境变量是否正确设置。 -
打开终端并执行
go version
。如果得到带有版本名称的输出,则 Go 已正确安装。 -
在
GOPATH/src
文件夹中创建存储库。
如何做...
以下步骤涵盖了解决方案:
-
打开控制台并创建文件夹
chapter01/recipe01
。 -
导航到目录。
-
创建
main.go
文件,内容如下:
package main
import (
"log"
"runtime"
)
const info = `
Application %s starting.
The binary was build by GO: %s`
func main() {
log.Printf(info, "Example", runtime.Version())
}
-
通过执行
go run main.go
运行代码。 -
在终端中查看输出:
它是如何工作的...
runtime
包包含许多有用的函数。要找出 Go 运行时版本,可以使用 Version
函数。文档说明该函数返回提交的哈希值,以及二进制构建时的日期或标签。
实际上,Version
函数返回 runtime/internal/sys
的 Version
常量。常量本身位于 $GOROOT/src/runtime/internal/sys/zversion.go
文件中。
这个 .go
文件是由 go dist
工具生成的,版本是通过 go/src/cmd/dist/build.go
文件中的 findgoversion
函数解析的,如下所述。
$GOROOT/VERSION
优先级最高。如果文件为空或不存在,则使用 $GOROOT/VERSION.cache
文件。如果也找不到 $GOROOT/VERSION.cache
,则工具会尝试使用 Git 信息来解析版本,但在这种情况下,您需要为 Go 源代码初始化 Git 存储库。
访问程序参数
参数化程序运行的最简单方法是使用命令行参数作为程序参数。
简单地说,参数化的程序调用可能如下所示:./parsecsv user.csv role.csv
。在这种情况下,parsecsv
是执行二进制文件的名称,user.csv
和role.csv
是修改程序调用的参数(在这种情况下是要解析的文件)。
如何做...
-
打开控制台并创建文件夹
chapter01/recipe02
。 -
导航到目录。
-
创建
main.go
文件,内容如下:
package main
import (
"fmt"
"os"
)
func main() {
args := os.Args
// This call will print
// all command line arguments.
fmt.Println(args)
// The first argument, zero item from slice,
// is the name of the called binary.
programName := args[0]
fmt.Printf("The binary name is: %s \n", programName)
// The rest of the arguments could be obtained
// by omitting the first argument.
otherArgs := args[1:]
fmt.Println(otherArgs)
for idx, arg := range otherArgs {
fmt.Printf("Arg %d = %s \n", idx, arg)
}
}
-
通过执行
go build -o test
构建二进制文件。 -
执行命令
./test arg1 arg2
。(Windows 用户可以运行test.exe arg1 arg2
)。 -
在终端中查看输出:
它是如何工作的...
Go 标准库提供了几种访问程序调用参数的方法。最通用的方法是通过 OS 包中的Args
变量访问参数。
通过这种方式,您可以在字符串切片中获取命令行中的所有参数。这种方法的优点是参数的数量是动态的,这样您可以,例如,将要由程序处理的文件的名称传递给程序。
上面的示例只是回显传递给程序的所有参数。最后,假设二进制文件名为test
,程序运行由终端命令./test arg1 arg2
执行。
具体来说,os.Args[0]
将返回./test
。os.Args[1:]
返回不带二进制名称的其余参数。在现实世界中,最好不要依赖于传递给程序的参数数量,而是始终检查参数数组的长度。否则,如果给定索引上的参数不在范围内,程序将自然地发生恐慌。
还有更多...
如果参数被定义为标志,-flag value
,则需要额外的逻辑来将值分配给标志。在这种情况下,使用flag
包有更好的方法来解析这些标志。这种方法是下一个配方的一部分。
使用 flag 包创建程序接口
前面的配方描述了如何通过非常通用的方法访问程序参数。
这个配方将提供一种通过程序标志定义接口的方法。这种方法主导了基于 GNU/Linux、BSD 和 macOS 的系统。程序调用的示例可以是ls -l
,在*NIX 系统上,它将列出当前目录中的文件。
Go 标志处理包不支持像ls -ll
这样的标志组合,其中在单个破折号后有多个标志。每个标志必须是单独的。Go 标志包也不区分长选项和短选项。最后,-flag
和--flag
是等效的。
如何做到...
-
打开控制台并创建文件夹
chapter01/recipe03
。 -
导航到目录。
-
创建带有以下内容的
main.go
文件:
package main
import (
"flag"
"fmt"
"log"
"os"
"strings"
)
// Custom type need to implement
// flag.Value interface to be able to
// use it in flag.Var function.
type ArrayValue []string
func (s *ArrayValue) String() string {
return fmt.Sprintf("%v", *s)
}
func (a *ArrayValue) Set(s string) error {
*a = strings.Split(s, ",")
return nil
}
func main() {
// Extracting flag values with methods returning pointers
retry := flag.Int("retry", -1, "Defines max retry count")
// Read the flag using the XXXVar function.
// In this case the variable must be defined
// prior to the flag.
var logPrefix string
flag.StringVar(&logPrefix, "prefix", "", "Logger prefix")
var arr ArrayValue
flag.Var(&arr, "array", "Input array to iterate through.")
// Execute the flag.Parse function, to
// read the flags to defined variables.
// Without this call the flag
// variables remain empty.
flag.Parse()
// Sample logic not related to flags
logger := log.New(os.Stdout, logPrefix, log.Ldate)
retryCount := 0
for retryCount < *retry {
logger.Println("Retrying connection")
logger.Printf("Sending array %v\n", arr)
retryCount++
}
}
-
通过执行
go build -o util
来构建二进制文件。 -
从控制台执行
./util -retry 2 -prefix=example -array=1,2
。 -
在终端中查看输出:
它是如何工作的...
对于代码中的标志定义,flag
包定义了两种类型的函数。
第一种类型是标志类型的简单名称,例如Int
。这个函数将返回整数变量的指针,解析标志的值将存储在其中。
XXXVar
函数是第二种类型。它们提供相同的功能,但需要提供变量的指针。解析的标志值将存储在给定的变量中。
Go 库还支持自定义标志类型。自定义类型必须实现flag
包中的Value
接口。
例如,假设标志retry
定义了重新连接到端点的重试限制,标志prefix
定义了日志中每行的前缀,而array
是作为有效负载发送到服务器的数组标志。终端中的程序调用将如./util -retry 2 -prefix=example array=1,2
。
上述代码的重要部分是Parse()
函数,它从Args[1:]
中解析定义的标志。在定义所有标志并在访问值之前必须调用该函数。
上面的代码显示了如何从命令行标志中解析一些数据类型。类似地,其他内置类型也可以解析。
最后一个标志array
演示了自定义类型标志的定义。请注意,ArrayType
实现了flag
包中的Value
接口。
还有更多...
flag
包包含更多函数来设计带有标志的接口。值得阅读FlagSet
的文档。
通过定义新的FlagSet
,可以通过调用myFlagset.Parse(os.Args[2:])
来解析参数。这样你就可以基于第一个标志拥有标志子集。
使用默认值获取和设置环境变量
前一个教程,使用 flag 包创建程序接口,描述了如何将标志用作程序参数。
特别是对于较大的应用程序,另一种典型的参数化方式是使用环境变量进行配置。环境变量作为配置选项显著简化了应用程序的部署。这在云基础设施中也非常常见。
通常,本地数据库连接和自动构建环境的配置是不同的。
如果配置由环境变量定义,就不需要更改应用程序配置文件甚至应用程序代码。导出的环境变量(例如DBSTRING
)就是我们所需要的。如果环境变量不存在,将配置默认值也非常实用。这样,应用程序开发人员的生活就轻松多了。
本教程将演示如何读取、设置和取消设置环境变量。它还将向您展示如何在变量未设置时实现默认选项。
如何做…
-
打开控制台并创建文件夹
chapter01/recipe04
。 -
导航到目录。
-
创建
get.go
文件,内容如下:
package main
import (
"log"
"os"
)
func main() {
connStr := os.Getenv("DB_CONN")
log.Printf("Connection string: %s\n", connStr)
}
-
通过在终端中调用
DB_CONN=db:/user@example && go run get.go
来执行代码。 -
在终端中查看输出:
- 创建
lookup.go
文件,内容如下:
package main
import (
"log"
"os"
)
func main() {
key := "DB_CONN"
connStr, ex := os.LookupEnv(key)
if !ex {
log.Printf("The env variable %s is not set.\n", key)
}
fmt.Println(connStr)
}
-
在终端中调用
unset DB_CONN && go run lookup.go
来执行代码。 -
在终端中查看输出:
- 创建
main.go
文件,内容如下:
package main
import (
"log"
"os"
)
func main() {
key := "DB_CONN"
// Set the environmental variable.
os.Setenv(key, "postgres://as:[email protected]/pg?
sslmode=verify-full")
val := GetEnvDefault(key, "postgres://as:as@localhost/pg?
sslmode=verify-full")
log.Println("The value is :" + val)
os.Unsetenv(key)
val = GetEnvDefault(key, "postgres://as:[email protected]/pg?
sslmode=verify-full")
log.Println("The default value is :" + val)
}
func GetEnvDefault(key, defVal string) string {
val, ex := os.LookupEnv(key)
if !ex {
return defVal
}
return val
}
-
在终端中执行
go run main.go
来运行代码。 -
在终端中查看输出:
它是如何工作的…
环境变量可以通过os
包中的Getenv
和Setenv
函数来访问。这些函数的名称不言自明,不需要进一步的描述。
os
包中还有一个有用的函数。LookupEnv
函数提供两个值作为结果;变量的值,以及布尔值,定义变量在环境中是否设置。
os.Getenv
函数的缺点是,即使在环境变量未设置的情况下,它也会返回空字符串。
这个缺点可以通过os.LookupEnv
函数来克服,该函数返回环境变量的字符串值和一个布尔值,指示变量是否设置。
要实现检索环境变量或默认值,使用os.LookupEnv
函数。简单地说,如果变量未设置,也就是第二个返回值是false
,那么就返回默认值。该函数的使用是第 9 步的一部分。
检索当前工作目录
应用程序的另一个有用信息来源是目录,程序二进制文件所在的位置。有了这些信息,程序就可以访问与二进制文件一起放置的资源和文件。
自 Go 1.8 版本以来,本教程使用了 Go 的解决方案。这是首选方案。
如何做…
-
打开控制台并创建文件夹
chapter01/recipe05
。 -
导航到目录。
-
创建
main.go
文件,内容如下:
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
ex, err := os.Executable()
if err != nil {
panic(err)
}
// Path to executable file
fmt.Println(ex)
// Resolve the direcotry
// of the executable
exPath := filepath.Dir(ex)
fmt.Println("Executable path :" + exPath)
// Use EvalSymlinks to get
// the real path.
realPath, err := filepath.EvalSymlinks(exPath)
if err != nil {
panic(err)
}
fmt.Println("Symlink evaluated:" + realPath)
}
-
通过命令
go build -o binary
构建二进制文件。 -
通过终端调用
./binary
来执行二进制文件。 -
查看输出。它应该显示在您的机器上的绝对路径:
它是如何工作的…
自 Go 1.8 以来,os
包中的Executable
函数是解析可执行文件路径的首选方法。Executable
函数返回执行的二进制文件的绝对路径(除非返回错误)。
为了解析二进制路径的目录,应用了filepath
包中的Dir
。唯一的问题是结果可能是symlink
或它指向的路径。
为了克服这种不稳定的行为,可以使用filepath
包中的EvalSymlinks
来应用到结果路径上。通过这种方法,返回的值将是二进制文件的真实路径。
可以使用os
库中的Executable
函数获取二进制文件所在目录的信息。
请注意,如果代码是通过go run
命令运行的,实际的可执行文件位于临时目录中。
获取当前进程 PID
了解正在运行的进程的 PID 是有用的。PID 可以被操作系统实用程序用来查找有关进程本身的信息。在进程失败的情况下,了解 PID 也很有价值,这样您可以在系统日志中跟踪进程行为,例如/var/log/messages
,/var/log/syslog
。
本示例向您展示了如何使用os
包获取执行程序的 PID,并将其与操作系统实用程序一起使用以获取更多信息。
如何做…
-
打开控制台并创建文件夹
chapter01/recipe06
。 -
导航到目录。
-
使用以下内容创建
main.go
文件:
package main
import (
"fmt"
"os"
"os/exec"
"strconv"
)
func main() {
pid := os.Getpid()
fmt.Printf("Process PID: %d \n", pid)
prc := exec.Command("ps", "-p", strconv.Itoa(pid), "-v")
out, err := prc.Output()
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
-
通过执行
go run main.go
来运行代码。 -
在终端中查看输出:
工作原理…
os
包中的Getpid
函数返回进程的 PID。示例代码展示了如何从操作系统实用程序ps
获取有关进程的更多信息。
在应用程序启动时打印 PID 可能很有用,这样在崩溃时也可以通过检索到的 PID 来调查原因。
处理操作系统信号
信号是操作系统与正在运行的进程通信的基本方式。最常见的两个信号是SIGINT
和SIGTERM
。这些信号会导致程序终止。
还有一些信号,比如SIGHUP
。SIGHUP
表示调用进程的终端已关闭,例如,程序可以决定转移到后台。
Go 提供了一种处理应用程序接收到信号时的行为的方法。本示例将提供一个实现处理的示例。
如何做…
-
打开控制台并创建文件夹
chapter01/recipe07
。 -
导航到目录。
-
使用以下内容创建
main.go
文件:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// Create the channel where the received
// signal would be sent. The Notify
// will not block when the signal
// is sent and the channel is not ready.
// So it is better to
// create buffered channel.
sChan := make(chan os.Signal, 1)
// Notify will catch the
// given signals and send
// the os.Signal value
// through the sChan.
// If no signal specified in
// argument, all signals are matched.
signal.Notify(sChan,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)
// Create channel to wait till the
// signal is handled.
exitChan := make(chan int)
go func() {
signal := <-sChan
switch signal {
case syscall.SIGHUP:
fmt.Println("The calling terminal has been closed")
exitChan <- 0
case syscall.SIGINT:
fmt.Println("The process has been interrupted by CTRL+C")
exitChan <- 1
case syscall.SIGTERM:
fmt.Println("kill SIGTERM was executed for process")
exitChan <- 1
case syscall.SIGQUIT:
fmt.Println("kill SIGQUIT was executed for process")
exitChan <- 1
}
}()
code := <-exitChan
os.Exit(code)
}
-
通过执行
go run main.go
来运行代码。 -
通过按下CTRL + C发送
SIGINT
信号给应用程序。 -
查看输出:
工作原理…
在资源被获取的应用程序中,如果立即终止可能会发生资源泄漏。最好处理信号并采取一些必要的步骤来释放资源。上述代码展示了如何做到这一点的概念。
signal
包中的Notify
函数将帮助我们处理接收到的信号。
如果在Notify
函数中未指定信号作为参数,函数将捕获所有可能的信号。
请注意,signal
包的Notify
函数通过sChan
通道与goroutine
通信。Notify
然后捕获定义的信号并将其发送到goroutine
进行处理。最后,exitChan
用于解析进程的退出代码。
重要的信息是,如果分配的通道未准备好,Notify
函数将不会阻止信号。这样信号可能会被错过。为了避免错过信号,最好创建缓冲通道。
请注意,SIGKILL
和SIGSTOP
信号可能无法被Notify
函数捕获,因此无法处理这些信号。
调用外部进程
Go 二进制文件也可以用作各种实用程序的工具,并且可以使用go run
来替代 bash 脚本。出于这些目的,通常会调用命令行实用程序。
在这个示例中,将提供如何执行和处理子进程的基础知识。
准备工作
测试以下命令是否在你的终端中工作:
-
测试
ls
(Windows 中为dir
)命令是否存在于你的$PATH
中。 -
你应该能够在终端中执行
ls
(Windows 中为dir
)命令。
如何做…
以下步骤涵盖了解决方案:
-
打开控制台并创建文件夹
chapter01/recipe08
。 -
导航到目录。
-
创建
run.go
文件,内容如下:
package main
import (
"bytes"
"fmt"
"os/exec"
)
func main() {
prc := exec.Command("ls", "-a")
out := bytes.NewBuffer([]byte{})
prc.Stdout = out
err := prc.Run()
if err != nil {
fmt.Println(err)
}
if prc.ProcessState.Success() {
fmt.Println("Process run successfully with output:\n")
fmt.Println(out.String())
}
}
-
通过执行
go run run.go
来运行代码。 -
在终端中查看输出:
- 创建
start.go
文件,内容如下:
package main
import (
"fmt"
"os/exec"
)
func main() {
prc := exec.Command("ls", "-a")
err := prc.Start()
if err != nil {
fmt.Println(err)
}
prc.Wait()
if prc.ProcessState.Success() {
fmt.Println("Process run successfully with output:\n")
fmt.Println(out.String())
}
}
-
通过执行
go run start.go
来运行代码。 -
在终端中查看输出:
工作原理…
Go 标准库提供了一种简单的调用外部进程的方法。这可以通过os/exec
包的Command
函数来实现。
最简单的方法是创建Cmd
结构并调用Run
函数。Run
函数执行进程并等待其完成。如果命令退出时出现错误,err
值将不为空。
这更适合调用操作系统的实用程序和工具,这样程序不会挂起太久。
进程也可以异步执行。这可以通过调用Cmd
结构的Start
方法来实现。在这种情况下,进程被执行,但是主goroutine
不会等待它结束。Wait
方法可以用来等待进程结束。Wait
方法完成后,进程的资源将被释放。
这种方法更适合执行长时间运行的进程和程序依赖的服务。
另请参阅
这个示例描述了如何简单地执行子进程。本章还提供了检索子进程信息和从子进程读取/写入的示例,介绍了如何从子进程读取和写入,并获取有用的进程信息的步骤。
检索子进程信息
调用外部进程示例描述了如何同步和异步调用子进程。自然地,要处理进程行为,你需要更多地了解进程。这个示例展示了如何在子进程终止后获取 PID 和基本信息。
关于运行进程的信息只能通过syscall
包获得,而且高度依赖于平台。
准备工作
测试sleep
(Windows 中为timeout
)命令是否存在于终端中。
如何做…
-
打开控制台并创建文件夹
chapter01/recipe09
。 -
导航到目录。
-
创建
main_running.go
文件,内容如下:
package main
import (
"fmt"
"os/exec"
"runtime"
)
func main() {
var cmd string
if runtime.GOOS == "windows" {
cmd = "timeout"
} else {
cmd = "sleep"
}
proc := exec.Command(cmd, "1")
proc.Start()
// No process state is returned
// till the process finish.
fmt.Printf("Process state for running process: %v\n",
proc.ProcessState)
// The PID could be obtain
// event for the running process
fmt.Printf("PID of running process: %d\n\n",
proc.Process.Pid)
}
-
通过执行
go run main_running.go
来运行代码。 -
在终端中查看输出:
- 创建
main.go
文件,内容如下:
func main() {
var cmd string
if runtime.GOOS == "windows" {
cmd = "timeout"
} else {
cmd = "sleep"
}
proc := exec.Command(cmd, "1")
proc.Start()
// Wait function will
// wait till the process ends.
proc.Wait()
// After the process terminates
// the *os.ProcessState contains
// simple information
// about the process run
fmt.Printf("PID: %d\n", proc.ProcessState.Pid())
fmt.Printf("Process took: %dms\n",
proc.ProcessState.SystemTime()/time.Microsecond)
fmt.Printf("Exited sucessfuly : %t\n",
proc.ProcessState.Success())
}
-
通过执行
go run main.go
来运行代码。 -
在终端中查看输出:
工作原理…
os/exec
标准库提供了执行进程的方法。使用Command
,将返回Cmd
结构。Cmd
提供了对进程表示的访问。当进程正在运行时,你只能找到 PID。
你只能获取有关进程的少量信息。但是通过检索进程的 PID,你可以调用操作系统的实用程序来获取更多信息。
请记住,即使子进程正在运行,也可以获取其 PID。另一方面,只有在进程终止后,os
包的ProcessState
结构才可用。
另请参阅
本章中有与进程处理相关的从子进程中读取/写入和调用外部进程的配方。
从子进程中读取/写入
每个执行的进程都有标准输出、输入和错误输出。Go 标准库提供了读取和写入这些内容的方法。
本配方将介绍如何读取进程的输出并写入子进程的输入的方法。
准备就绪
验证以下命令是否在终端中工作:
-
测试终端中是否存在
ls
(Windows 中的dir
)命令。 -
您应该能够在终端中执行
ls
(Windows 中的dir
)命令。
如何做…
-
打开控制台并创建文件夹
chapter01/recipe10
。 -
导航到目录。
-
创建
main_read_output.go
文件,内容如下:
package main
import (
"fmt"
"os/exec"
"runtime"
)
func main() {
var cmd string
if runtime.GOOS == "windows" {
cmd = "dir"
} else {
cmd = "ls"
}
proc := exec.Command(cmd)
// Output will run the process
// terminates and returns the standard
// output in a byte slice.
buff, err := proc.Output()
if err != nil {
panic(err)
}
// The output of child
// process in form
// of byte slice
// printed as string
fmt.Println(string(buff))
}
-
通过执行
go run main_read_output.go
来运行代码。 -
在终端中查看输出:
- 创建
main_read_stdout.go
文件,内容如下:
package main
import (
"bytes"
"fmt"
"os/exec"
"runtime"
)
func main() {
var cmd string
if runtime.GOOS == "windows" {
cmd = "dir"
} else {
cmd = "ls"
}
proc := exec.Command(cmd)
buf := bytes.NewBuffer([]byte{})
// The buffer which implements
// io.Writer interface is assigned to
// Stdout of the process
proc.Stdout = buf
// To avoid race conditions
// in this example. We wait till
// the process exit.
proc.Run()
// The process writes the output to
// to buffer and we use the bytes
// to print the output.
fmt.Println(string(buf.Bytes()))
}
-
通过执行
go run main_read_stdout.go
来运行代码。 -
在终端中查看输出:
- 创建
main_read_read.go
文件,内容如下:
package main
import (
"bufio"
"context"
"fmt"
"os/exec"
"time"
)
func main() {
cmd := "ping"
timeout := 2 * time.Second
// The command line tool
// "ping" is executed for
// 2 seconds
ctx, _ := context.WithTimeout(context.TODO(), timeout)
proc := exec.CommandContext(ctx, cmd, "example.com")
// The process output is obtained
// in form of io.ReadCloser. The underlying
// implementation use the os.Pipe
stdout, _ := proc.StdoutPipe()
defer stdout.Close()
// Start the process
proc.Start()
// For more comfortable reading the
// bufio.Scanner is used.
// The read call is blocking.
s := bufio.NewScanner(stdout)
for s.Scan() {
fmt.Println(s.Text())
}
}
-
通过执行
go run main_read.go
来运行代码。 -
在终端中查看输出:
- 创建
sample.go
文件,内容如下:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
fmt.Println(sc.Text())
}
}
- 创建
main.go
文件,内容如下:
package main
import (
"bufio"
"fmt"
"io"
"os/exec"
"time"
)
func main() {
cmd := []string{"go", "run", "sample.go"}
// The command line tool
// "ping" is executed for
// 2 seconds
proc := exec.Command(cmd[0], cmd[1], cmd[2])
// The process input is obtained
// in form of io.WriteCloser. The underlying
// implementation use the os.Pipe
stdin, _ := proc.StdinPipe()
defer stdin.Close()
// For debugging purposes we watch the
// output of the executed process
stdout, _ := proc.StdoutPipe()
defer stdout.Close()
go func() {
s := bufio.NewScanner(stdout)
for s.Scan() {
fmt.Println("Program says:" + s.Text())
}
}()
// Start the process
proc.Start()
// Now the following lines
// are written to child
// process standard input
fmt.Println("Writing input")
io.WriteString(stdin, "Hello\n")
io.WriteString(stdin, "Golang\n")
io.WriteString(stdin, "is awesome\n")
time.Sleep(time.Second * 2)
proc.Process.Kill()
}
-
通过执行
go run main.go
来运行代码。 -
在终端中查看输出:
工作原理…
os/exec
包的Cmd
结构提供了访问进程输出/输入的函数。有几种方法可以读取进程的输出。
读取进程输出的最简单方法之一是使用Cmd
结构的Output
或CombinedOutput
方法(获取Stderr
和Stdout
)。在调用此函数时,程序会同步等待子进程终止,然后将输出返回到字节缓冲区。
除了Output
和OutputCombined
方法外,Cmd
结构提供了Stdout
属性,可以将io.Writer
分配给它。分配的写入器然后作为进程输出的目的地。它可以是文件、字节缓冲区或任何实现io.Writer
接口的类型。
读取进程输出的最后一种方法是通过调用Cmd
结构的StdoutPipe
方法获取io.Reader
。StdoutPipe
方法在Stdout
之间创建管道,进程在其中写入输出,并提供Reader
,它作为程序读取进程输出的接口。这样,进程的输出被传送到检索到的io.Reader
。
向进程的stdin
写入的方式相同。在所有选项中,将演示使用io.Writer
的方式。
可以看到,有几种方法可以从子进程中读取和写入。使用stderr
和stdin
的方式几乎与步骤 6-7 中描述的方式相同。最后,访问输入/输出的方法可以这样分为:
-
同步(等待进程结束并获取字节):使用
Cmd
的Output
和CombinedOutput
方法。 -
IO:输出或输入以
io.Writer/Reader
的形式提供。XXXPipe
和StdXXX
属性是这种方法的正确选择。
IO 类型更加灵活,也可以异步使用。
优雅地关闭应用程序
服务器和守护程序是长时间运行的程序(通常是几天甚至几周)。这些长时间运行的程序通常在开始时分配资源(数据库连接,网络套接字),并在资源存在的时间内保持这些资源。如果这样的进程被终止并且关闭未得到适当处理,可能会发生资源泄漏。为了避免这种行为,应该实现所谓的优雅关闭。
在这种情况下,优雅意味着应用程序捕获终止信号(如果可能的话),并在终止之前尝试清理和释放分配的资源。这个食谱将向您展示如何实现优雅关闭。
食谱处理操作系统信号描述了捕获操作系统信号。相同的方法将用于实现优雅关闭。在程序终止之前,它将清理并执行一些其他活动。
如何做...
-
打开控制台并创建文件夹
chapter01/recipe11
。 -
导航到目录。
-
创建
main.go
文件,内容如下:
package main
import (
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
"time"
)
var writer *os.File
func main() {
// The file is opened as
// a log file to write into.
// This way we represent the resources
// allocation.
var err error
writer, err = os.OpenFile(fmt.Sprintf("test_%d.log",
time.Now().Unix()), os.O_RDWR|os.O_CREATE, os.ModePerm)
if err != nil {
panic(err)
}
// The code is running in a goroutine
// independently. So in case the program is
// terminated from outside, we need to
// let the goroutine know via the closeChan
closeChan := make(chan bool)
go func() {
for {
time.Sleep(time.Second)
select {
case <-closeChan:
log.Println("Goroutine closing")
return
default:
log.Println("Writing to log")
io.WriteString(writer, fmt.Sprintf("Logging access
%s\n", time.Now().String()))
}
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan,
syscall.SIGTERM,
syscall.SIGQUIT,
syscall.SIGINT)
// This is blocking read from
// sigChan where the Notify function sends
// the signal.
<-sigChan
// After the signal is received
// all the code behind the read from channel could be
// considered as a cleanup.
// CLEANUP SECTION
close(closeChan)
releaseAllResources()
fmt.Println("The application shut down gracefully")
}
func releaseAllResources() {
io.WriteString(writer, "Application releasing
all resources\n")
writer.Close()
}
-
通过执行
go run main.go
运行代码。 -
按下CTRL + C发送
SIGINT
信号。 -
等待终端输出如下:
recipe11
文件夹还应包含一个名为test_XXXX.log
的文件,其中包含如下行:
它是如何工作的...
从sigChan
读取是阻塞的,因此程序会一直运行,直到通过通道发送信号。sigChan
是Notify
函数发送信号的通道。
程序的主要代码在一个新的goroutine
中运行。这样,当主函数在sigChan
上被阻塞时,工作将继续。一旦从操作系统发送信号到进程,sigChan
接收到信号并在从sigChan
通道读取的行下面的代码执行。这段代码可以被视为清理部分。
请注意,步骤 7 的终端输出包含最终日志应用程序释放所有资源
,这是清理部分的一部分。
另请参阅
有关信号捕获工作原理的详细描述在食谱处理操作系统信号中。
使用功能选项进行文件配置
这个食谱与 Go 标准库没有直接关系,但包括如何处理应用程序的可选配置。该食谱将在实际情况下使用函数选项模式与文件配置。
如何做...
-
打开控制台并创建文件夹
chapter01/recipe12
。 -
导航到目录。
-
创建
main.go
文件,内容如下:
package main
import (
"encoding/json"
"fmt"
"os"
)
type Client struct {
consulIP string
connString string
}
func (c *Client) String() string {
return fmt.Sprintf("ConsulIP: %s , Connection String: %s",
c.consulIP, c.connString)
}
var defaultClient = Client{
consulIP: "localhost:9000",
connString: "postgres://localhost:5432",
}
// ConfigFunc works as a type to be used
// in functional options
type ConfigFunc func(opt *Client)
// FromFile func returns the ConfigFunc
// type. So this way it could read the configuration
// from the json.
func FromFile(path string) ConfigFunc {
return func(opt *Client) {
f, err := os.Open(path)
if err != nil {
panic(err)
}
defer f.Close()
decoder := json.NewDecoder(f)
fop := struct {
ConsulIP string `json:"consul_ip"`
}{}
err = decoder.Decode(&fop)
if err != nil {
panic(err)
}
opt.consulIP = fop.ConsulIP
}
}
// FromEnv reads the configuration
// from the environmental variables
// and combines them with existing ones.
func FromEnv() ConfigFunc {
return func(opt *Client) {
connStr, exist := os.LookupEnv("CONN_DB")
if exist {
opt.connString = connStr
}
}
}
func NewClient(opts ...ConfigFunc) *Client {
client := defaultClient
for _, val := range opts {
val(&client)
}
return &client
}
func main() {
client := NewClient(FromFile("config.json"), FromEnv())
fmt.Println(client.String())
}
- 在同一文件夹中,创建名为
config.json
的文件,内容如下:
{
"consul_ip":"127.0.0.1"
}
-
通过命令
CONN_DB=oracle://local:5921 go run main.go
执行代码。 -
查看输出:
它是如何工作的...
函数选项模式的核心概念是配置 API 包含功能参数。在这种情况下,NewClient
函数接受各种数量的ConfigFunc
参数,然后逐个应用于defaultClient
结构。这样,可以以极大的灵活性修改默认配置。
查看FromFile
和FromEnv
函数,它们返回ConfigFunc
,实际上是访问文件或环境变量。
最后,您可以检查输出,该输出应用了配置选项和结果Client
结构,其中包含来自文件和环境变量的值。
第二章:字符串和其他内容
本章中的配方有:
-
在字符串中查找子字符串
-
将字符串分解为单词
-
使用分隔符连接字符串切片
-
使用 writer 连接字符串
-
使用 tabwriter 对齐文本
-
替换字符串的一部分
-
通过正则表达式模式在文本中查找子字符串
-
从非 Unicode 字符集解码字符串
-
控制大小写
-
解析逗号分隔的数据
-
管理字符串中的空格
-
缩进文本文档
介绍
在开发人员的生活中,对字符串和基于字符串的数据进行操作是常见任务。本章介绍如何使用 Go 标准库处理这些任务。毫无疑问,使用标准库可以做很多事情。
检查 Go 是否已正确安装。第一章的准备就绪部分,与环境交互的检索 Golang 版本配方将对您有所帮助。
在字符串中查找子字符串
在开发人员中,查找字符串中的子字符串是最常见的任务之一。大多数主流语言都在标准库中实现了这一点。Go 也不例外。本配方描述了 Go 实现这一功能的方式。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe01
。 -
导航到目录。
-
创建
contains.go
文件,内容如下:
package main
import (
"fmt"
"strings"
)
const refString = "Mary had a little lamb"
func main() {
lookFor := "lamb"
contain := strings.Contains(refString, lookFor)
fmt.Printf("The \"%s\" contains \"%s\": %t \n", refString,
lookFor, contain)
lookFor = "wolf"
contain = strings.Contains(refString, lookFor)
fmt.Printf("The \"%s\" contains \"%s\": %t \n", refString,
lookFor, contain)
startsWith := "Mary"
starts := strings.HasPrefix(refString, startsWith)
fmt.Printf("The \"%s\" starts with \"%s\": %t \n", refString,
startsWith, starts)
endWith := "lamb"
ends := strings.HasSuffix(refString, endWith)
fmt.Printf("The \"%s\" ends with \"%s\": %t \n", refString,
endWith, ends)
}
-
通过执行
go run contains.go
来运行代码。 -
在终端中查看输出:
它是如何工作的...
Go 库strings
包含处理字符串操作的函数。这次可以使用Contains
函数。Contains
函数只是检查字符串是否包含给定的子字符串。实际上,Contains
函数中使用了Index
函数。
要检查字符串是否以子字符串开头,可以使用HasPrefix
函数。要检查字符串是否以子字符串结尾,可以使用HasSuffix
函数。
实际上,Contains
函数是通过使用同一包中的Index
函数实现的。可以猜到,实际实现方式是这样的:如果给定子字符串的索引大于-1
,则Contains
函数返回true
。
HasPrefix
和HasSuffix
函数的工作方式不同:内部实现只是检查字符串和子字符串的长度,如果它们相等或字符串更长,则比较字符串的所需部分。
另请参阅
本配方描述了如何匹配精确的子字符串。通过正则表达式模式在文本中查找子字符串配方将帮助您了解如何使用正则表达式模式匹配。
将字符串分解为单词
将字符串分解为单词可能有些棘手。首先,决定单词是什么,分隔符是什么,是否有任何空格或其他字符。做出这些决定后,可以从strings
包中选择适当的函数。本配方将描述常见情况。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe02
。 -
导航到目录。
-
创建
whitespace.go
文件,内容如下:
package main
import (
"fmt"
"strings"
)
const refString = "Mary had a little lamb"
func main() {
words := strings.Fields(refString)
for idx, word := range words {
fmt.Printf("Word %d is: %s\n", idx, word)
}
}
-
通过执行
go run whitespace.go
来运行代码。 -
在终端中查看输出:
- 创建另一个名为
anyother.go
的文件,内容如下:
package main
import (
"fmt"
"strings"
)
const refString = "Mary_had a little_lamb"
func main() {
words := strings.Split(refString, "_")
for idx, word := range words {
fmt.Printf("Word %d is: %s\n", idx, word)
}
}
-
通过执行
go run anyother.go
来运行代码。 -
在终端中查看输出:
- 创建另一个名为
specfunction.go
的文件,内容如下:
package main
import (
"fmt"
"strings"
)
const refString = "Mary*had,a%little_lamb"
func main() {
// The splitFunc is called for each
// rune in a string. If the rune
// equals any of character in a "*%,_"
// the refString is split.
splitFunc := func(r rune) bool {
return strings.ContainsRune("*%,_", r)
}
words := strings.FieldsFunc(refString, splitFunc)
for idx, word := range words {
fmt.Printf("Word %d is: %s\n", idx, word)
}
}
-
通过执行
go run specfunction.go
来运行代码。 -
在终端中查看输出:
- 创建另一个名为
regex.go
的文件,内容如下:
package main
import (
"fmt"
"regexp"
)
const refString = "Mary*had,a%little_lamb"
func main() {
words := regexp.MustCompile("[*,%_]{1}").Split(refString, -1)
for idx, word := range words {
fmt.Printf("Word %d is: %s\n", idx, word)
}
}
-
通过执行
go run regex.go
来运行代码。 -
在终端中查看输出:
它是如何工作的...
将字符串拆分为单词的最简单形式考虑任何空白字符作为分隔符。具体来说,空白字符由unicode
包中的IsSpace
函数定义:
'\t', '\n', '\v', '\f', '\r', ' ', U+0085 (NEL), U+00A0 (NBSP).
strings
包的Fields
函数可以用于按空格字符拆分句子,如前面提到的。步骤1-5涵盖了这种简单情况。
如果需要其他分隔符,就需要使用Split
函数。使用其他分隔符拆分在步骤6-8中介绍。只需注意字符串中的空白字符被省略。
如果您需要更复杂的函数来决定是否在给定点拆分字符串,FieldsFunc
可能适合您。函数的一个参数是消耗给定字符串的符文并在该点返回true
的函数。这个选项由步骤9-11覆盖。
正则表达式是示例中提到的最后一个选项。regexp
包的Regexp
结构包含Split
方法,它的工作方式与您期望的一样。它在匹配组的位置拆分字符串。这种方法在步骤12-14中使用。
还有更多...
strings
包还提供了各种SplitXXX
函数,可以帮助您实现更具体的任务。
使用分隔符连接字符串切片
将字符串拆分为单词这个教程引导我们完成了根据定义的规则将单个字符串拆分为子字符串的任务。另一方面,本教程描述了如何使用给定的字符串作为分隔符将多个字符串连接成单个字符串。
一个真实的用例可能是动态构建 SQL 选择语句条件的问题。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe03
。 -
导航到目录。
-
创建
join.go
文件,内容如下:
package main
import (
"fmt"
"strings"
)
const selectBase = "SELECT * FROM user WHERE %s "
var refStringSlice = []string{
" FIRST_NAME = 'Jack' ",
" INSURANCE_NO = 333444555 ",
" EFFECTIVE_FROM = SYSDATE "}
func main() {
sentence := strings.Join(refStringSlice, "AND")
fmt.Printf(selectBase+"\n", sentence)
}
-
通过执行
go run join.go
来运行代码。 -
在终端中查看输出:
- 创建
join_manually.go
文件,内容如下:
package main
import (
"fmt"
"strings"
)
const selectBase = "SELECT * FROM user WHERE "
var refStringSlice = []string{
" FIRST_NAME = 'Jack' ",
" INSURANCE_NO = 333444555 ",
" EFFECTIVE_FROM = SYSDATE "}
type JoinFunc func(piece string) string
func main() {
jF := func(p string) string {
if strings.Contains(p, "INSURANCE") {
return "OR"
}
return "AND"
}
result := JoinWithFunc(refStringSlice, jF)
fmt.Println(selectBase + result)
}
func JoinWithFunc(refStringSlice []string,
joinFunc JoinFunc) string {
concatenate := refStringSlice[0]
for _, val := range refStringSlice[1:] {
concatenate = concatenate + joinFunc(val) + val
}
return concatenate
}
-
通过执行
go run join.go
来运行代码。 -
在终端中查看输出:
它是如何工作的...
为了将字符串切片连接成单个字符串,strings
包的Join
函数就在那里。简单地说,您需要提供需要连接的字符串切片。这样,您可以舒适地连接字符串切片。步骤1-5展示了使用Join
函数的方法。
当然,可以通过迭代切片来手动实现连接。这样,您可以通过一些更复杂的逻辑自定义分隔符。步骤6-8只是表示手动连接如何与更复杂的决策逻辑一起使用,基于当前处理的字符串。
还有更多...
Join
函数由bytes
包提供,自然用于连接字节切片。
使用写入器连接字符串
除了内置的+
运算符外,还有更多连接字符串的方法。本教程将描述使用bytes
包和内置的copy
函数更高效地连接字符串的方法。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe04
。 -
导航到目录。
-
创建
concat_buffer.go
文件,内容如下:
package main
import (
"bytes"
"fmt"
)
func main() {
strings := []string{"This ", "is ", "even ",
"more ", "performant "}
buffer := bytes.Buffer{}
for _, val := range strings {
buffer.WriteString(val)
}
fmt.Println(buffer.String())
}
-
通过执行
go run concat_buffer.go
来运行代码。 -
在终端中查看输出:
- 创建
concat_copy.go
文件,内容如下:
package main
import (
"fmt"
)
func main() {
strings := []string{"This ", "is ", "even ",
"more ", "performant "}
bs := make([]byte, 100)
bl := 0
for _, val := range strings {
bl += copy(bs[bl:], []byte(val))
}
fmt.Println(string(bs[:]))
}
-
在终端中执行
go run concat_copy.go
来运行代码。 -
在终端中查看输出:
它是如何工作的...
步骤1-5涵盖了将bytes
包Buffer
作为性能友好的字符串连接解决方案的用法。Buffer
结构实现了WriteString
方法,可以用于有效地将字符串连接到底层字节切片中。
在所有情况下都不需要使用这种改进,只需要在程序将要连接大量字符串的情况下考虑一下(例如,在内存中的 CSV 导出和其他情况)。
在步骤6 - 8中介绍的内置的copy
函数可以用于完成string
的连接。这种方法对最终字符串长度有一些假设,或者可以实时完成。然而,如果结果写入的缓冲区的容量小于已写部分和要附加的字符串的总和,缓冲区必须扩展(通常是通过分配具有更大容量的新切片)。
还有更多...
仅供比较,这里有一个基准代码,比较了内置的+
运算符、bytes.Buffer
和内置的copy
的性能:
- 在其中创建一个
bench
文件夹和文件bench_test.go
,内容如下:
package main
import (
"bytes"
"testing"
)
const testString = "test"
func BenchmarkConcat(b *testing.B) {
var str string
b.ResetTimer()
for n := 0; n < b.N; n++ {
str += testString
}
b.StopTimer()
}
func BenchmarkBuffer(b *testing.B) {
var buffer bytes.Buffer
b.ResetTimer()
for n := 0; n < b.N; n++ {
buffer.WriteString(testString)
}
b.StopTimer()
}
func BenchmarkCopy(b *testing.B) {
bs := make([]byte, b.N)
bl := 0
b.ResetTimer()
for n := 0; n < b.N; n++ {
bl += copy(bs[bl:], testString)
}
b.StopTimer()
}
- 查看基准测试的结果:
使用 tabwriter 对齐文本
在某些情况下,输出(通常是数据输出)是通过制表文本完成的,这些文本以良好排列的单元格格式化。这种格式可以通过text/tabwriter
包实现。该包提供了Writer
过滤器,它将带有制表符的文本转换为格式良好的输出文本。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe05
。 -
导航到目录。
-
创建
tabwriter.go
文件,内容如下:
package main
import (
"fmt"
"os"
"text/tabwriter"
)
func main() {
w := tabwriter.NewWriter(os.Stdout, 15, 0, 1, ' ',
tabwriter.AlignRight)
fmt.Fprintln(w, "username\tfirstname\tlastname\t")
fmt.Fprintln(w, "sohlich\tRadomir\tSohlich\t")
fmt.Fprintln(w, "novak\tJohn\tSmith\t")
w.Flush()
}
-
通过执行
go run tabwriter.go
来运行代码。 -
在终端中查看输出:
它是如何工作的...
通过调用NewWriter
函数创建具有配置参数的Writer
过滤器。由此Writer
写入的所有数据都根据参数进行格式化。这里使用os.Stdout
仅用于演示目的。
text/tabwriter
包还提供了一些更多的配置选项,比如flag
参数。最有用的是tabwriter.AlignRight
,它配置了写入器在每一列中将内容对齐到右侧。
替换字符串的一部分
与字符串处理相关的另一个非常常见的任务是在字符串中替换子字符串。Go 标准库提供了Replace
函数和Replacer
类型,用于一次替换多个字符串。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe06
。 -
导航到目录。
-
创建
replace.go
文件,内容如下:
package main
import (
"fmt"
"strings"
)
const refString = "Mary had a little lamb"
const refStringTwo = "lamb lamb lamb lamb"
func main() {
out := strings.Replace(refString, "lamb", "wolf", -1)
fmt.Println(out)
out = strings.Replace(refStringTwo, "lamb", "wolf", 2)
fmt.Println(out)
}
-
通过执行
go run replace.go
来运行代码。 -
在终端中查看输出:
- 创建
replacer.go
文件,内容如下:
package main
import (
"fmt"
"strings"
)
const refString = "Mary had a little lamb"
func main() {
replacer := strings.NewReplacer("lamb", "wolf", "Mary", "Jack")
out := replacer.Replace(refString)
fmt.Println(out)
}
-
通过执行
go run replacer.go
来运行代码。 -
在终端中查看输出:
- 创建
regexp.go
文件,内容如下:
package main
import (
"fmt"
"regexp"
)
const refString = "Mary had a little lamb"
func main() {
regex := regexp.MustCompile("l[a-z]+")
out := regex.ReplaceAllString(refString, "replacement")
fmt.Println(out)
}
-
通过执行
go run regexp.go
来运行代码。 -
在终端中查看输出:
它是如何工作的...
strings
包的Replace
函数被广泛用于简单的替换。最后一个整数参数定义了将进行多少次替换(在-1
的情况下,所有字符串都被替换。看到Replace
的第二个用法,只有前两次出现被替换)。Replace
函数的用法在步骤1 - 5中呈现。
除了Replace
函数,Replacer
结构也有WriteString
方法。这个方法将使用Replacer
中定义的所有替换写入给定的写入器。这种类型的主要目的是可重用性。它可以一次替换多个字符串,并且对并发使用是安全的;参见步骤6 - 8。
替换子字符串,甚至匹配模式的更复杂方法,自然是使用正则表达式。Regex
类型指针方法ReplaceAllString
可以用于此目的。步骤9 - 11说明了regexp
包的用法。
还有更多...
如果需要更复杂的逻辑来进行替换,那么regexp
包可能是应该使用的包。
通过正则表达式模式在文本中查找子字符串
总是有一些任务,比如验证输入、在文档中搜索信息,甚至从给定字符串中清除不需要的转义字符。对于这些情况,通常使用正则表达式。
Go 标准库包含regexp
包,涵盖了正则表达式的操作。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe07
。 -
导航到目录。
-
创建
regexp.go
文件,内容如下:
package main
import (
"fmt"
"regexp"
)
const refString = `[{ \"email\": \"[email protected]\" \
"phone\": 555467890},
{ \"email\": \"[email protected]\" \
"phone\": 555467890}]`
func main() {
// This pattern is simplified for brevity
emailRegexp := regexp.MustCompile("[a-zA-Z0-9]{1,}
@[a-zA-Z0-9]{1,}\\.[a-z]{1,}")
first := emailRegexp.FindString(refString)
fmt.Println("First: ")
fmt.Println(first)
all := emailRegexp.FindAllString(refString, -1)
fmt.Println("All: ")
for _, val := range all {
fmt.Println(val)
}
}
-
通过执行
go run regexp.go
来运行代码。 -
在终端中查看输出:
工作原理...
FindString
或FindAllString
函数是在给定字符串中查找匹配模式的最简单方法。唯一的区别是Regexp
的FindString
方法只会返回第一个匹配项。另一方面,FindAllString
会返回一个包含所有匹配项的字符串切片。
Regexp
类型提供了丰富的FindXXX
方法。本教程仅描述了通常最有用的String
变体。请注意,前面的代码使用了regexp
包的MustCompile
函数,如果正则表达式的编译失败,它会引发 panic。
另请参阅
除了这种复杂的正则表达式模式匹配,还可以仅匹配子字符串。这种方法在本章的在字符串中查找子字符串教程中有描述。
从非 Unicode 字符集解码字符串
一个鲜为人知的事实是,所有.go
文件中的内容都是用 UTF-8 编码的。信不信由你,Unicode 并不是世界上唯一的字符集。例如,Windows-1250 编码在 Windows 用户中广泛传播。
在处理非 Unicode 字符串时,需要将内容转换为 Unicode。本教程演示了如何解码和编码非 Unicode 字符串。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe08
。 -
导航到目录。
-
创建内容为
Gdańsk
的文件win1250.txt
。该文件必须以 windows-1250 字符集进行编码。如果不确定如何操作,只需跳到第 6 步,完成第 7 步后,将创建 windows-1250 编码的文件,然后可以将out.txt
文件重命名并返回第 4 步。 -
创建
decode.go
文件,内容如下:
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
"golang.org/x/text/encoding/charmap"
)
func main() {
// Open windows-1250 file.
f, err := os.Open("win1250.txt")
if err != nil {
panic(err)
}
defer f.Close()
// Read all in raw form.
b, err := ioutil.ReadAll(f)
if err != nil {
panic(err)
}
content := string(b)
fmt.Println("Without decode: " + content)
// Decode to unicode
decoder := charmap.Windows1250.NewDecoder()
reader := decoder.Reader(strings.NewReader(content))
b, err = ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
fmt.Println("Decoded: " + string(b))
}
-
通过执行
go run decode.go
来运行代码。 -
在终端中查看输出:
- 创建名为
encode.go
的文件,内容如下:
package main
import (
"io"
"os"
"golang.org/x/text/encoding/charmap"
)
func main() {
f, err := os.OpenFile("out.txt", os.O_CREATE|os.O_RDWR,
os.ModePerm|os.ModeAppend)
if err != nil {
panic(err)
}
defer f.Close()
// Decode to unicode
encoder := charmap.Windows1250.NewEncoder()
writer := encoder.Writer(f)
io.WriteString(writer, "Gdańsk")
}
-
通过执行
go run encode.go
来运行代码。 -
在 Windows-1250 编码和 UTF-8 编码的文件
out.txt
中查看输出。
工作原理...
包golang.org/x/text/encoding/charmap
包含了简单编码和解码的Charset
类型。该类型实现了创建Decoder
结构的NewDecoder
方法。
步骤1-5展示了解码Reader
的用法。
编码工作类似。创建编码Writer
,然后由该Writer
写入的每个字符串都会被编码为 Windows-1250 编码。
请注意,Windows-1250 被选择作为示例。包golang.org/x/text/encoding/charmap
包含了许多其他字符集选项。
控制大小写
有许多实际任务需要修改大小写。让我们挑选其中的一些:
-
不区分大小写的比较
-
自动首字母大写
-
驼峰式转蛇式转换
为此,strings
包提供了ToLower
、ToUpper
、ToTitle
和Title
函数。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe09
。 -
导航到目录。
-
创建
case.go
文件,内容如下:
package main
import (
"fmt"
"strings"
"unicode"
)
const email = "[email protected]"
const name = "isaac newton"
const upc = "upc"
const i = "i"
const snakeCase = "first_name"
func main() {
// For comparing the user input
// sometimes it is better to
// compare the input in a same
// case.
input := "[email protected]"
input = strings.ToLower(input)
emailToCompare := strings.ToLower(email)
matches := input == emailToCompare
fmt.Printf("Email matches: %t\n", matches)
upcCode := strings.ToUpper(upc)
fmt.Println("UPPER case: " + upcCode)
// This digraph has different upper case and
// title case.
str := "dz"
fmt.Printf("%s in upper: %s and title: %s \n", str,
strings.ToUpper(str), strings.ToTitle(str))
// Use of XXXSpecial function
title := strings.ToTitle(i)
titleTurk := strings.ToTitleSpecial(unicode.TurkishCase, i)
if title != titleTurk {
fmt.Printf("ToTitle is defferent: %#U vs. %#U \n",
title[0], []rune(titleTurk)[0])
}
// In some cases the input
// needs to be corrected in case.
correctNameCase := strings.Title(name)
fmt.Println("Corrected name: " + correctNameCase)
// Converting the snake case
// to camel case with use of
// Title and ToLower functions.
firstNameCamel := toCamelCase(snakeCase)
fmt.Println("Camel case: " + firstNameCamel)
}
func toCamelCase(input string) string {
titleSpace := strings.Title(strings.Replace(input, "_", " ", -1))
camel := strings.Replace(titleSpace, " ", "", -1)
return strings.ToLower(camel[:1]) + camel[1:]
}
-
通过执行
go run case.go
来运行代码。 -
在终端中查看输出:
它是如何工作的...
请注意,Unicode 中的标题大小写映射与大写映射不同。不同之处在于字符数需要特殊处理。这些主要是连字和双字母,如fl,dz和lj,以及一些多音调希腊字符。例如,U+01C7 (LJ)映射到U+01C8 (Lj),而不是U+01C9 (lj)。
为了进行适当的不区分大小写比较,应该使用strings
包中的EqualFold
函数。该函数使用大小写折叠来规范化字符串并进行比较。
解析逗号分隔的数据
有多种表格数据格式。CSV(逗号分隔值)是用于数据传输和导出的最基本格式之一。没有定义 CSV 的标准,但格式本身在 RFC 4180 中有描述。
这个示例介绍了如何舒适地解析 CSV 格式的数据。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe10
。 -
导航到目录。
-
创建名为
data.csv
的文件,其中包含以下内容:
"Name","Surname","Age"
# this is comment in data
"John","Mnemonic",20
Maria,Tone,21
- 创建名为
data.go
的文件,其中包含以下内容:
package main
import (
"encoding/csv"
"fmt"
"os"
)
func main() {
file, err := os.Open("data.csv")
if err != nil {
panic(err)
}
defer file.Close()
reader := csv.NewReader(file)
reader.FieldsPerRecord = 3
reader.Comment = '#'
for {
record, e := reader.Read()
if e != nil {
fmt.Println(e)
break
}
fmt.Println(record)
}
}
-
通过执行
go run data.go
来运行代码。 -
在终端中查看输出:
- 创建名为
data_uncommon.csv
的文件,其中包含以下内容:
Name;Surname;Age
"John";Mnemonic;20
"Maria";Tone;21
- 创建名为
data_uncommon.go
的文件,其中包含以下内容:
package main
import (
"encoding/csv"
"fmt"
"os"
)
func main() {
file, err := os.Open("data_uncommon.csv")
if err != nil {
panic(err)
}
defer file.Close()
reader := csv.NewReader(file)
reader.Comma = ';'
for {
record, e := reader.Read()
if e != nil {
fmt.Println(e)
break
}
fmt.Println(record)
}
}
-
通过执行
go run data_uncommon.go
来运行代码。 -
在终端中查看输出:
它是如何工作的...
与简单地逐行扫描输入并使用strings.Split
和其他方法解析 CSV 格式不同,Go 提供了更好的方法。encoding/csv
包中的NewReader
函数返回Reader
结构,该结构提供了读取 CSV 文件的 API。Reader
结构保留了变量来配置read
参数,根据您的需求。
Reader
的FieldsPerRecord
参数是一个重要的设置。这样可以验证每行的单元格数。默认情况下,当设置为0
时,它设置为第一行中的记录数。如果设置为正值,则记录数必须匹配。如果设置为负值,则不进行单元格计数验证。
另一个有趣的配置是Comment
参数,它允许您定义解析数据中的注释字符。在示例中,整行都会被忽略。
Go 1.10 现在禁止使用荒谬的逗号和注释设置。这意味着空值、回车、换行、无效符文和 Unicode 替换字符。还禁止将逗号和注释设置为相等。
管理字符串中的空白
字符串输入可能包含过多的空白、过少的空白或不合适的空白字符。本示例包括了如何处理这些并将字符串格式化为所需格式的提示。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe11
。 -
导航到目录。
-
创建名为
whitespace.go
的文件,其中包含以下内容:
package main
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
)
func main() {
stringToTrim := "\t\t\n Go \tis\t Awesome \t\t"
trimResult := strings.TrimSpace(stringToTrim)
fmt.Println(trimResult)
stringWithSpaces := "\t\t\n Go \tis\n Awesome \t\t"
r := regexp.MustCompile("\\s+")
replace := r.ReplaceAllString(stringWithSpaces, " ")
fmt.Println(replace)
needSpace := "need space"
fmt.Println(pad(needSpace, 14, "CENTER"))
fmt.Println(pad(needSpace, 14, "LEFT"))
}
func pad(input string, padLen int, align string) string {
inputLen := len(input)
if inputLen >= padLen {
return input
}
repeat := padLen - inputLen
var output string
switch align {
case "RIGHT":
output = fmt.Sprintf("% "+strconv.Itoa(-padLen)+"s", input)
case "LEFT":
output = fmt.Sprintf("% "+strconv.Itoa(padLen)+"s", input)
case "CENTER":
bothRepeat := float64(repeat) / float64(2)
left := int(math.Floor(bothRepeat)) + inputLen
right := int(math.Ceil(bothRepeat))
output = fmt.Sprintf("% "+strconv.Itoa(left)+"s%
"+strconv.Itoa(right)+"s", input, "")
}
return output
}
-
通过执行
go run whitespace.go
来运行代码。 -
查看输出:
它是如何工作的...
在代码处理之前修剪字符串是非常常见的做法,正如前面的代码所示,标准的 Go 库可以轻松完成这项工作。strings
库还提供了更多TrimXXX
函数的变体,也允许修剪字符串中的其他字符。
要修剪前导和结束的空白,可以使用strings
包的TrimSpace
函数。这是代码的以下部分的典型示例,这也是之前示例中包含的:
stringToTrim := "\t\t\n Go \tis\t Awesome \t\t"
stringToTrim = strings.TrimSpace(stringToTrim)
regex
包适用于替换多个空格和制表符,可以通过这种方式准备字符串以便进一步处理。请注意,使用此方法时,换行符将被替换为一个空格。
代码的这一部分表示使用正则表达式将所有多个空格替换为单个空格:
r := regexp.MustCompile("\\s+")
replace := r.ReplaceAllString(stringToTrim, " ")
填充不是strings
包的显式函数,但可以通过fmt
包的Sprintf
函数实现。代码中的pad
函数使用格式化模式% <+/-padding>s
和一些简单的数学运算来找出填充。最后,填充数字前的减号作为右填充,正数作为左填充。
另请参阅
有关如何使用正则表达式的更多提示,您可以在本章中查看通过正则表达式模式在文本中查找子字符串的示例。
对文本文档进行缩进
前面的示例描述了如何进行字符串填充和修剪空白。这个示例将指导您如何对文本文档进行缩进和取消缩进。将使用前面示例中的类似原则。
如何做...
-
打开控制台并创建文件夹
chapter02/recipe12
。 -
创建名为
main.go
的文件,并包含以下内容:
package main
import (
"fmt"
"strconv"
"strings"
"unicode"
)
func main() {
text := "Hi! Go is awesome."
text = Indent(text, 6)
fmt.Println(text)
text = Unindent(text, 3)
fmt.Println(text)
text = Unindent(text, 10)
fmt.Println(text)
text = IndentByRune(text, 10, '.')
fmt.Println(text)
}
// Indent indenting the input by given indent and rune
func IndentByRune(input string, indent int, r rune) string {
return strings.Repeat(string(r), indent) + input
}
// Indent indenting the input by given indent
func Indent(input string, indent int) string {
padding := indent + len(input)
return fmt.Sprintf("% "+strconv.Itoa(padding)+"s", input)
}
// Unindent unindenting the input string. In case the
// input is indented by less than "indent" spaces
// the min of this both is removed.
func Unindent(input string, indent int) string {
count := 0
for _, val := range input {
if unicode.IsSpace(val) {
count++
}
if count == indent || !unicode.IsSpace(val) {
break
}
}
return input[count:]
}
-
在终端中执行
go run main.go
来运行代码。 -
查看输出:
它是如何工作的...
缩进就像填充一样简单。在这种情况下,使用相同的格式选项。indent
实现的更可读形式可以使用strings
包的Repeat
函数。上述代码中的IndentByRune
函数应用了这种方法。
在这种情况下,取消缩进意味着删除给定数量的前导空格。在上述代码中,Unindent
的实现会删除最少数量的前导空格或给定的缩进。
另请参阅
管理字符串中的空白示例也以更宽松的方式处理空格。
第三章:处理数字
本章的食谱有:
-
将字符串转换为数字
-
比较浮点数
-
舍入浮点数
-
浮点数运算
-
格式化数字
-
在二进制、八进制、十进制和十六进制之间转换
-
使用正确的复数格式
-
生成随机数
-
操作复数
-
在度和弧度之间转换
-
取对数
-
生成校验和
介绍
数字通常是每个应用程序的不可避免的部分——打印格式化的数字、转换基数表示等等。本章介绍了许多常见的操作。
检查 Go 是否已正确安装。第一章的准备就绪部分,与环境交互,将对您有所帮助。
将字符串转换为数字
本食谱将向您展示如何将包含数字的字符串转换为数值类型(整数或浮点值)。
操作步骤...
-
打开控制台并创建文件夹
chapter03/recipe01
。 -
导航到该目录。
-
创建包含以下内容的
main.go
文件:
package main
import (
"fmt"
"strconv"
)
const bin = "00001"
const hex = "2f"
const intString = "12"
const floatString = "12.3"
func main() {
// Decimals
res, err := strconv.Atoi(intString)
if err != nil {
panic(err)
}
fmt.Printf("Parsed integer: %d\n", res)
// Parsing hexadecimals
res64, err := strconv.ParseInt(hex, 16, 32)
if err != nil {
panic(err)
}
fmt.Printf("Parsed hexadecima: %d\n", res64)
// Parsing binary values
resBin, err := strconv.ParseInt(bin, 2, 32)
if err != nil {
panic(err)
}
fmt.Printf("Parsed bin: %d\n", resBin)
// Parsing floating-points
resFloat, err := strconv.ParseFloat(floatString, 32)
if err != nil {
panic(err)
}
fmt.Printf("Parsed float: %.5f\n", resFloat)
}
-
在终端中执行命令
go run main.go
。 -
您将看到以下输出:
工作原理...
在前面示例代码中的主要函数是strconv
包的ParseInt
函数。该函数带有三个参数:输入、输入的基数和位大小。基数确定了如何解析数字。请注意,十六进制的基数(第二个参数)为 16,二进制的基数为 2。strconv
包的Atoi
函数实际上就是带有基数 10 的ParseInt
函数。
ParseFloat
函数将字符串转换为浮点数。第二个参数是bitSize
的精度。bitSize = 64
将导致float64
。bitSize = 32
将导致float64
,但可以在不改变其值的情况下转换为float32
。
比较浮点数
由于浮点数的表示方式,比较两个看似相同的数字时可能会出现不一致。与整数不同,IEEE 浮点数只是近似值。需要将数字转换为计算机可以以二进制形式存储的形式,这会导致轻微的精度或舍入偏差。例如,值 1.3 可以表示为 1.29999999999。可以通过一些容差进行比较。要比较任意精度的数字,可以使用big
包。
操作步骤...
-
打开控制台并创建文件夹
chapter03/recipe02
。 -
导航到该目录。
-
创建包含以下内容的
tolerance.go
文件:
package main
import (
"fmt"
"math"
)
const da = 0.29999999999999998889776975374843459576368331909180
const db = 0.3
func main() {
daStr := fmt.Sprintf("%.10f", da)
dbStr := fmt.Sprintf("%.10f", db)
fmt.Printf("Strings %s = %s equals: %v \n", daStr,
dbStr, dbStr == daStr)
fmt.Printf("Number equals: %v \n", db == da)
// As the precision of float representation
// is limited. For the float comparison it is
// better to use comparison with some tolerance.
fmt.Printf("Number equals with TOLERANCE: %v \n",
equals(da, db))
}
const TOLERANCE = 1e-8
// Equals compares the floating-point numbers
// with tolerance 1e-8
func equals(numA, numB float64) bool {
delta := math.Abs(numA - numB)
if delta < TOLERANCE {
return true
}
return false
}
-
在终端中执行命令
go run tolerance.go
。 -
您将看到以下输出:
- 创建包含以下内容的
big.go
文件:
package main
import (
"fmt"
"math/big"
)
var da float64 = 0.299999992
var db float64 = 0.299999991
var prec uint = 32
var prec2 uint = 16
func main() {
fmt.Printf("Comparing float64 with '==' equals: %v\n", da == db)
daB := big.NewFloat(da).SetPrec(prec)
dbB := big.NewFloat(db).SetPrec(prec)
fmt.Printf("A: %v \n", daB)
fmt.Printf("B: %v \n", dbB)
fmt.Printf("Comparing big.Float with precision: %d : %v\n",
prec, daB.Cmp(dbB) == 0)
daB = big.NewFloat(da).SetPrec(prec2)
dbB = big.NewFloat(db).SetPrec(prec2)
fmt.Printf("A: %v \n", daB)
fmt.Printf("B: %v \n", dbB)
fmt.Printf("Comparing big.Float with precision: %d : %v\n",
prec2, daB.Cmp(dbB) == 0)
}
-
在终端中执行命令
go run big.go
。 -
您将看到以下输出:
工作原理...
在不使用任何内置包的情况下进行浮点数比较的第一种方法(步骤 1-5)需要使用所谓的EPSILON
常量。这是选择的足够小的增量(差异)的值,以便将两个数字视为相等。增量常数可以达到 1e-8 的数量级,这通常是足够的精度。
第二个选项更复杂,但对于进一步处理浮点数更有用。math/big
包提供了可以配置为给定精度的Float
类型。该包的优势在于精度可以比float64
类型的精度高得多。出于说明目的,使用了较小的精度值来显示给定精度的四舍五入和比较。
请注意,当使用 16 位精度时,da
和db
数字相等,当使用 32 位精度时,它们不相等。最大可配置的精度可以从big.MaxPrec
常量中获得。
四舍五入浮点数
将浮点数四舍五入为整数或特定精度必须正确进行。最常见的错误是将浮点类型float64
转换为整数类型,并认为它已经处理好了。
一个例子可能是将数字 3.9999 转换为整数,并期望它变成值为 4 的整数。实际结果将是 3。在撰写本书时,Go 的当前版本(1.9.2)不包含Round
函数。然而,在 1.10 版本中,Round
函数已经在math
包中实现。
如何做...
-
打开控制台并创建文件夹
chapter03/recipe03
。 -
导航到该目录。
-
创建
round.go
文件,内容如下:
package main
import (
"fmt"
"math"
)
var valA float64 = 3.55554444
func main() {
// Bad assumption on rounding
// the number by casting it to
// integer.
intVal := int(valA)
fmt.Printf("Bad rounding by casting to int: %v\n", intVal)
fRound := Round(valA)
fmt.Printf("Rounding by custom function: %v\n", fRound)
}
// Round returns the nearest integer.
func Round(x float64) float64 {
t := math.Trunc(x)
if math.Abs(x-t) >= 0.5 {
return t + math.Copysign(1, x)
}
return t
}
-
通过在终端中运行
go run round.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
将浮点数转换为整数实际上只是截断了浮点值。比如值 2 表示为 1.999999;在这种情况下,输出将是 1,这不是您期望的。
正确的浮点数四舍五入的方法是使用一个函数,该函数还会考虑小数部分。常用的四舍五入方法是向远离零的方向舍入(也称为商业舍入)。简而言之,如果数字包含小数部分的绝对值大于或等于 0.5,则将数字四舍五入,否则将向下舍入。
在Round
函数中,math
包的Trunc
函数截断了数字的小数部分。然后提取了数字的小数部分。如果值超过 0.5 的限制,那么就会加上与整数值相同的符号的 1。
Go 版本 1.10 使用了一个更快的实现,该实现在示例中提到。在 1.10 版本中,您可以直接调用math.Round
函数来获得四舍五入的数字。
浮点数算术
如前面的示例所述,浮点数的表示也使算术变得复杂。对于一般目的,内置的float64
上的操作已经足够。如果需要更高的精度,则需要使用math/big
包。本示例将向您展示如何处理这个问题。
如何做...
-
打开控制台并创建文件夹
chapter03/recipe04
。 -
导航到该目录。
-
创建
main.go
文件,内容如下:
package main
import (
"fmt"
"math/big"
)
const PI = `3.1415926535897932384626433832795028841971693
993751058209749445923078164062862089986280348253
421170679821480865132823066470938446095505822317
253594081284811174502841027019385211055596446229
4895493038196`
const diameter = 3.0
const precision = 400
func main() {
pi, _ := new(big.Float).SetPrec(precision).SetString(PI)
d := new(big.Float).SetPrec(precision).SetFloat64(diameter)
circumference := new(big.Float).Mul(pi, d)
pi64, _ := pi.Float64()
fmt.Printf("Circumference big.Float = %.400f\n",
circumference)
fmt.Printf("Circumference float64 = %.400f\n", pi64*diameter)
sum := new(big.Float).Add(pi, pi)
fmt.Printf("Sum = %.400f\n", sum)
diff := new(big.Float).Sub(pi, pi)
fmt.Printf("Diff = %.400f\n", diff)
quo := new(big.Float).Quo(pi, pi)
fmt.Printf("Quocient = %.400f\n", quo)
}
-
通过在终端中运行
go run main.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
big
包提供了对高精度浮点数进行算术运算的支持。前面的示例说明了对数字的基本操作。请注意,代码将float64
类型和big.Float
类型的操作进行了比较。
通过使用高精度数字,使用big.Float
类型是至关重要的。当big.Float
转换回内置的float64
类型时,高精度会丢失。
还有更多...
big
包包含Float
类型的更多操作。查看此包的文档(golang.org/pkg/math/big/#Float
)以获取更多详细信息。
另请参阅
浮点数的比较和四舍五入在比较浮点数和四舍五入浮点数示例中有提到。
格式化数字
如果数字转换为字符串,通常需要合理格式化。数字的格式化意味着数字以给定的数字和小数点打印出来。还可以选择值的表示。然而,与此密切相关的问题是数字格式的本地化。例如,一些语言使用逗号分隔的零。
如何做...
-
打开控制台并创建文件夹
chapter03/recipe05
。 -
导航到目录。
-
创建
format.go
文件,内容如下:
package main
import (
"fmt"
)
var integer int64 = 32500
var floatNum float64 = 22000.456
func main() {
// Common way how to print the decimal
// number
fmt.Printf("%d \n", integer)
// Always show the sign
fmt.Printf("%+d \n", integer)
// Print in other base X -16, o-8, b -2, d - 10
fmt.Printf("%X \n", integer)
fmt.Printf("%#X \n", integer)
// Padding with leading zeros
fmt.Printf("%010d \n", integer)
// Left padding with spaces
fmt.Printf("% 10d \n", integer)
// Right padding
fmt.Printf("% -10d \n", integer)
// Print floating
// point number
fmt.Printf("%f \n", floatNum)
// Floating-point number
// with limited precision = 5
fmt.Printf("%.5f \n", floatNum)
// Floating-point number
// in scientific notation
fmt.Printf("%e \n", floatNum)
// Floating-point number
// %e for large exponents
// or %f otherwise
fmt.Printf("%g \n", floatNum)
}
-
在主终端中运行
go run format.go
来执行代码。 -
你将看到以下输出:
- 创建文件
localized.go
,内容如下:
package main
import (
"golang.org/x/text/language"
"golang.org/x/text/message"
)
const num = 100000.5678
func main() {
p := message.NewPrinter(language.English)
p.Printf(" %.2f \n", num)
p = message.NewPrinter(language.German)
p.Printf(" %.2f \n", num)
}
-
在主终端中运行
go run localized.go
来执行代码。 -
你将看到以下输出:
它是如何工作的...
代码示例显示了整数和浮点数的最常用选项。
Go 中的格式化源自 C 的printf
函数。所谓的动词
用于定义数字的格式化。例如,动词可以是%X
,实际上是值的占位符。
除了基本格式化外,还有与本地习俗相关的格式化规则。根据区域设置进行格式化,包golang.org/x/text/message
可以提供帮助。请参阅本食谱中的第二个代码示例。这样,可以对数字格式进行本地化。
还有更多...
有关所有格式选项,请参阅fmt
包。strconv
包在需要以不同基数格式化数字时也可能很有用。以下食谱描述了数字转换的可能性,但副作用是如何以不同基数格式化数字的选项。
在二进制、八进制、十进制和十六进制之间转换
在某些情况下,整数值可以用除十进制表示以外的其他表示。这些表示之间的转换很容易通过strconv
包来完成。
如何做...
-
打开控制台并创建文件夹
chapter03/recipe06
。 -
导航到目录。
-
创建
convert.go
文件,内容如下:
package main
import (
"fmt"
"strconv"
)
const bin = "10111"
const hex = "1A"
const oct = "12"
const dec = "10"
const floatNum = 16.123557
func main() {
// Converts binary value into hex
v, _ := ConvertInt(bin, 2, 16)
fmt.Printf("Binary value %s converted to hex: %s\n", bin, v)
// Converts hex value into dec
v, _ = ConvertInt(hex, 16, 10)
fmt.Printf("Hex value %s converted to dec: %s\n", hex, v)
// Converts oct value into hex
v, _ = ConvertInt(oct, 8, 16)
fmt.Printf("Oct value %s converted to hex: %s\n", oct, v)
// Converts dec value into oct
v, _ = ConvertInt(dec, 10, 8)
fmt.Printf("Dec value %s converted to oct: %s\n", dec, v)
//... analogically any other conversion
// could be done.
}
// ConvertInt converts the given string value of base
// to defined toBase.
func ConvertInt(val string, base, toBase int) (string, error) {
i, err := strconv.ParseInt(val, base, 64)
if err != nil {
return "", err
}
return strconv.FormatInt(i, toBase), nil
}
-
在主终端中运行
go run convert.go
来执行代码。 -
你将看到以下输出:
它是如何工作的...
strconv
包提供了ParseInt
和FormatInt
函数,这些函数可以说是互补的函数。函数ParseInt
能够解析任何基数表示的整数。另一方面,函数FormatInt
可以将整数格式化为任何给定的基数。
最后,可以将整数的字符串表示解析为内置的int64
类型,然后将解析后的整数的字符串格式化为给定的基数表示。
使用正确的复数格式
在为用户显示消息时,如果句子更加人性化,交互会更加愉快。Go 包golang.org/x/text
,即扩展包,包含了以正确方式格式化复数的功能。
准备工作
执行go get -x golang.org/x/text
以获取扩展包,如果你还没有的话。
如何做...
-
打开控制台并创建文件夹
chapter03/recipe07
。 -
导航到目录。
-
创建
plurals.go
文件,内容如下:
package main
import (
"golang.org/x/text/feature/plural"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
func main() {
message.Set(language.English, "%d items to do",
plural.Selectf(1, "%d", "=0", "no items to do",
plural.One, "one item to do",
"<100", "%[1]d items to do",
plural.Other, "lot of items to do",
))
message.Set(language.English, "The average is %.2f",
plural.Selectf(1, "%.2f",
"<1", "The average is zero",
"=1", "The average is one",
plural.Other, "The average is %[1]f ",
))
prt := message.NewPrinter(language.English)
prt.Printf("%d items to do", 0)
prt.Println()
prt.Printf("%d items to do", 1)
prt.Println()
prt.Printf("%d items to do", 10)
prt.Println()
prt.Printf("%d items to do", 1000)
prt.Println()
prt.Printf("The average is %.2f", 0.8)
prt.Println()
prt.Printf("The average is %.2f", 1.0)
prt.Println()
prt.Printf("The average is %.2f", 10.0)
prt.Println()
}
-
在主终端中运行
go run plurals.go
来执行代码。 -
你将看到以下输出:
它是如何工作的...
包golang.org/x/text/message
包含函数NewPrinter
,接受语言标识并创建格式化的 I/O,与fmt
包相同,但具有根据性别和复数形式翻译消息的能力。
message
包的Set
函数添加了翻译和复数选择。复数形式本身是根据Selectf
函数设置的规则选择的。Selectf
函数生成基于plural.Form
或选择器的规则的catalog.Message
类型。
上述示例代码使用了plural.One
和plural.Other
形式,以及=x, <x
选择器。这些与格式化动词%d
匹配(也可以使用其他动词)。选择第一个匹配的情况。
还有更多...
有关选择器和形式的更多信息,请参阅golang.org/x/text/message
包的文档。
生成随机数
本教程展示了如何生成随机数。这个功能由math/rand
包提供。由math/rand
生成的随机数被认为是不安全的,因为序列是可重复的,具有给定的种子。
要生成加密安全的数字,应使用crypto/rand
包。这些序列是不可重复的。
如何做...
-
打开控制台并创建文件夹
chapter03/recipe08
。 -
导航到目录。
-
创建具有以下内容的
rand.go
文件:
package main
import (
crypto "crypto/rand"
"fmt"
"math/big"
"math/rand"
)
func main() {
sec1 := rand.New(rand.NewSource(10))
sec2 := rand.New(rand.NewSource(10))
for i := 0; i < 5; i++ {
rnd1 := sec1.Int()
rnd2 := sec2.Int()
if rnd1 != rnd2 {
fmt.Println("Rand generated non-equal sequence")
break
} else {
fmt.Printf("Math/Rand1: %d , Math/Rand2: %d\n", rnd1, rnd2)
}
}
for i := 0; i < 5; i++ {
safeNum := NewCryptoRand()
safeNum2 := NewCryptoRand()
if safeNum == safeNum2 {
fmt.Println("Crypto generated equal numbers")
break
} else {
fmt.Printf("Crypto/Rand1: %d , Crypto/Rand2: %d\n",
safeNum, safeNum2)
}
}
}
func NewCryptoRand() int64 {
safeNum, err := crypto.Int(crypto.Reader, big.NewInt(100234))
if err != nil {
panic(err)
}
return safeNum.Int64()
}
-
通过在主终端中运行
go run rand.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
上述代码介绍了如何生成随机数的两种可能性。第一种选项使用math/rand
包,这是不安全的,允许我们使用相同的种子号生成相同的序列。这种方法通常用于测试。这样做的原因是为了使序列可重现。
第二个选项,即加密安全选项,是使用crypto/rand
包。API 使用Reader
提供具有加密强大伪随机生成器实例。包本身具有默认的Reader
,通常基于基于系统的随机数生成器。
操作复数
复数通常用于科学应用和计算。Go 将复数实现为原始类型。复数的特定操作是math/cmplx
包的一部分。
如何做...
-
打开控制台并创建文件夹
chapter03/recipe09
。 -
导航到目录。
-
创建具有以下内容的
complex.go
文件:
package main
import (
"fmt"
"math/cmplx"
)
func main() {
// complex numbers are
// defined as real and imaginary
// part defined by float64
a := complex(2, 3)
fmt.Printf("Real part: %f \n", real(a))
fmt.Printf("Complex part: %f \n", imag(a))
b := complex(6, 4)
// All common
// operators are useful
c := a - b
fmt.Printf("Difference : %v\n", c)
c = a + b
fmt.Printf("Sum : %v\n", c)
c = a * b
fmt.Printf("Product : %v\n", c)
c = a / b
fmt.Printf("Product : %v\n", c)
conjugate := cmplx.Conj(a)
fmt.Println("Complex number a's conjugate : ", conjugate)
cos := cmplx.Cos(b)
fmt.Println("Cosine of b : ", cos)
}
-
通过在主终端中运行
go run complex.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
基本运算符是为原始类型complex
实现的。复数的其他操作由math/cmplx
包提供。如果需要高精度操作,则没有big
实现。
另一方面,复数可以实现为实数,并且虚部由big.Float
类型表示。
在度和弧度之间转换
三角函数运算和几何操作通常以弧度为单位进行;能够将这些转换为度数及其相反是非常有用的。本教程将向您展示如何处理这些单位之间的转换。
如何做...
-
打开控制台并创建文件夹
chapter03/recipe10
。 -
导航到目录。
-
创建具有以下内容的
radians.go
文件:
package main
import (
"fmt"
"math"
)
type Radian float64
func (rad Radian) ToDegrees() Degree {
return Degree(float64(rad) * (180.0 / math.Pi))
}
func (rad Radian) Float64() float64 {
return float64(rad)
}
type Degree float64
func (deg Degree) ToRadians() Radian {
return Radian(float64(deg) * (math.Pi / 180.0))
}
func (deg Degree) Float64() float64 {
return float64(deg)
}
func main() {
val := radiansToDegrees(1)
fmt.Printf("One radian is : %.4f degrees\n", val)
val2 := degreesToRadians(val)
fmt.Printf("%.4f degrees is %.4f rad\n", val, val2)
// Conversion as part
// of type methods
val = Radian(1).ToDegrees().Float64()
fmt.Printf("Degrees: %.4f degrees\n", val)
val = Degree(val).ToRadians().Float64()
fmt.Printf("Rad: %.4f radians\n", val)
}
func degreesToRadians(deg float64) float64 {
return deg * (math.Pi / 180.0)
}
func radiansToDegrees(rad float64) float64 {
return rad * (180.0 / math.Pi)
}
-
通过在主终端中运行
go run radians.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
Go 标准库不包含任何将弧度转换为度数及其相反的函数。但至少 Pi 常数是math
包的一部分,因此可以按照示例代码中所示进行转换。
上述代码还介绍了定义具有附加方法的自定义类型的方法。这些方法通过方便的 API 简化了值的转换。
取对数
对数在科学应用以及数据可视化和测量中被使用。内置的math
包包含了常用的对数基数。使用这些,你可以得到所有的基数。
操作步骤...
-
打开控制台并创建文件夹
chapter03/recipe11
。 -
导航到目录。
-
创建
log.go
文件,内容如下:
package main
import (
"fmt"
"math"
)
func main() {
ln := math.Log(math.E)
fmt.Printf("Ln(E) = %.4f\n", ln)
log10 := math.Log10(-100)
fmt.Printf("Log10(10) = %.4f\n", log10)
log2 := math.Log2(2)
fmt.Printf("Log2(2) = %.4f\n", log2)
log_3_6 := Log(3, 6)
fmt.Printf("Log3(6) = %.4f\n", log_3_6)
}
// Log computes the logarithm of
// base > 1 and x greater 0
func Log(base, x float64) float64 {
return math.Log(x) / math.Log(base)
}
-
在主终端中运行
go run log.go
来执行代码。 -
你将看到以下输出:
工作原理...
标准包math
包含了所有常用对数的函数,因此你可以轻松地得到二进制、十进制和自然对数。查看Log函数,它通过助手定义的公式计算任何以x为底的y的对数:
标准库中对数的内部实现自然是基于近似值的。这个函数可以在$GOROOT/src/math/log.go
文件中找到。
生成校验和
哈希,或者所谓的校验和,是快速比较任何内容的最简单方法。这个示例演示了如何创建文件内容的校验和。为了演示目的,将使用 MD5 哈希函数。
操作步骤...
-
打开控制台并创建文件夹
chapter03/recipe12
。 -
导航到目录。
-
创建
content.dat
文件,内容如下:
This is content to check
- 创建
checksum.go
文件,内容如下:
package main
import (
"crypto/md5"
"fmt"
"io"
"os"
)
var content = "This is content to check"
func main() {
checksum := MD5(content)
checksum2 := FileMD5("content.dat")
fmt.Printf("Checksum 1: %s\n", checksum)
fmt.Printf("Checksum 2: %s\n", checksum2)
if checksum == checksum2 {
fmt.Println("Content matches!!!")
}
}
// MD5 creates the md5
// hash for given content encoded in
// hex string
func MD5(data string) string {
h := md5.Sum([]byte(data))
return fmt.Sprintf("%x", h)
}
// FileMD5 creates hex encoded md5 hash
// of file content
func FileMD5(path string) string {
h := md5.New()
f, err := os.Open(path)
if err != nil {
panic(err)
}
defer f.Close()
_, err = io.Copy(h, f)
if err != nil {
panic(err)
}
return fmt.Sprintf("%x", h.Sum(nil))
}
-
在主终端中运行
go run checksum.go
来执行代码。 -
你将看到以下输出:
- 创建
sha_panic.go
文件,内容如下:
package main
import (
"crypto"
)
func main() {
crypto.SHA1.New()
}
-
在主终端中运行
go run sha_panic.go
来执行代码。 -
你将看到以下输出:
工作原理...
crypto
包包含了众所周知的哈希函数的实现。MD5
哈希函数位于crypto/md5
包中。crypto
包中的每个哈希函数都实现了Hash
接口。注意Hash
包含了Write
方法。通过Write
方法,它可以被用作Writer
。这可以在FileMD5
函数中看到。Hash
的Sum
方法接受字节切片的参数,结果哈希值将放置在其中。
注意这一点。Sum
方法不会计算参数的哈希值,而是将哈希计算到参数中。
另一方面,md5.Sum
包函数可以直接用于生成哈希。在这种情况下,Sum
函数的参数是计算出的哈希值。
自然地,crypto
包实现了SHA
变体和其他哈希函数。这些通常以相同的方式使用。哈希函数可以通过crypto
包的常量crypto.Hash
(例如,crypto.MD5.New()
)来访问,但是这种方式,给定函数的包也必须链接到构建的二进制文件中(可以使用空白导入,import _ "crypto/md5"
),否则对New
的调用将会导致恐慌。
hash
包本身包含了 CRC 校验和等内容。
第四章:从前有座山
本章中的食谱有:
-
查找今天的日期
-
将日期格式化为字符串
-
将字符串解析为日期
-
将日期转换为纪元和反之亦然
-
从日期中检索时间单位
-
日期算术
-
查找两个日期之间的差异
-
在不同时区之间转换
-
定期运行代码块
-
等待一定时间
-
超时长时间运行的操作
-
序列化时间和日期
介绍
本章主要讨论与时间相关的任务和操作。Go 将所有这些集中在名为time
的标准包中。使用此包,您可以获取当前时间和日期,将日期格式化为字符串,转换时区,创建定时器和创建滴答器。请记住,您可以实现和设计功能的方式总是很多,本章将只展示其中的一些方式。
验证 Go 是否正确安装。如果有任何问题,请参阅第一章中的检索 Golang 版本,并按照准备就绪部分的步骤进行操作。
查找今天的日期
获取当前日期是任何系统或应用程序的常见任务。让我们看看如何使用 Go 的标准库来完成这个任务。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe01
。 -
导航到目录。
-
创建名为
today.go
的文件,内容如下:
package main
import (
"fmt"
"time"
)
func main() {
today := time.Now()
fmt.Println(today)
}
-
通过在主终端中运行
go run today.go
来执行代码。 -
您将看到以下输出:
工作原理...
内置包time
包含函数Now
,该函数提供了初始化为当前本地时间和日期的Time
实例。
Time
类型是以纳秒为单位的时间点。Time
的零值是公元 1 年 1 月 1 日 00:00:00.000000000 UTC。
不应使用Time
类型的指针。如果只使用值(而不是变量的指针),则Time
实例被认为是安全的,可用于多个 goroutine。唯一的例外是序列化。
另请参阅
有关Time
类型的更多信息,请参阅time
包文档:golang.org/pkg/time
。
将日期格式化为字符串
如果需要时间值的文本表示形式,通常期望某种格式。time
包的Time
类型提供了在给定格式中创建string
输出的能力。有一些关于如何做到这一点的规则,我们将介绍一些有用的规则。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe02
。 -
导航到目录。
-
创建名为
format.go
的文件,内容如下:
package main
import (
"fmt"
"time"
)
func main() {
tTime := time.Date(2017, time.March, 5, 8, 5, 2, 0, time.Local)
// The formatting is done
// with use of reference value
// Jan 2 15:04:05 2006 MST
fmt.Printf("tTime is: %s\n", tTime.Format("2006/1/2"))
fmt.Printf("The time is: %s\n", tTime.Format("15:04"))
//The predefined formats could
// be used
fmt.Printf("The time is: %s\n", tTime.Format(time.RFC1123))
// The formatting supports space padding
//only for days in Go version 1.9.2
fmt.Printf("tTime is: %s\n", tTime.Format("2006/1/_2"))
// The zero padding is done by adding 0
fmt.Printf("tTime is: %s\n", tTime.Format("2006/01/02"))
//The fraction with leading zeros use 0s
fmt.Printf("tTime is: %s\n", tTime.Format("15:04:05.00"))
//The fraction without leading zeros use 9s
fmt.Printf("tTime is: %s\n", tTime.Format("15:04:05.999"))
// Append format appends the formatted time to given
// buffer
fmt.Println(string(tTime.AppendFormat([]byte("The time
is up: "), "03:04PM")))
}
-
通过在主终端中运行
go run format.go
来执行代码。 -
您将看到以下输出:
工作原理...
time
包的Time
类型提供了Format
方法来格式化输出字符串。
Go 使用引用时间值Jan 2 15:04:05 2006 MST
来定义格式布局。有关填充选项,请参阅代码示例。
参考日期的备忘录是,以数字形式给出时,表示为 1,2,3,4,5,6,-7。-7 值表示 MST 时区比 UTC 晚 7 小时。
时间包包括一些预定义格式(例如time.Kitchen
);您可以在包常量的文档中发现这些内容。(golang.org/pkg/time/#pkg-constants
)
另请参阅
有关所有预定义格式和格式选项,请参阅time
包的文档:golang.org/pkg/time
。
将字符串解析为日期
日期格式化中使用的概念与日期解析中使用的概念相同。可以使用相同的参考日期和布局原则。本食谱将向您展示如何将字符串输入转换为Time
实例。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe03
。 -
导航到目录。
-
创建包含以下内容的
parse.go
文件:
package main
import (
"fmt"
"time"
)
func main() {
// If timezone is not defined
// than Parse function returns
// the time in UTC timezone.
t, err := time.Parse("2/1/2006", "31/7/2015")
if err != nil {
panic(err)
}
fmt.Println(t)
// If timezone is given than it is parsed
// in given timezone
t, err = time.Parse("2/1/2006 3:04 PM MST",
"31/7/2015 1:25 AM DST")
if err != nil {
panic(err)
}
fmt.Println(t)
// Note that the ParseInLocation
// parses the time in given location, if the
// string does not contain time zone definition
t, err = time.ParseInLocation("2/1/2006 3:04 PM ",
"31/7/2015 1:25 AM ", time.Local)
if err != nil {
panic(err)
}
fmt.Println(t)
}
-
在主终端中运行
go run parse.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
time
包包含Parse
函数,用于解析带有时间信息的字符串。
传入日期字符串的格式由参考日期给出,格式化为匹配的格式。请记住,参考时间是Jan 2 15:04:05 2006 MST
。
如果给定的时间字符串不包含有关时区的信息,则Parse
函数的结果将始终为UTC
。
如果提供了时区信息,则时间始终是所提供时区的时间瞬间。
ParseInLocation
函数接受第三个参数,即位置。如果时间字符串不包含任何时区信息,则时间将被解析为给定位置的Time
实例。
将日期转换为时期,反之亦然
时期是描述时间点的通用系统。时期时间的开始被定义为00:00:00 1 Jan 1970 UTC
。时期的值是自时间戳以来的秒数,减去那时以来的闰秒数。
time
包和Time
类型使您能够操作并找出 UNIX 时期时间。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe04
。 -
导航到目录。
-
创建包含以下内容的
epoch.go
文件:
package main
import (
"fmt"
"time"
)
func main() {
// Set the epoch from int64
t := time.Unix(0, 0)
fmt.Println(t)
// Get the epoch
// from Time instance
epoch := t.Unix()
fmt.Println(epoch)
// Current epoch time
apochNow := time.Now().Unix()
fmt.Printf("Epoch time in seconds: %d\n", apochNow)
apochNano := time.Now().UnixNano()
fmt.Printf("Epoch time in nano-seconds: %d\n", apochNano)
}
-
在主终端中运行
go run epoch.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
time
包包含Unix
函数,它接受两个int64
参数,即时期时间的秒数和纳秒数。这样,您可以从时期值获取Time
实例。
要从Time
实例获取时期值,可以调用与从时期创建Time
相同名称的方法Unix
。还有一个名为UnixNano
的方法,它返回毫秒的计数,而不是秒。
从日期中检索时间单位
Time
类型还提供了从实例中检索时间单位的 API。这意味着您可以找出实例表示的月份中的哪一天,或者一天中的哪个小时。本教程展示了如何获取这样的单位。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe05
。 -
导航到目录。
-
创建包含以下内容的
units.go
文件:
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2017, 11, 29, 21, 0, 0, 0, time.Local)
fmt.Printf("Extracting units from: %v\n", t)
dOfMonth := t.Day()
weekDay := t.Weekday()
month := t.Month()
fmt.Printf("The %dth day of %v is %v\n", dOfMonth,
month, weekDay)
}
-
在主终端中运行
go run units.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
Time
类型提供了提取时间单位的方法。前面的示例显示了提取星期几、月份和月份的日期。类似地,还可以提取小时、秒和其他单位。
自然地,API 直接未提供的单位需要从现有单位中派生出来。
日期算术
time
包的Time
类型还允许您对给定的日期和时间执行基本算术运算。这样,您可以找出过去和未来的日期。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe06
。 -
导航到目录。
-
创建包含以下内容的
arithmetics.go
文件:
package main
import (
"fmt"
"time"
)
func main() {
l, err := time.LoadLocation("Europe/Vienna")
if err != nil {
panic(err)
}
t := time.Date(2017, 11, 30, 11, 10, 20, 0, l)
fmt.Printf("Default date is: %v\n", t)
// Add 3 days
r1 := t.Add(72 * time.Hour)
fmt.Printf("Default date +3HRS is: %v\n", r1)
// Subtract 3 days
r1 = t.Add(-72 * time.Hour)
fmt.Printf("Default date -3HRS is: %v\n", r1)
// More comfortable api
// to add days/months/years
r1 = t.AddDate(1, 3, 2)
fmt.Printf("Default date +1YR +3MTH +2D is: %v\n", r1)
}
-
在主终端中运行
go run arithmetics.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
time
包的Time
类型提供了两种操作日期和时间的基本方法。
第一种方法Add
接受time.Duration
和AddDate
。使用Add
方法,您可以通过正号将时间向未来移动,并通过添加负号将时间向后移动。
第二种方法AddDate
,消耗int64
参数作为年、月和日,并添加更大的时间量。
请注意,AddDate
会对结果进行标准化,与time.Date
函数相同。标准化意味着将月份添加到 8 月 31 日将导致 10 月 1 日,因为接下来的一个月只有 30 天(9 月 31 日不存在)。
查找两个日期之间的差异
查找两个日期之间的差异并不是一项不寻常的任务。对于这个操作,Go 标准包time
,分别是Time
类型,提供了支持方法。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe07
。 -
导航到目录。
-
创建
diff.go
文件,内容如下:
package main
import (
"fmt"
"time"
)
func main() {
l, err := time.LoadLocation("Europe/Vienna")
if err != nil {
panic(err)
}
t := time.Date(2000, 1, 1, 0, 0, 0, 0, l)
t2 := time.Date(2000, 1, 3, 0, 0, 0, 0, l)
fmt.Printf("First Default date is %v\n", t)
fmt.Printf("Second Default date is %v\n", t2)
dur := t2.Sub(t)
fmt.Printf("The duration between t and t2 is %v\n", dur)
dur = time.Since(t)
fmt.Printf("The duration between now and t is %v\n", dur)
dur = time.Until(t)
fmt.Printf("The duration between t and now is %v\n", dur)
}
-
通过在主终端中运行
go run diff.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
Time
实例的Sub
方法是找出两个日期之间差异的通用方法。结果是time.Duration
,表示这些日期之间的纳秒计数。
请注意,如果差异超过了最大/最小time.Duration
的限制,那么将返回最大或最小值。
函数Since
和Until
只是计算现在和给定日期之间差异的一种更简洁的方式。它们的工作方式与它们的名称提示的一样。Since
函数返回的结果与time.Now().Sub(t)
相同;同样,Until
返回的结果与t.Sub(time.Now())
相同。
Sub
方法自然也考虑了时区。因此,差异是相对于每个Time
实例的位置返回的。
在不同时区之间转换
处理时区很困难。处理不同时区的一个好方法是将一个时区作为系统中的参考时区,并在需要时转换其他时区。这个配方向您展示了如何在不同时区之间进行时间转换。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe08
。 -
导航到目录。
-
创建
timezones.go
文件,内容如下:
package main
import (
"fmt"
"time"
)
func main() {
eur, err := time.LoadLocation("Europe/Vienna")
if err != nil {
panic(err)
}
t := time.Date(2000, 1, 1, 0, 0, 0, 0, eur)
fmt.Printf("Original Time: %v\n", t)
phx, err := time.LoadLocation("America/Phoenix")
if err != nil {
panic(err)
}
t2 := t.In(phx)
fmt.Printf("Converted Time: %v\n", t2)
}
-
通过在主终端中运行
go run timezones.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
Time
类型提供了In
方法,它消耗指向time.Location
的指针。返回的Time
是原始的转换为给定时区的时间。请注意,Time
实例被认为是不可变的,因此改变实例的方法会导致新的Time
实例。
time
包引用IANA 时区数据库作为位置的来源。LoadLocation
函数查找ZONEINFO
环境变量中的目录或 ZIP 文件。如果找不到,则在 UNIX 系统上搜索已知的安装位置。最后,它在$GOROOT/lib/time/zoneinfo.zip
中查找。
定期运行代码块
除了日期和时间操作,time
包还提供了对周期性和延迟代码执行的支持。通常,应用程序健康检查、活动检查或任何周期性作业都可以通过这种方式实现。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe09
。 -
导航到目录。
-
创建
ticker.go
文件,内容如下:
package main
import (
"fmt"
"os"
"os/signal"
"time"
)
func main() {
c := make(chan os.Signal, 1)
signal.Notify(c)
ticker := time.NewTicker(time.Second)
stop := make(chan bool)
go func() {
defer func() { stop <- true }()
for {
select {
case <-ticker.C:
fmt.Println("Tick")
case <-stop:
fmt.Println("Goroutine closing")
return
}
}
}()
// Block until
// the signal is received
<-c
ticker.Stop()
// Stop the goroutine
stop <- true
// Wait until the
<-stop
fmt.Println("Application stopped")
}
-
通过在主终端中运行
go run ticker.go
来执行代码。 -
等待几秒钟,然后按Ctrl + C发送
SIGINT
信号。 -
您将看到以下输出:
它是如何工作的...
Ticker
持有C
通道,用于传递周期性的滴答声。实例是根据滴答声之间的给定间隔创建的。间隔由time.Duration
值定义。
打算定期执行的代码在无限循环中的 goroutine 中执行。从Ticker
通道读取会阻塞循环,直到传递滴答声。
请注意,一旦调用Stop
方法停止Ticker
,C
通道并不会关闭,它只是停止传递滴答声。因此,前面的代码包含了select
结构,其中停止通道可以传递停止信号。这样就可以进行优雅的关闭。
等待一定时间
前面的示例描述了如何定期执行代码。本示例将向您展示如何延迟执行代码。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe10
。 -
导航到该目录。
-
创建
delay.go
文件,内容如下:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
t := time.NewTimer(3 * time.Second)
fmt.Printf("Start waiting at %v\n",
time.Now().Format(time.UnixDate))
<-t.C
fmt.Printf("Code executed at %v\n",
time.Now().Format(time.UnixDate))
wg := &sync.WaitGroup{}
wg.Add(1)
fmt.Printf("Start waiting for AfterFunc at %v\n",
time.Now().Format(time.UnixDate))
time.AfterFunc(3*time.Second, func() {
fmt.Printf("Code executed for AfterFunc at %v\n",
time.Now().Format(time.UnixDate))
wg.Done()
})
wg.Wait()
fmt.Printf("Waiting on time.After at %v\n",
time.Now().Format(time.UnixDate))
<-time.After(3 * time.Second)
fmt.Printf("Code resumed at %v\n",
time.Now().Format(time.UnixDate))
}
-
在主终端中运行
go run delay.go
来执行代码。 -
您将看到以下输出:
工作原理是...
要执行带有一定延迟的代码,可以使用time
包中的Timer
。这个工作原理与前面的定期运行代码块中描述的相同。
Timer
包含C
通道,在给定时间后传递滴答声。之后,该通道不会再传递其他滴答声。
相同的功能由time
包的AfterFunc
函数提供。它只是简化了使用。请注意,这里不需要通道。示例代码使用sync.WaitGroup
来等待给定的函数执行。
time.After
是前面示例中的最后选择。该函数返回一个通道,在给定时间后传递滴答声。请注意Timer
和After
函数之间的区别。Timer
是可重用的结构(提供Stop
和Reset
方法)。另一方面,After
函数只能使用一次,因为它不提供任何重置选项。
超时长时间运行的操作
前面的示例描述了如何延迟执行代码的概念。相同的概念可以用来实现长时间运行操作的超时。本示例将说明如何实现这一点。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe11
。 -
导航到该目录。
-
创建
timeout.go
文件,内容如下:
package main
import (
"fmt"
"time"
)
func main() {
to := time.After(3 * time.Second)
list := make([]string, 0)
done := make(chan bool, 1)
fmt.Println("Starting to insert items")
go func() {
defer fmt.Println("Exiting goroutine")
for {
select {
case <-to:
fmt.Println("The time is up")
done <- true
return
default:
list = append(list, time.Now().String())
}
}
}()
<-done
fmt.Printf("Managed to insert %d items\n", len(list))
}
-
在主终端中运行
go run timeout.go
来执行代码。 -
您将看到以下输出:
工作原理是...
在前面的代码中,长时间运行的操作的超时是通过time.After
函数实现的,该函数提供在给定时间后传递的通道。
操作本身被包装到一个选择语句中,该语句在time.After
通道和默认选项之间进行选择,执行操作。
请注意,您需要允许代码定期从time.After
通道中读取,以了解超时是否已经超过。否则,如果默认的代码分支完全阻塞执行,就没有办法知道超时是否已经过去。
还有更多...
示例实现使用了time.After
函数,但Timer
函数也可以以相同的方式使用。内置库还使用context.WithTimeout
来实现超时功能。
序列化时间和日期
在序列化日期和时间信息时,需要选择合适的格式。本示例将说明time
包如何帮助选择合适的格式并正确进行序列化。
如何做...
-
打开控制台并创建文件夹
chapter04/recipe12
。 -
导航到该目录。
-
创建
serialize.go
文件,内容如下:
package main
import (
"encoding/json"
"fmt"
"time"
)
func main() {
eur, err := time.LoadLocation("Europe/Vienna")
if err != nil {
panic(err)
}
t := time.Date(2017, 11, 20, 11, 20, 10, 0, eur)
// json.Marshaler interface
b, err := t.MarshalJSON()
if err != nil {
panic(err)
}
fmt.Println("Serialized as RFC 3339:", string(b))
t2 := time.Time{}
t2.UnmarshalJSON(b)
fmt.Println("Deserialized from RFC 3339:", t2)
// Serialize as epoch
epoch := t.Unix()
fmt.Println("Serialized as Epoch:", epoch)
// Deserialize epoch
jsonStr := fmt.Sprintf("{ \"created\":%d }", epoch)
data := struct {
Created int64 `json:"created"`
}{}
json.Unmarshal([]byte(jsonStr), &data)
deserialized := time.Unix(data.Created, 0)
fmt.Println("Deserialized from Epoch:", deserialized)
}
-
在主终端中运行
go run serialize.go
来执行代码。 -
您将看到以下输出:
工作原理是...
Time
函数实现了二进制、Gob 和 JSON 序列化的接口。JSON 格式被认为是非常通用的,因此展示了将值序列化为 JSON 的示例。请注意,Time
函数以 RFC 3339(www.ietf.org/rfc/rfc3339.txt
)的方式序列化值,该规范提出了所谓的互联网日期/时间格式。
另一种非常通用的序列化/保留时间的方法是使用纪元时间。纪元时间与时区无关,因为它是由自某一绝对时间点以来经过的秒/纳秒定义的。最后,它被表示为一个数字,因此没有理由对值进行序列化和反序列化。
第五章:进入和退出
本章包含以下教程:
-
读取标准输入
-
写入标准输出和错误
-
通过名称打开文件
-
将文件读入字符串
-
读写不同的字符集
-
在文件中寻找位置
-
读写二进制数据
-
同时写入多个写入器
-
写入和读取之间的管道
-
将对象序列化为二进制格式
-
读写 ZIP 文件
-
有效地解析大型 XML 文件
-
从不完整的 JSON 数组中提取数据
介绍
本章将介绍典型的 I/O 操作和相关任务,以及各种输入源的写入和读取。我们将介绍 XML 处理、解压缩压缩文件以及使用随机访问文件。
检查 Go 是否已正确安装。第一章的准备就绪部分,与环境交互的检索 Golang 版本教程将对您有所帮助。
读取标准输入
每个进程都拥有自己的标准输入、输出和错误文件描述符。stdin
作为进程的输入。本教程描述了如何从stdin
读取数据。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe01
。 -
导航到目录。
-
创建名为
fmt.go
的文件,内容如下:
package main
import (
"fmt"
)
func main() {
var name string
fmt.Println("What is your name?")
fmt.Scanf("%s\n", &name)
var age int
fmt.Println("What is your age?")
fmt.Scanf("%d\n", &age)
fmt.Printf("Hello %s, your age is %d\n", name, age)
}
-
使用
go run fmt.go
执行代码。 -
输入
John
并按Enter。 -
输入
40
并按Enter。 -
您将看到以下输出:
- 创建名为
scanner.go
的文件,内容如下:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// The Scanner is able to
// scan input by lines
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
txt := sc.Text()
fmt.Printf("Echo: %s\n", txt)
}
}
-
使用
go run scanner.go
执行代码。 -
输入
Hello
并按Enter。 -
按下CTRL + C发送
SIGINT
。 -
查看输出:
- 创建名为
reader.go
的文件,内容如下:
package main
import (
"fmt"
"os"
)
func main() {
for {
data := make([]byte, 8)
n, err := os.Stdin.Read(data)
if err == nil && n > 0 {
process(data)
} else {
break
}
}
}
func process(data []byte) {
fmt.Printf("Received: %X %s\n", data, string(data))
}
-
使用管道输入
echo 'Go is awesome!' | go run reader.go
执行代码。 -
查看输出:
工作原理...
Go 进程的stdin
可以通过os
包的Stdin
获取。实际上,它是一个实现了Reader
接口的File
类型。从Reader
读取非常容易。上述代码展示了从Stdin
读取的三种常见方式。
第一个选项演示了fmt
包的使用,该包提供了Scan
、Scanf
和Scanln
函数。Scanf
函数将输入读取到给定的变量中。Scanf
的优点是可以确定扫描值的格式。Scan
函数只是将输入读取到变量中(没有预定义的格式),而Scanln
则像其名称一样,读取以换行符结束的输入。
Scanner
是示例代码中显示的第二个选项,它提供了一种方便的扫描大量输入的方式。Scanner
包含了Split
函数,可以定义自定义的分割函数。例如,要从stdin
扫描单词,可以使用bufio.ScanWords
预定义的SplitFunc
。
通过Reader
API 进行读取是最后介绍的方法。这种方法可以更好地控制输入的读取方式。
写入标准输出和错误
正如前面的教程所述,每个进程都有stdin
、stdout
和stderr
文件描述符。标准方法是使用stdout
作为进程输出,stderr
作为进程错误输出。由于这些是文件描述符,数据写入的目标可以是任何东西,从控制台到套接字。本教程将向您展示如何写入stdout
和stderr
。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe02
。 -
导航到目录。
-
创建名为
stdouterr.go
的文件,内容如下:
package main
import (
"fmt"
"io"
"os"
)
func main() {
// Simply write string
io.WriteString(os.Stdout,
"This is string to standard output.\n")
io.WriteString(os.Stderr,
"This is string to standard error output.\n")
// Stdout/err implements
// writer interface
buf := []byte{0xAF, 0xFF, 0xFE}
for i := 0; i < 200; i++ {
if _, e := os.Stdout.Write(buf); e != nil {
panic(e)
}
}
// The fmt package
// could be used too
fmt.Fprintln(os.Stdout, "\n")
}
-
使用
go run stdouterr.go
执行代码。 -
查看输出:
工作原理...
与前面示例中的Stdin
一样,Stdout
和Stderr
是文件描述符。因此,它们实现了Writer
接口。
前面的示例展示了如何通过io.WriteString
函数、Writer
API 的使用以及fmt
包和FprintXX
函数来写入这些内容的几种方法。
通过名称打开文件
文件访问是一种非常常见的操作,用于存储或读取数据。本示例说明了如何使用标准库通过文件名和路径打开文件。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe03
。 -
导航到目录。
-
创建目录
temp
并在其中创建文件file.txt
。 -
编辑
file.txt
文件并将This file content
写入文件。 -
使用以下内容创建
openfile.go
文件:
package main
import (
"fmt"
"io"
"io/ioutil"
"os"
)
func main() {
f, err := os.Open("temp/file.txt")
if err != nil {
panic(err)
}
c, err := ioutil.ReadAll(f)
if err != nil {
panic(err)
}
fmt.Printf("### File content ###\n%s\n", string(c))
f.Close()
f, err = os.OpenFile("temp/test.txt", os.O_CREATE|os.O_RDWR,
os.ModePerm)
if err != nil {
panic(err)
}
io.WriteString(f, "Test string")
f.Close()
}
- 文件结构应该如下所示:
-
使用
go run openfile.go
执行代码。 -
查看输出,
temp
文件夹中还应该有一个新文件test.txt
:
它是如何工作的...
os
包提供了一种简单的打开文件的方式。函数Open
通过路径打开文件,只以只读模式打开。另一个函数OpenFile
更强大,需要文件路径、标志和权限。
标志常量在os
包中定义,可以使用二进制 OR 运算符|
组合它们。权限由os
包常量(例如os.ModePerm
)或数字表示法(如0777
,权限为-rwxrwxrwx
)设置。
将文件读取为字符串
在前面的示例中,我们看到了从Stdin
读取和打开文件。在本示例中,我们将稍微结合这两者,并展示如何将文件读取为字符串。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe04
。 -
导航到目录。
-
创建目录
temp
并在其中创建文件file.txt
。 -
编辑
file.txt
文件并写入多行内容。 -
使用以下内容创建
readfile.go
文件:
package main
import "os"
import "bufio"
import "bytes"
import "fmt"
import "io/ioutil"
func main() {
fmt.Println("### Read as reader ###")
f, err := os.Open("temp/file.txt")
if err != nil {
panic(err)
}
defer f.Close()
// Read the
// file with reader
wr := bytes.Buffer{}
sc := bufio.NewScanner(f)
for sc.Scan() {
wr.WriteString(sc.Text())
}
fmt.Println(wr.String())
fmt.Println("### ReadFile ###")
// for smaller files
fContent, err := ioutil.ReadFile("temp/file.txt")
if err != nil {
panic(err)
}
fmt.Println(string(fContent))
}
-
使用
go run readfile.go
执行代码。 -
查看输出:
它是如何工作的...
从文件中读取很简单,因为File
类型实现了Reader
和Writer
接口。这样,所有适用于Reader
接口的函数和方法都适用于File
类型。前面的示例展示了如何使用Scanner
读取文件并将内容写入字节缓冲区(这比字符串连接更高效)。这样,您可以控制从文件中读取的内容量。
第二种方法使用ioutil.ReadFile
更简单,但应谨慎使用,因为它会读取整个文件。请记住,文件可能很大,可能会威胁应用程序的稳定性。
读取/写入不同的字符集
各种来源的输入可能以各种字符集的形式出现并不是例外。请注意,许多系统使用 Windows 操作系统,但也有其他系统。Go 默认期望程序中使用的字符串是基于 UTF-8 的。如果不是,则必须从给定的字符集解码以便能够处理该字符串。本示例将展示以 UTF-8 之外的字符集读取和写入文件。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe05
。 -
导航到目录。
-
使用以下内容创建
charset.go
文件:
package main
import (
"fmt"
"io/ioutil"
"os"
"golang.org/x/text/encoding/charmap"
)
func main() {
// Write the string
// encoded to Windows-1252
encoder := charmap.Windows1252.NewEncoder()
s, e := encoder.String("This is sample text with runes Š")
if e != nil {
panic(e)
}
ioutil.WriteFile("example.txt", []byte(s), os.ModePerm)
// Decode to UTF-8
f, e := os.Open("example.txt")
if e != nil {
panic(e)
}
defer f.Close()
decoder := charmap.Windows1252.NewDecoder()
reader := decoder.Reader(f)
b, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
fmt.Println(string(b))
}
-
使用
go run charset.go
执行代码。 -
查看输出:
它是如何工作的...
golang.org/x/text/encoding/charmap
包包含代表广泛使用的字符集的 Charmap
类型指针常量。Charmap
类型提供了为给定字符集创建编码器和解码器的方法。Encoder
创建编码 Writer
,将写入的字节编码为所选字符集。类似地,Decoder
可以创建解码 Reader
,从所选字符集解码所有读取的数据。
另请参阅
第二章,字符串和其他内容,还包含了编码/解码字符串到另一个字符集的教程从非 Unicode 字符集解码字符串。
在文件中寻找位置
在某些情况下,您需要从文件的特定位置读取或写入,例如索引文件。本教程将向您展示如何在平面文件操作的上下文中使用位置寻找。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe06
。 -
导航到目录。
-
创建名为
flatfile.txt
的文件,并包含以下内容:
123.Jun.......Wong......
12..Novak.....Jurgen....
10..Thomas....Sohlich...
- 创建名为
fileseek.go
的文件,并包含以下内容:
package main
import (
"errors"
"fmt"
"os"
)
const lineLegth = 25
func main() {
f, e := os.OpenFile("flatfile.txt", os.O_RDWR|os.O_CREATE,
os.ModePerm)
if e != nil {
panic(e)
}
defer f.Close()
fmt.Println(readRecords(2, "last", f))
if err := writeRecord(2, "first", "Radomir", f); err != nil {
panic(err)
}
fmt.Println(readRecords(2, "first", f))
if err := writeRecord(10, "first", "Andrew", f); err != nil {
panic(err)
}
fmt.Println(readRecords(10, "first", f))
fmt.Println(readLine(2, f))
}
func readLine(line int, f *os.File) (string, error) {
lineBuffer := make([]byte, 24)
f.Seek(int64(line*lineLegth), 0)
_, err := f.Read(lineBuffer)
return string(lineBuffer), err
}
func writeRecord(line int, column, dataStr string, f *os.File)
error {
definedLen := 10
position := int64(line * lineLegth)
switch column {
case "id":
definedLen = 4
case "first":
position += 4
case "last":
position += 14
default:
return errors.New("Column not defined")
}
if len([]byte(dataStr)) > definedLen {
return fmt.Errorf("Maximum length for '%s' is %d",
column, definedLen)
}
data := make([]byte, definedLen)
for i := range data {
data[i] = '.'
}
copy(data, []byte(dataStr))
_, err := f.WriteAt(data, position)
return err
}
func readRecords(line int, column string, f *os.File)
(string, error) {
lineBuffer := make([]byte, 24)
f.ReadAt(lineBuffer, int64(line*lineLegth))
var retVal string
switch column {
case "id":
return string(lineBuffer[:3]), nil
case "first":
return string(lineBuffer[4:13]), nil
case "last":
return string(lineBuffer[14:23]), nil
}
return retVal, errors.New("Column not defined")
}
-
使用
go run fileseek.go
执行代码。 -
查看输出:
- 以十六进制显示文件
xxd flatfile.txt
。
它是如何工作的...
前面的示例使用 flatfile
作为演示如何在文件中寻找、读取和写入的例子。通常,可以使用 Seek
方法来移动当前指针在 File
中的位置。它接受两个参数,即位置和如何计算位置,0 - 相对于文件原点,1 - 相对于当前位置,2 - 相对于文件末尾
。这样,您可以在文件中移动光标。Seek
方法在前面代码中的 readLine
函数的实现中使用。
flatfile
是存储数据的最基本形式。记录结构具有固定长度,记录部分也是如此。示例中的平面文件结构是:ID
- 4 个字符,FirstName
- 10 个字符,LastName
- 10 个字符。整个记录长度为 24 个字符,以换行符结束,即第 25 个字符。
os.File
还包含 ReadAt
和 WriteAt
方法。这些方法消耗要写入/读取的字节和开始的偏移量。这简化了在文件中特定位置的写入和读取。
请注意,示例假定每个符文只有一个字节,这对于特殊字符等可能并不正确。
读取和写入二进制数据
本教程描述了如何以二进制形式写入和读取任何类型。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe07
。 -
导航到目录。
-
创建名为
rwbinary.go
的文件,并包含以下内容:
package main
import (
"bytes"
"encoding/binary"
"fmt"
)
func main() {
// Writing binary values
buf := bytes.NewBuffer([]byte{})
if err := binary.Write(buf, binary.BigEndian, 1.004);
err != nil {
panic(err)
}
if err := binary.Write(buf, binary.BigEndian,
[]byte("Hello")); err != nil {
panic(err)
}
// Reading the written values
var num float64
if err := binary.Read(buf, binary.BigEndian, &num);
err != nil {
panic(err)
}
fmt.Printf("float64: %.3f\n", num)
greeting := make([]byte, 5)
if err := binary.Read(buf, binary.BigEndian, &greeting);
err != nil {
panic(err)
}
fmt.Printf("string: %s\n", string(greeting))
}
-
通过
go run rwbinary.go
执行代码。 -
查看输出:
它是如何工作的...
可以使用 encoding/binary
包写入二进制数据。函数 Write
消耗应该写入数据的 Writer
,字节顺序(BigEndian
/LittleEndian
),最后是要写入 Writer
的值。
要类似地读取二进制数据,可以使用 Read
函数。请注意,从二进制源读取数据并没有什么神奇之处。您需要确定从 Reader
中获取的数据是什么。如果不确定,数据可能会被获取到适合大小的任何类型中。
同时向多个写入器写入
当您需要将相同的输出写入多个目标时,内置包中提供了帮助。本教程展示了如何同时实现写入多个目标。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe08
。 -
导航到目录。
-
创建名为
multiwr.go
的文件,并包含以下内容:
package main
import "io"
import "bytes"
import "os"
import "fmt"
func main() {
buf := bytes.NewBuffer([]byte{})
f, err := os.OpenFile("sample.txt", os.O_CREATE|os.O_RDWR,
os.ModePerm)
if err != nil {
panic(err)
}
wr := io.MultiWriter(buf, f)
_, err = io.WriteString(wr, "Hello, Go is awesome!")
if err != nil {
panic(err)
}
fmt.Println("Content of buffer: " + buf.String())
}
-
通过
go run multiwr.go
执行代码。 -
查看输出:
- 检查创建文件的内容:
Hello, Go is awesome!
工作原理...
io
包含MultiWriter
函数,带有Writers
的可变参数。当调用Writer
上的Write
方法时,数据将被写入所有底层的Writers
。
在写入器和读取器之间进行管道传输
进程之间的管道是使用第一个进程的输出作为其他进程的输入的简单方法。在 Go 中也可以使用相同的概念,例如,将数据从一个套接字传输到另一个套接字,创建隧道连接。本教程将向您展示如何使用 Go 内置库创建管道。
操作步骤如下...
-
打开控制台并创建文件夹
chapter05/recipe09
。 -
导航到目录。
-
创建
pipe.go
文件,内容如下:
package main
import (
"io"
"log"
"os"
"os/exec"
)
func main() {
pReader, pWriter := io.Pipe()
cmd := exec.Command("echo", "Hello Go!\nThis is example")
cmd.Stdout = pWriter
go func() {
defer pReader.Close()
if _, err := io.Copy(os.Stdout, pReader); err != nil {
log.Fatal(err)
}
}()
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
-
通过
go run pipe.go
执行代码。 -
查看输出:
工作原理...
io.Pipe
函数创建内存管道,并返回管道的两端,一端是PipeReader
,另一端是PipeWriter
。对PipeWriter
的每次Write
都会被阻塞,直到另一端的Read
消耗。
该示例显示了从执行命令的输出到父程序的标准输出的管道输出。通过将pWriter
分配给cmd.Stdout
,子进程的标准输出被写入管道,goroutine
中的io.Copy
消耗写入的数据,将数据复制到os.Stdout
。
将对象序列化为二进制格式
除了众所周知的 JSON 和 XML 之外,Go 还提供了二进制格式gob
。本教程将介绍如何使用gob
包的基本概念。
操作步骤如下...
-
打开控制台并创建文件夹
chapter05/recipe10
。 -
导航到目录。
-
创建
gob.go
文件,内容如下:
package main
import (
"bytes"
"encoding/gob"
"fmt"
)
type User struct {
FirstName string
LastName string
Age int
Active bool
}
func (u User) String() string {
return fmt.Sprintf(`{"FirstName":%s,"LastName":%s,
"Age":%d,"Active":%v }`,
u.FirstName, u.LastName, u.Age, u.Active)
}
type SimpleUser struct {
FirstName string
LastName string
}
func (u SimpleUser) String() string {
return fmt.Sprintf(`{"FirstName":%s,"LastName":%s}`,
u.FirstName, u.LastName)
}
func main() {
var buff bytes.Buffer
// Encode value
enc := gob.NewEncoder(&buff)
user := User{
"Radomir",
"Sohlich",
30,
true,
}
enc.Encode(user)
fmt.Printf("%X\n", buff.Bytes())
// Decode value
out := User{}
dec := gob.NewDecoder(&buff)
dec.Decode(&out)
fmt.Println(out.String())
enc.Encode(user)
out2 := SimpleUser{}
dec.Decode(&out2)
fmt.Println(out2.String())
}
-
通过
go run gob.go
执行代码。 -
查看输出:
工作原理...
gob
序列化和反序列化需要编码器和解码器。gob.NewEncoder
函数创建具有底层Writer
的Encoder
。每次调用Encode
方法都会将对象序列化为gob
格式。gob
格式本身是自描述的二进制格式。这意味着每个序列化的结构都以其描述为前缀。
要从序列化形式解码数据,必须通过调用gob.NewDecoder
创建Decoder
,并使用底层的Reader
。然后,Decode
接受应将数据反序列化到的结构的指针。
注意,gob 格式不需要源和目标类型完全匹配。有关规则,请参考encoding
/gob
包。
读取和写入 ZIP 文件
ZIP 压缩是一种广泛使用的压缩格式。通常使用 ZIP 格式来上传文件集或者导出压缩文件作为输出。本教程将向您展示如何使用标准库以编程方式处理 ZIP 文件。
操作步骤如下...
-
打开控制台并创建文件夹
chapter05/recipe11
。 -
导航到目录。
-
创建
zip.go
文件,内容如下:
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
)
func main() {
var buff bytes.Buffer
// Compress content
zipW := zip.NewWriter(&buff)
f, err := zipW.Create("newfile.txt")
if err != nil {
panic(err)
}
_, err = f.Write([]byte("This is my file content"))
if err != nil {
panic(err)
}
err = zipW.Close()
if err != nil {
panic(err)
}
//Write output to file
err = ioutil.WriteFile("data.zip", buff.Bytes(), os.ModePerm)
if err != nil {
panic(err)
}
// Decompress the content
zipR, err := zip.OpenReader("data.zip")
if err != nil {
panic(err)
}
for _, file := range zipR.File {
fmt.Println("File " + file.Name + " contains:")
r, err := file.Open()
if err != nil {
log.Fatal(err)
}
_, err = io.Copy(os.Stdout, r)
if err != nil {
panic(err)
}
err = r.Close()
if err != nil {
panic(err)
}
fmt.Println()
}
}
-
通过
go run zip.go
执行代码。 -
查看输出:
工作原理...
内置包zip
包含NewWriter
和NewReader
函数,用于创建zip.Writer
以进行压缩,以及zip.Reader
以进行解压缩。
ZIP 文件的每个记录都是使用创建的zip.Writer
的Create
方法创建的。然后使用返回的Writer
来写入内容主体。
要解压文件,使用OpenReader
函数创建 zipped 文件中记录的ReadCloser
。创建的ReaderCloser
的File
字段是zip.File
指针的切片。通过调用Open
方法并读取返回的ReadCloser
来获取文件的内容。
只需在Create
方法的文件名中添加斜杠即可创建文件夹。例如folder/newfile.txt
。
有效解析大型 XML 文件
XML 是一种非常常见的数据交换格式。Go 库包含对解析 XML 文件的支持,方式与 JSON 相同。通常,使用与 XML 方案对应的结构,并借助此帮助一次解析 XML 内容。问题在于当 XML 文件太大而无法放入内存时,因此需要分块解析文件。这个示例将揭示如何处理大型 XML 文件并解析所需的信息。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe11
。 -
导航到目录。
-
创建
data.xml
文件,内容如下:
<?xml version="1.0"?>
<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.</description>
</book>
<book id="bk112">
<author>Galos, Mike</author>
<title>Visual Studio 7: A Comprehensive Guide</title>
<genre>Computer</genre>
<price>49.95</price>
<publish_date>2001-04-16</publish_date>
<description>Microsoft Visual Studio 7 is explored
in depth, looking at how Visual Basic, Visual C++, C#,
and ASP+ are integrated into a comprehensive development
environment.</description>
</book>
</catalog>
- 创建
xml.go
文件,内容如下:
package main
import (
"encoding/xml"
"fmt"
"os"
)
type Book struct {
Title string `xml:"title"`
Author string `xml:"author"`
}
func main() {
f, err := os.Open("data.xml")
if err != nil {
panic(err)
}
defer f.Close()
decoder := xml.NewDecoder(f)
// Read the book one by one
books := make([]Book, 0)
for {
tok, _ := decoder.Token()
if tok == nil {
break
}
switch tp := tok.(type) {
case xml.StartElement:
if tp.Name.Local == "book" {
// Decode the element to struct
var b Book
decoder.DecodeElement(&b, &tp)
books = append(books, b)
}
}
}
fmt.Println(books)
}
-
通过
go run xml.go
执行代码。 -
查看输出:
工作原理...
使用xml
包的NewDecoder
函数创建 XML 内容的Decoder
。
通过在Decoder
上调用Token
方法,接收xml.Token
。xml.Token
是保存令牌类型的接口。可以根据类型定义代码的行为。示例代码测试解析的xml.StartElement
是否是book
元素之一。然后将数据部分解析为Book
结构。这样,底层Decoder
中的Reader
中的指针位置将被结构数据移动,解析可以继续进行。
从不完整的 JSON 数组中提取数据
这个示例包含一个非常特定的用例,即您的程序从不可靠的来源消耗 JSON,而 JSON 包含一个具有开始标记[
的对象数组,但数组中的项目数量非常大,而 JSON 的结尾可能已损坏。
如何做...
-
打开控制台并创建文件夹
chapter05/recipe13
。 -
导航到目录。
-
创建
json.go
文件,内容如下:
package main
import (
"encoding/json"
"fmt"
"strings"
)
const js = `
[
{
"name":"Axel",
"lastname":"Fooley"
},
{
"name":"Tim",
"lastname":"Burton"
},
{
"name":"Tim",
"lastname":"Burton"
`
type User struct {
Name string `json:"name"`
LastName string `json:"lastname"`
}
func main() {
userSlice := make([]User, 0)
r := strings.NewReader(js)
dec := json.NewDecoder(r)
for {
tok, err := dec.Token()
if err != nil {
break
}
if tok == nil {
break
}
switch tp := tok.(type) {
case json.Delim:
str := tp.String()
if str == "" || str == "{" {
for dec.More() {
u := User{}
err := dec.Decode(&u)
if err == nil {
userSlice = append(userSlice, u)
} else {
break
}
}
}
}
}
fmt.Println(userSlice)
}
-
通过
go run json.go
执行代码。 -
查看输出:
![
工作原理...
除了Unmarshall
函数外,json
包还包含Decoder
API。使用NewDecoder
可以创建Decoder
。通过在解码器上调用Token
方法,可以读取底层Reader
并返回Token
接口。这可以保存多个值。
其中之一是Delim
类型,它是包含{
、[
、]
、}
中之一的 rune。基于此,检测到 JSON 数组的开始。通过解码器上的More
方法,可以检测到更多要解码的对象。
第六章:发现文件系统
本章包含以下示例:
-
获取文件信息
-
创建临时文件
-
写入文件
-
从多个 goroutine 写入文件
-
列出目录
-
更改文件权限
-
创建文件和目录
-
过滤文件列表
-
比较两个文件
-
解析用户主目录
介绍
本章将引导您完成文件和目录中的典型操作。我们还将介绍如何获取用户主目录并为其创建临时文件。
检查 Go 是否已正确安装。第一章的准备就绪部分中的检索 Golang 版本示例将对您有所帮助。
获取文件信息
如果您需要发现有关访问文件的基本信息,Go 的标准库提供了一种方法来完成这个任务。本示例展示了如何访问这些信息。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe01
。 -
导航到目录。
-
创建包含内容
This is test file
的示例test.file
。 -
创建包含以下内容的
fileinfo.go
文件:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("test.file")
if err != nil {
panic(err)
}
fi, err := f.Stat()
if err != nil {
panic(err)
}
fmt.Printf("File name: %v\n", fi.Name())
fmt.Printf("Is Directory: %t\n", fi.IsDir())
fmt.Printf("Size: %d\n", fi.Size())
fmt.Printf("Mode: %v\n", fi.Mode())
}
-
在主终端中运行
go run fileinfo.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
os.File
类型通过Stat
方法提供对FileInfo
类型的访问。FileInfo
结构包含有关文件的所有基本信息。
创建临时文件
临时文件通常在运行测试用例时使用,或者如果您的应用程序需要一个存储短期内容的地方,例如用户数据上传和当前处理的数据。本示例将介绍创建此类文件或目录的最简单方法。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe02
。 -
导航到目录。
-
创建包含以下内容的
tempfile.go
文件:
package main
import "io/ioutil"
import "os"
import "fmt"
func main() {
tFile, err := ioutil.TempFile("", "gostdcookbook")
if err != nil {
panic(err)
}
// The called is responsible for handling
// the clean up.
defer os.Remove(tFile.Name())
fmt.Println(tFile.Name())
// TempDir returns
// the path in string.
tDir, err := ioutil.TempDir("", "gostdcookbookdir")
if err != nil {
panic(err)
}
defer os.Remove(tDir)
fmt.Println(tDir)
}
-
在主终端中运行
go run tempfile.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
ioutil
包含TempFile
和TempDir
函数。TempFile
函数消耗目录和文件前缀。返回具有底层临时文件的os.File
。请注意,调用者负责清理文件。前面的示例使用os.Remove
函数来清理文件。
TempDir
函数的工作方式相同。不同之处在于返回包含目录路径的string
。
临时file
/dir
名称由前缀和随机后缀组成。多个调用具有相同参数的TempFile
/Dir
函数的程序将不会获得相同的结果。
写入文件
写入文件是每个程序员的基本任务;Go 支持多种方法来完成这个任务。本示例将展示其中一些方法。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe03
。 -
导航到目录。
-
创建包含以下内容的
writefile.go
文件:
package main
import (
"io"
"os"
"strings"
)
func main() {
f, err := os.Create("sample.file")
if err != nil {
panic(err)
}
defer f.Close()
_, err = f.WriteString("Go is awesome!\n")
if err != nil {
panic(err)
}
_, err = io.Copy(f, strings.NewReader("Yeah! Go
is great.\n"))
if err != nil {
panic(err)
}
}
-
在主终端中运行
go run writefile.go
来执行代码。 -
检查创建的
sample.file
的内容:
它是如何工作的...
os.File
类型实现了Writer
接口,因此可以通过使用Writer
接口的任何选项来写入文件。前面的示例使用了os.File
类型的WriteString
方法。通用的io.WriteString
方法也可以使用。
从多个 goroutine 写入文件
本示例将向您展示如何安全地从多个 goroutine 写入文件。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe04
。 -
导航到目录。
-
创建包含以下内容的
syncwrite.go
文件:
package main
import (
"fmt"
"io"
"os"
"sync"
)
type SyncWriter struct {
m sync.Mutex
Writer io.Writer
}
func (w *SyncWriter) Write(b []byte) (n int, err error) {
w.m.Lock()
defer w.m.Unlock()
return w.Writer.Write(b)
}
var data = []string{
"Hello!",
"Ola!",
"Ahoj!",
}
func main() {
f, err := os.Create("sample.file")
if err != nil {
panic(err)
}
wr := &SyncWriter{sync.Mutex{}, f}
wg := sync.WaitGroup{}
for _, val := range data {
wg.Add(1)
go func(greetings string) {
fmt.Fprintln(wr, greetings)
wg.Done()
}(val)
}
wg.Wait()
}
-
在主终端中运行
go run syncwrite.go
来执行代码。 -
检查创建的
sample.file
的内容:
它是如何工作的...
并发写入文件是一个可能导致文件内容不一致的问题。最好通过使用Mutex
或任何其他同步原语来同步对文件的写入。这样,您可以确保一次只有一个 goroutine 能够写入文件。
上述代码创建了一个带有Mutex
的Writer
,它嵌入了Writer
(在本例中是os.File
),对于每个Write
调用,内部锁定Mutex
以提供排他性。写操作完成后,Mutex
原语会自然解锁。
列出目录
这个示例将向您展示如何列出目录内容。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe05
。 -
导航到目录。
-
创建一个名为
folder
的目录。 -
创建
listdir.go
文件,并包含以下内容:
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
func main() {
fmt.Println("List by ReadDir")
listDirByReadDir(".")
fmt.Println()
fmt.Println("List by Walk")
listDirByWalk(".")
}
func listDirByWalk(path string) {
filepath.Walk(path, func(wPath string, info os.FileInfo,
err error) error {
// Walk the given dir
// without printing out.
if wPath == path {
return nil
}
// If given path is folder
// stop list recursively and print as folder.
if info.IsDir() {
fmt.Printf("[%s]\n", wPath)
return filepath.SkipDir
}
// Print file name
if wPath != path {
fmt.Println(wPath)
}
return nil
})
}
func listDirByReadDir(path string) {
lst, err := ioutil.ReadDir(path)
if err != nil {
panic(err)
}
for _, val := range lst {
if val.IsDir() {
fmt.Printf("[%s]\n", val.Name())
} else {
fmt.Println(val.Name())
}
}
}
-
在主终端中运行
go run listdir.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
上面的示例中的文件夹列表使用了两种方法。第一种更简单的方法是使用listDirByReadDir
函数,并利用ioutil
包中的ReadDir
函数。此函数返回表示实际目录内容的FileInfo
结构的切片。请注意,ReadDir
函数不会递归读取文件夹。实际上,ReadDir
函数在内部使用os
包中File
类型的Readdir
方法。
另一方面,更复杂的listDirByWalk
使用filepath.Walk
函数,该函数消耗要遍历的路径,并具有处理给定路径中的每个文件或文件夹的函数。主要区别在于Walk
函数递归读取目录。这种方法的核心部分是WalkFunc
类型,其函数是消耗列表的结果。请注意,该函数通过返回filepath.SkipDir
错误来阻止基础文件夹上的递归调用。Walk
函数还首先处理调用路径,因此您也需要处理这一点(在本例中,我们跳过打印并返回 nil,因为我们需要递归处理此文件夹)。
更改文件权限
这个示例说明了如何以编程方式更改文件权限。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe06
。 -
导航到目录。
-
创建
filechmod.go
文件,并包含以下内容:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Create("test.file")
if err != nil {
panic(err)
}
defer f.Close()
// Obtain current permissions
fi, err := f.Stat()
if err != nil {
panic(err)
}
fmt.Printf("File permissions %v\n", fi.Mode())
// Change permissions
err = f.Chmod(0777)
if err != nil {
panic(err)
}
fi, err = f.Stat()
if err != nil {
panic(err)
}
fmt.Printf("File permissions %v\n", fi.Mode())
}
-
在主终端中运行
go run filechmod.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
os
包中File
类型的Chmod
方法可用于更改文件权限。上面的示例只是创建文件并将权限更改为0777
。
只需注意fi.Mode()
被调用两次,因为它提取了文件当前状态的权限(os.FileMode
)。
更改权限的最简单方法是使用os.Chmod
函数,它执行相同的操作,但您不需要在代码中获取File
类型。
创建文件和目录
这个示例描述了在代码中创建文件和目录的几种一般方法。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe07
。 -
导航到目录。
-
创建
create.go
文件,并包含以下内容:
package main
import (
"os"
)
func main() {
f, err := os.Create("created.file")
if err != nil {
panic(err)
}
f.Close()
f, err = os.OpenFile("created.byopen", os.O_CREATE|os.O_APPEND,
os.ModePerm)
if err != nil {
panic(err)
}
f.Close()
err = os.Mkdir("createdDir", 0777)
if err != nil {
panic(err)
}
err = os.MkdirAll("sampleDir/path1/path2", 0777)
if err != nil {
panic(err)
}
}
-
在主终端中运行
go run create.go
来执行代码。 -
列出
chapter06/recipe07
目录的内容:
它是如何工作的...
前面的示例代表了创建文件或目录的四种方法。os.Create
函数是创建文件的最简单方法。使用此函数,您将以0666
的权限创建文件。
如果需要使用任何其他权限配置创建文件,则应使用os
包的OpenFile
函数。
可以使用os
包的Mkdir
函数创建目录。这样,将创建具有给定权限的目录。第二个选项是使用MkdirAll
函数。此函数还会创建目录,但如果给定路径包含不存在的目录,则会创建路径中的所有目录(它与 Unix 的mkdir
实用程序的-p
选项的工作方式相同)。
过滤文件列表
本教程向您展示了如何列出与给定模式匹配的文件路径。列表不必来自同一文件夹。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe08
。 -
导航到目录。
-
创建
filter.go
文件,内容如下:
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
for i := 1; i <= 6; i++ {
_, err := os.Create(fmt.Sprintf("./test.file%d", i))
if err != nil {
fmt.Println(err)
}
}
m, err := filepath.Glob("./test.file[1-3]")
if err != nil {
panic(err)
}
for _, val := range m {
fmt.Println(val)
}
// Cleanup
for i := 1; i <= 6; i++ {
err := os.Remove(fmt.Sprintf("./test.file%d", i))
if err != nil {
fmt.Println(err)
}
}
}
-
在主终端中运行
go run filter.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
要获取与给定模式对应的过滤文件列表,可以使用filepath
包中的Glob
函数。有关模式语法,请参阅filepath.Match
函数的文档(golang.org/pkg/path/filepath/#Match
)。
请注意,filepath.Glob
的返回结果是与匹配路径对应的字符串切片。
另请参阅
本章的列出目录教程展示了更通用的方法,其中可以使用filepath.Walk
函数来列出和过滤路径。
比较两个文件
本教程为您提供了如何比较两个文件的提示。本教程将向您展示如何快速确定文件是否相同。本教程还将向您展示如何找到两者之间的差异。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe09
。 -
导航到目录。
-
创建
comparison.go
文件,内容如下:
package main
import (
"bufio"
"crypto/md5"
"fmt"
"io"
"os"
)
var data = []struct {
name string
cont string
perm os.FileMode
}{
{"test1.file", "Hello\nGolang is great", 0666},
{"test2.file", "Hello\nGolang is great", 0666},
{"test3.file", "Not matching\nGolang is great\nLast line",
0666},
}
func main() {
files := []*os.File{}
for _, fData := range data {
f, err := os.Create(fData.name)
if err != nil {
panic(err)
}
defer f.Close()
_, err = io.WriteString(f, fData.cont)
if err != nil {
panic(err)
}
files = append(files, f)
}
// Compare by checksum
checksums := []string{}
for _, f := range files {
f.Seek(0, 0) // reset to beginning of file
sum, err := getMD5SumString(f)
if err != nil {
panic(err)
}
checksums = append(checksums, sum)
}
fmt.Println("### Comparing by checksum ###")
compareCheckSum(checksums[0], checksums[1])
compareCheckSum(checksums[0], checksums[2])
fmt.Println("### Comparing line by line ###")
files[0].Seek(0, 0)
files[2].Seek(0, 0)
compareFileByLine(files[0], files[2])
// Cleanup
for _, val := range data {
os.Remove(val.name)
}
}
func getMD5SumString(f *os.File) (string, error) {
file1Sum := md5.New()
_, err := io.Copy(file1Sum, f)
if err != nil {
return "", err
}
return fmt.Sprintf("%X", file1Sum.Sum(nil)), nil
}
func compareCheckSum(sum1, sum2 string) {
match := "match"
if sum1 != sum2 {
match = " does not match"
}
fmt.Printf("Sum: %s and Sum: %s %s\n", sum1, sum2, match)
}
func compareLines(line1, line2 string) {
sign := "o"
if line1 != line2 {
sign = "x"
}
fmt.Printf("%s | %s | %s \n", sign, line1, line2)
}
func compareFileByLine(f1, f2 *os.File) {
sc1 := bufio.NewScanner(f1)
sc2 := bufio.NewScanner(f2)
for {
sc1Bool := sc1.Scan()
sc2Bool := sc2.Scan()
if !sc1Bool && !sc2Bool {
break
}
compareLines(sc1.Text(), sc2.Text())
}
}
-
在主终端中运行
go run comparison.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
可以通过几种方式来比较两个文件。本教程描述了两种基本方法。第一种方法是通过创建文件的校验和来比较整个文件。
第三章的生成校验和教程展示了如何创建文件的校验和。这种方式,getMD5SumString
函数生成校验和字符串,它是 MD5 字节结果的十六进制表示。然后比较这些字符串。
第二种方法是逐行比较文件(在本例中是字符串内容)。如果行不匹配,则包括x
标记。这是您可以比较二进制内容的方式,但您需要按字节块(字节切片)扫描文件。
解析用户主目录
例如,程序知道用户的主目录可能是有益的,例如,如果您需要存储自定义用户配置或与用户相关的任何其他数据。本教程将描述如何找到当前用户的主目录。
如何做...
-
打开控制台并创建文件夹
chapter06/recipe10
。 -
导航到目录。
-
创建
home.go
文件,内容如下:
package main
import (
"fmt"
"log"
"os/user"
)
func main() {
usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
fmt.Println("The user home directory: " + usr.HomeDir)
}
-
在主终端中运行
go run home.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
os/user
包包含Current
函数,它提供os.User
类型的指针。User
包含HomeDir
属性,其中包含当前用户主目录的路径。
请注意,这对于交叉编译的代码不起作用,因为实现取决于本机代码。
第七章:连接网络
本章包含以下示例:
-
解析本地 IP 地址
-
连接到远程服务器
-
通过 IP 地址解析域名,反之亦然
-
连接到 HTTP 服务器
-
解析和构建 URL
-
创建 HTTP 请求
-
读取和写入 HTTP 头
-
处理 HTTP 重定向
-
使用 RESTful API
-
发送简单的电子邮件
-
调用 JSON-RPC 服务
介绍
本章主要讨论网络。本章中的大多数示例都集中在客户端。我们将介绍如何解析有关机器、域名和 IP 解析的基本信息,以及如何通过 TCP 相关协议(如 HTTP 和 SMTP)进行连接。最后,我们将使用标准库进行 JSON-RCP 1.0 的远程过程调用。
检查 Go 是否已正确安装。第一章中的准备就绪部分中的检索 Golang 版本示例,与环境交互,将有所帮助。验证是否有其他应用程序阻止了7070
端口。
解析本地 IP 地址
本示例解释了如何从可用的本地接口中检索 IP 地址。
如何做...
-
打开控制台并创建文件夹
chapter07/recipe01
。 -
导航到目录。
-
创建
interfaces.go
文件,内容如下:
package main
import (
"fmt"
"net"
)
func main() {
// Get all network interfaces
interfaces, err := net.Interfaces()
if err != nil {
panic(err)
}
for _, interf := range interfaces {
// Resolve addresses
// for each interface
addrs, err := interf.Addrs()
if err != nil {
panic(err)
}
fmt.Println(interf.Name)
for _, add := range addrs {
if ip, ok := add.(*net.IPNet); ok {
fmt.Printf("\t%v\n", ip)
}
}
}
}
-
在主终端中运行
go run interfaces.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
net 包包含Interfaces
函数,它将网络接口列为Interface
结构的切片。Interface
结构具有Addrs
方法,它列出可用的网络地址。这样,您可以按接口列出地址。
另一个选项是使用net
包的InterfaceAddrs
函数,它提供了实现Addr
接口的结构体切片。这为您提供了获取所需信息的方法。
连接到远程服务器
基于 TCP 的协议是网络通信中最重要的协议。作为提醒,HTTP、FTP、SMTP 和其他协议都属于这一组。本示例让您了解如何一般连接到 TCP 服务器。
如何做...
-
打开控制台并创建文件夹
chapter07/recipe02
。 -
导航到目录。
-
创建
tcpclient.go
文件,内容如下:
package main
import (
"bufio"
"context"
"fmt"
"io"
"net"
"net/http"
"time"
)
type StringServer string
func (s StringServer) ServeHTTP(rw http.ResponseWriter,
req *http.Request) {
rw.Write([]byte(string(s)))
}
func createServer(addr string) http.Server {
return http.Server{
Addr: addr,
Handler: StringServer("HELLO GOPHER!\n"),
}
}
const addr = "localhost:7070"
func main() {
s := createServer(addr)
go s.ListenAndServe()
// Connect with plain TCP
conn, err := net.Dial("tcp", addr)
if err != nil {
panic(err)
}
defer conn.Close()
_, err = io.WriteString(conn, "GET / HTTP/1.1\r\nHost:
localhost:7070\r\n\r\n")
if err != nil {
panic(err)
}
scanner := bufio.NewScanner(conn)
conn.SetReadDeadline(time.Now().Add(time.Second))
for scanner.Scan() {
fmt.Println(scanner.Text())
}
ctx, _ := context.WithTimeout(context.Background(),
5*time.Second)
s.Shutdown(ctx)
}
-
在主终端中运行
go run tcpclient.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
net 包包含Dial
函数,它消耗网络类型和地址。在前面的示例中,网络是tcp
,地址是localhost:8080
。
一旦Dial
函数成功,就会返回Conn
类型,它作为已打开套接字的引用。Conn
接口还定义了Read
和Write
函数,因此它们可以用作写入和从套接字读取的Writer
和Reader
函数。最后,示例代码使用Scanner
来获取响应。请注意,这种情况下Scanner
可以工作是因为有换行符。否则,应该使用更通用的Read
方法。在示例中,通过SetReadDeadline
方法设置了Read
截止日期。关键之处在于截止日期不是持续时间,而是Time
。这意味着截止日期被设置为将来的时间点。如果您在循环中从套接字读取数据并需要将读取超时设置为 10 秒,则每次迭代都应包含类似于conn.SetReadDeadline(time.Now().Add(10*time.Second))
的代码。
只是为了解释整个代码示例,使用了HTTP
标准包中的 HTTP 服务器作为客户端的对应部分。这部分在另一个示例中有所涵盖。
通过 IP 地址解析域名,反之亦然
这个教程将介绍如何将 IP 地址转换为主机地址,反之亦然。
如何做到...
-
打开控制台并创建文件夹
chapter07/recipe03
。 -
导航到目录。
-
创建
lookup.go
文件,内容如下:
package main
import (
"fmt"
"net"
)
func main() {
// Resolve by IP
addrs, err := net.LookupAddr("127.0.0.1")
if err != nil {
panic(err)
}
for _, addr := range addrs {
fmt.Println(addr)
}
//Resolve by address
ips, err := net.LookupIP("localhost")
if err != nil {
panic(err)
}
for _, ip := range ips {
fmt.Println(ip.String())
}
}
-
在主终端中运行
go run lookup.go
来执行代码。 -
你将看到以下输出:
它是如何工作的...
从 IP 地址解析域名可以使用net
包中的LookupAddr
函数来完成。要从域名找出 IP 地址,应用LookupIP
函数。
连接到 HTTP 服务器
前面的教程连接到远程服务器让我们深入了解了如何在较低级别连接 TCP 服务器。在这个教程中,将展示如何在较高级别与 HTTP 服务器通信。
如何做到...
-
打开控制台并创建文件夹
chapter07/recipe04
。 -
导航到目录。
-
创建
http.go
文件,内容如下:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
type StringServer string
func (s StringServer) ServeHTTP(rw http.ResponseWriter,
req *http.Request) {
req.ParseForm()
fmt.Printf("Received form data: %v\n", req.Form)
rw.Write([]byte(string(s)))
}
func createServer(addr string) http.Server {
return http.Server{
Addr: addr,
Handler: StringServer("Hello world"),
}
}
const addr = "localhost:7070"
func main() {
s := createServer(addr)
go s.ListenAndServe()
useRequest()
simplePost()
}
func simplePost() {
res, err := http.Post("http://localhost:7070",
"application/x-www-form-urlencoded",
strings.NewReader("name=Radek&surname=Sohlich"))
if err != nil {
panic(err)
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
res.Body.Close()
fmt.Println("Response from server:" + string(data))
}
func useRequest() {
hc := http.Client{}
form := url.Values{}
form.Add("name", "Radek")
form.Add("surname", "Sohlich")
req, err := http.NewRequest("POST",
"http://localhost:7070",
strings.NewReader(form.Encode()))
req.Header.Add("Content-Type",
"application/x-www-form-urlencoded")
res, err := hc.Do(req)
if err != nil {
panic(err)
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
res.Body.Close()
fmt.Println("Response from server:" + string(data))
}
-
在主终端中运行
go run http.go
来执行代码。 -
你将看到以下输出:
它是如何工作的...
连接到 HTTP 服务器可以借助net/http
包来完成。当然,你还有其他方法可以实现这一点,但上面的代码说明了两种最常见的方法。第一种选项实现了simplePost
函数,并且演示了使用默认客户端。这里选择了 POST 方法,因为它比 GET 更复杂。Post
方法接受 URL、内容类型和Reader
形式的主体。调用Post
函数立即请求服务器并返回结果。
请注意,Post
方法只是在其实现中使用了http.DefaultClient
的一个包装函数。net/http
包还包含Get
函数。
useRequest
函数实现了相同的功能,但使用了更可定制的 API 和自己的Client
实例。该实现利用NewRequest
函数根据给定的参数创建请求:方法、URL 和请求主体。内容类型必须单独设置到Header
属性中。请求是通过Client
上创建的Do
方法执行的。
另请参阅
创建一个 HTTP 请求的教程将帮助您详细组装请求。
解析和构建 URL
在许多情况下,最好使用方便的工具来操作 URL,而不是试图将其作为简单的字符串处理。Go 标准库自然包含了操作 URL 的工具。这个教程将介绍其中一些主要功能。
如何做到...
-
打开控制台并创建文件夹
chapter07/recipe05
。 -
导航到目录。
-
创建
url.go
文件,内容如下:
package main
import (
"encoding/json"
"fmt"
"net/url"
)
func main() {
u := &url.URL{}
u.Scheme = "http"
u.Host = "localhost"
u.Path = "index.html"
u.RawQuery = "id=1&name=John"
u.User = url.UserPassword("admin", "1234")
fmt.Printf("Assembled URL:\n%v\n\n\n", u)
parsedURL, err := url.Parse(u.String())
if err != nil {
panic(err)
}
jsonURL, err := json.Marshal(parsedURL)
if err != nil {
panic(err)
}
fmt.Println("Parsed URL:")
fmt.Println(string(jsonURL))
}
-
在主终端中运行
go run url.go
来执行代码。 -
你将看到以下输出:
它是如何工作的...
net/url
包旨在帮助您操作和解析 URL。URL
结构包含了组合 URL 所需的字段。通过URL
结构的String
方法,可以轻松地将其转换为简单的字符串。
当字符串表示可用且需要额外操作时,可以利用net/url
的Parse
函数。这样,字符串可以转换为URL
结构,并且可以修改底层 URL。
创建一个 HTTP 请求
这个教程将向您展示如何使用特定参数构造 HTTP 请求。
如何做到...
-
打开控制台并创建文件夹
chapter07/recipe06
。 -
导航到目录。
-
创建
request.go
文件,内容如下:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
type StringServer string
func (s StringServer) ServeHTTP(rw http.ResponseWriter,
req *http.Request) {
req.ParseForm()
fmt.Printf("Received form data: %v\n", req.Form)
fmt.Printf("Received header: %v\n", req.Header)
rw.Write([]byte(string(s)))
}
func createServer(addr string) http.Server {
return http.Server{
Addr: addr,
Handler: StringServer("Hello world"),
}
}
const addr = "localhost:7070"
func main() {
s := createServer(addr)
go s.ListenAndServe()
form := url.Values{}
form.Set("id", "5")
form.Set("name", "Wolfgang")
req, err := http.NewRequest(http.MethodPost,
"http://localhost:7070",
strings.NewReader(form.Encode()))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type",
"application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
panic(err)
}
res.Body.Close()
fmt.Println("Response from server:" + string(data))
}
-
在主终端中运行
go run request.go
来执行代码。 -
你将看到以下输出:
它是如何工作的...
构造请求的更复杂的方式在示例代码中呈现。使用net/http
包的NewRequest
方法返回Request
结构的指针。该函数消耗方法的请求、URL 和请求的主体。注意表单的构建方式。使用url.Values
结构而不是使用普通字符串。最后,调用Encode
方法对给定的表单值进行编码。通过请求的http.Header
属性设置头。
读取和写入 HTTP 头
前面的示例描述了如何一般创建 HTTP 请求。本示例将详细介绍如何读取和写入请求头。
如何做...
-
打开控制台并创建文件夹
chapter07/recipe07
。 -
导航到目录。
-
创建包含以下内容的
headers.go
文件:
package main
import (
"fmt"
"net/http"
)
func main() {
header := http.Header{}
// Using the header as slice
header.Set("Auth-X", "abcdef1234")
header.Add("Auth-X", "defghijkl")
fmt.Println(header)
// retrieving slice of values in header
resSlice := header["Auth-X"]
fmt.Println(resSlice)
// get the first value
resFirst := header.Get("Auth-X")
fmt.Println(resFirst)
// replace all existing values with
// this one
header.Set("Auth-X", "newvalue")
fmt.Println(header)
// Remove header
header.Del("Auth-X")
fmt.Println(header)
}
-
在主终端中运行
go run headers.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
http
包中的头实际上表示为map[string][]string
,因此必须处理Header
类型。前面的代码显示了如何设置和读取头值。关于头的重要事情是头键的值是string
切片。因此,头中的每个键可以包含多个值。
Header
类型的Set
方法设置给定键下的单项切片。另一方面,Add
方法将值附加到切片。
使用Get
方法将从给定键下的切片中检索第一个值。如果需要整个切片,则需要将Header
处理为映射。可以使用Del
方法删除整个头键。
服务器和客户端都使用http
包的Request
和Header
类型,因此在服务器端和客户端端的处理方式相同。
处理 HTTP 重定向
在某些情况下,您需要更多控制重定向的处理方式。本示例将向您展示 Go 客户端实现的机制,以便您更多地控制处理 HTTP 重定向。
如何做...
-
打开控制台并创建文件夹
chapter07/recipe08
。 -
导航到目录。
-
创建包含以下内容的
redirects.go
文件:
package main
import (
"fmt"
"net/http"
)
const addr = "localhost:7070"
type RedirecServer struct {
redirectCount int
}
func (s *RedirecServer) ServeHTTP(rw http.ResponseWriter,
req *http.Request) {
s.redirectCount++
fmt.Println("Received header: " +
req.Header.Get("Known-redirects"))
http.Redirect(rw, req, fmt.Sprintf("/redirect%d",
s.redirectCount), http.StatusTemporaryRedirect)
}
func main() {
s := http.Server{
Addr: addr,
Handler: &RedirecServer{0},
}
go s.ListenAndServe()
client := http.Client{}
redirectCount := 0
// If the count of redirects is reached
// than return error.
client.CheckRedirect = func(req *http.Request,
via []*http.Request) error {
fmt.Println("Redirected")
if redirectCount > 2 {
return fmt.Errorf("Too many redirects")
}
req.Header.Set("Known-redirects", fmt.Sprintf("%d",
redirectCount))
redirectCount++
for _, prReq := range via {
fmt.Printf("Previous request: %v\n", prReq.URL)
}
return nil
}
_, err := client.Get("http://" + addr)
if err != nil {
panic(err)
}
}
-
在主终端中运行
go run redirects.go
来执行代码。 -
您将看到以下输出:
它是如何工作...
http
包的Client
包含CheckRedirect
字段。该字段是一个具有req
和via
参数的函数。req
是即将到来的请求,via
指的是以前的请求。这样,您可以在重定向后修改请求。在前面的示例中,修改了Known-redirects
头。
如果CheckRedirect
函数返回错误,则返回带有包装错误的关闭主体的最后响应。如果返回http.ErrUseLastResponse
,则返回最后的响应,但主体未关闭,因此可以读取它。
默认情况下,CheckRedirect
属性为 nil。在这种情况下,它最多有 10 次重定向。超过此计数后,重定向将停止。
消费 RESTful API
RESTful API 是应用程序和服务器提供其服务访问的最常见方式。本示例将向您展示如何使用标准库中的 HTTP 客户端来消费它。
如何做...
-
打开控制台并创建文件夹
chapter07/recipe09
。 -
导航到目录。
-
创建包含以下内容的
rest.go
文件:
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strconv"
"strings"
)
const addr = "localhost:7070"
type City struct {
ID string
Name string `json:"name"`
Location string `json:"location"`
}
func (c City) toJson() string {
return fmt.Sprintf(`{"name":"%s","location":"%s"}`,
c.Name, c.Location)
}
func main() {
s := createServer(addr)
go s.ListenAndServe()
cities, err := getCities()
if err != nil {
panic(err)
}
fmt.Printf("Retrived cities: %v\n", cities)
city, err := saveCity(City{"", "Paris", "France"})
if err != nil {
panic(err)
}
fmt.Printf("Saved city: %v\n", city)
}
func saveCity(city City) (City, error) {
r, err := http.Post("http://"+addr+"/cities",
"application/json",
strings.NewReader(city.toJson()))
if err != nil {
return City{}, err
}
defer r.Body.Close()
return decodeCity(r.Body)
}
func getCities() ([]City, error) {
r, err := http.Get("http://" + addr + "/cities")
if err != nil {
return nil, err
}
defer r.Body.Close()
return decodeCities(r.Body)
}
func decodeCity(r io.Reader) (City, error) {
city := City{}
dec := json.NewDecoder(r)
err := dec.Decode(&city)
return city, err
}
func decodeCities(r io.Reader) ([]City, error) {
cities := []City{}
dec := json.NewDecoder(r)
err := dec.Decode(&cities)
return cities, err
}
func createServer(addr string) http.Server {
cities := []City{City{"1", "Prague", "Czechia"},
City{"2", "Bratislava", "Slovakia"}}
mux := http.NewServeMux()
mux.HandleFunc("/cities", func(w http.ResponseWriter,
r *http.Request) {
enc := json.NewEncoder(w)
if r.Method == http.MethodGet {
enc.Encode(cities)
} else if r.Method == http.MethodPost {
data, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), 500)
}
r.Body.Close()
city := City{}
json.Unmarshal(data, &city)
city.ID = strconv.Itoa(len(cities) + 1)
cities = append(cities, city)
enc.Encode(city)
}
})
return http.Server{
Addr: addr,
Handler: mux,
}
}
-
在主终端中运行
go run rest.go
来执行代码。 -
您将看到以下输出:
它是如何工作的...
前面的示例代码显示了 REST API 的样子以及如何使用它。请注意,decodeCity
和decodeCities
函数受益于请求的Body
实现了Reader
接口。结构的反序列化通过json.Decoder
完成。
发送简单的电子邮件
本教程将简要介绍如何使用标准库连接到 SMTP 服务器并发送电子邮件。
准备工作
在本教程中,我们将使用谷歌 Gmail 账户发送电子邮件。通过一些配置,本教程也适用于其他 SMTP 服务器。
如何做...
-
打开控制台并创建文件夹
chapter07/recipe10
。 -
导航到目录。
-
创建
smtp.go
文件,内容如下:
package main
import (
"crypto/tls"
"fmt"
"net/smtp"
)
func main() {
var email string
fmt.Println("Enter username for smtp: ")
fmt.Scanln(&email)
var pass string
fmt.Println("Enter password for smtp: ")
fmt.Scanln(&pass)
auth := smtp.PlainAuth("", email, pass, "smtp.gmail.com")
c, err := smtp.Dial("smtp.gmail.com:587")
if err != nil {
panic(err)
}
defer c.Close()
config := &tls.Config{ServerName: "smtp.gmail.com"}
if err = c.StartTLS(config); err != nil {
panic(err)
}
if err = c.Auth(auth); err != nil {
panic(err)
}
if err = c.Mail(email); err != nil {
panic(err)
}
if err = c.Rcpt(email); err != nil {
panic(err)
}
w, err := c.Data()
if err != nil {
panic(err)
}
msg := []byte("Hello this is content")
if _, err := w.Write(msg); err != nil {
panic(err)
}
err = w.Close()
if err != nil {
panic(err)
}
err = c.Quit()
if err != nil {
panic(err)
}
}
-
在主终端中运行
go run smtp.go
来执行代码。 -
输入账户的电子邮件(谷歌账户)并按Enter。
-
输入账户的密码并按Enter。
-
在检查电子邮箱之前,您将看到以下输出:
工作原理...
smtp
包提供了与 SMTP 服务器交互的基本功能。Dial
函数提供客户端。客户端最重要的方法是Mail
,用于设置发件人邮件,Rcpt
,用于设置收件人邮件,以及Data
,提供Writer
,用于写入邮件内容。最后,Quit
方法发送 QUIT 并关闭与服务器的连接。
前面的示例使用了安全连接到 SMTP 服务器,因此客户端的Auth
方法用于设置身份验证,并调用StartTLS
方法以启动与服务器的安全连接。
请注意,Auth
结构是通过smtp
包的PlainAuth
函数单独创建的。
调用 JSON-RPC 服务
本教程将说明如何使用标准库调用 JSON-RPC 协议的过程。
如何做...
-
打开控制台并创建文件夹
chapter07/recipe11
。 -
导航到目录。
-
创建
jsonrpc.go
文件,内容如下:
package main
import (
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
type Args struct {
A, B int
}
type Result int
type RpcServer struct{}
func (t RpcServer) Add(args *Args, result *Result) error {
log.Printf("Adding %d to %d\n", args.A, args.B)
*result = Result(args.A + args.B)
return nil
}
const addr = ":7070"
func main() {
go createServer(addr)
client, err := jsonrpc.Dial("tcp", addr)
if err != nil {
panic(err)
}
defer client.Close()
args := &Args{
A: 2,
B: 3,
}
var result Result
err = client.Call("RpcServer.Add", args, &result)
if err != nil {
log.Fatalf("error in RpcServer", err)
}
log.Printf("%d+%d=%d\n", args.A, args.B, result)
}
func createServer(addr string) {
server := rpc.NewServer()
err := server.Register(RpcServer{})
if err != nil {
panic(err)
}
l, e := net.Listen("tcp", addr)
if e != nil {
log.Fatalf("Couldn't start listening on %s errors: %s",
addr, e)
}
for {
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
go server.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
-
在主终端中运行
go run jsonrpc.go
来执行代码。 -
您将看到以下输出:
工作原理...
Go 的标准库作为其内置包的一部分实现了 JSON-RPC 1.0。jsonrpc
包实现了Dial
函数,用于生成调用远程过程的客户端。客户端本身包含Call
方法,接受过程调用、参数和结果存储的指针。
createServer
将创建一个示例服务器来测试客户端调用。
HTTP 协议可以用作 JSON-RPC 的传输层。net/rpc
包包含DialHTTP
函数,能够创建客户端并调用远程过程。
第八章:使用数据库
本章包含以下配方:
-
连接数据库
-
验证连接
-
执行语句
-
使用预处理语句进行操作
-
取消挂起的查询
-
读取查询结果元数据
-
从查询结果中检索数据
-
将查询结果解析为映射
-
处理事务
-
执行存储过程和函数
介绍
每个数据库服务器都有自己的特点,而且协议也不同。自然地,语言库内部与数据库通信必须定制以适用于特定协议。
Go 标准库提供了用于与数据库服务器通信和操作的统一 API。此 API 位于sql
包中。要使用特定的数据库服务器,必须导入驱动程序。此驱动程序需要符合sql
包的规范。这样,您将能够受益于统一的方法。在本章中,我们将描述数据库操作的基础知识、事务处理以及如何使用存储过程。请注意,我们将在 PostgreSQL 数据库上说明该方法,但这些方法适用于大多数其他数据库。
连接数据库
与数据库工作的关键部分是与数据库本身的连接。Go 标准包仅涵盖了与数据库交互的抽象,必须使用第三方驱动程序。
在本配方中,我们将展示如何连接到 PostgreSQL 数据库。但是,这种方法适用于所有其他驱动程序实现了标准 API 的数据库。
准备就绪
通过在终端中调用go version
命令验证 Go 是否已正确安装。如果命令失败,请执行以下操作:
-
通过
go get -u github.com/lib/pq
获取 PostgreSQL 驱动程序 -
安装 PostgreSQL 数据库服务器(可选择使用 Docker 镜像而不是安装到主机系统)
-
我们将使用默认用户
postgres
和密码postgres
-
创建名为
example
的数据库
如何做...
-
打开控制台并创建文件夹
chapter08/recipe01
。 -
导航到目录。
-
使用以下内容创建
connect.go
文件:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
func main() {
connStr := "postgres://postgres:postgres@
localhost:5432/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
panic(err)
}
fmt.Println("Ping OK")
}
-
通过
go run connect.go
执行代码。 -
查看输出:
工作原理...
标准库包database/sql
提供了Open
函数,用于使用驱动程序名称和连接详细信息(在本例中为连接 URL)初始化与数据库的连接。请注意,Open
函数不会立即创建连接,可能只会验证传递给函数的参数。
可以通过返回的DB
结构指针中可用的Ping
方法验证与数据库的连接。
驱动程序本身在driver
包的init
函数中初始化。驱动程序通过sql
包的Register
函数向驱动程序名称注册自身。github.com/lib/pq
驱动程序将自身注册为postgres
。
验证连接
驱动程序实现中的数据库连接可能被池化,并且可能从池中拉出的连接已经断开。本配方将展示如何验证连接是否仍然有效。
准备就绪
通过在终端中调用go version
命令验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备就绪部分进行操作。
如何做...
-
打开控制台并创建文件夹
chapter08/recipe02
。 -
导航到目录。
-
使用以下内容创建
verify.go
文件:
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq"
)
func main() {
connStr := "postgres://postgres:postgres@
localhost:5432/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
panic(err)
}
fmt.Println("Ping OK.")
ctx, _ := context.WithTimeout(context.Background(),
time.Nanosecond)
err = db.PingContext(ctx)
if err != nil {
fmt.Println("Error: " + err.Error())
}
// Verify the connection is
conn, err := db.Conn(context.Background())
if err != nil {
panic(err)
}
defer conn.Close()
err = conn.PingContext(context.Background())
if err != nil {
panic(err)
}
fmt.Println("Connection Ping OK.")
}
-
通过
go run verify.go
执行代码。 -
查看输出:
工作原理...
如前一篇中提到的连接数据库,Open
函数可能只是验证连接细节,但不一定立即连接数据库。实际连接到数据库通常是延迟加载的,并且是通过对数据库的第一次语句执行创建的。
DB
结构的指针提供了Ping
方法,通常对数据库进行幂等调用。Ping
方法的变体是PingContext
,它只是添加了取消或超时数据库调用的能力。请注意,如果Ping
函数失败,连接将从数据库池中移除。
DB
结构的指针还提供了Conn
方法,用于从数据库池中检索连接。通过使用连接,您实际上保证使用相同的数据库会话。同样,DB
结构的指针包含PingContext
方法,Conn
指针提供了PingContext
方法来检查连接是否仍然活动。
执行语句
在以前的示例中,我们已经学习了如何连接和验证与数据库的连接。本示例将描述如何执行针对数据库的语句。
准备工作
通过在终端中调用go version
命令来验证 Go 是否已正确安装。如果命令失败,请按照本章第一篇中的准备工作部分进行操作。
按照本章第一篇中的说明设置 PostgreSQL 服务器。
如何做...
- 对您的示例数据库运行以下 SQL 脚本:
DROP TABLE IF EXISTS post;
CREATE TABLE post (
ID serial,
TITLE varchar(40),
CONTENT varchar(255),
CONSTRAINT pk_post PRIMARY KEY(ID)
);
SELECT * FROM post;
-
打开控制台并创建文件夹
chapter08/recipe03
。 -
导航到目录。
-
创建
statement.go
文件,内容如下:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
const sel = "SELECT * FROM post;"
const trunc = "TRUNCATE TABLE post;"
const ins = "INSERT INTO post(ID,TITLE,CONTENT)
VALUES (1,'Title 1','Content 1'),
(2,'Title 2','Content 2') "
func main() {
db := createConnection()
defer db.Close()
_, err := db.Exec(trunc)
if err != nil {
panic(err)
}
fmt.Println("Table truncated.")
r, err := db.Exec(ins)
if err != nil {
panic(err)
}
affected, err := r.RowsAffected()
if err != nil {
panic(err)
}
fmt.Printf("Inserted rows count: %d\n",
affected)
rs, err := db.Query(sel)
if err != nil {
panic(err)
}
count := 0
for rs.Next() {
count++
}
fmt.Printf("Total of %d was selected.\n", count)
}
func createConnection() *sql.DB {
connStr := "postgres://postgres:postgres@
localhost:5432/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
}
-
通过
go run statement.go
执行代码。 -
查看输出:
工作原理...
通常,我们可以执行两种类型的语句。对于第一种类型的语句,我们不期望任何行作为结果,最终我们得到的是没有输出或者只是受影响的行数。这种类型的语句通过DB
结构指针上的Exec
方法执行。在前面的示例代码中,我们有TRUNCATE
和INSERT
语句。但是这种方式也可以执行 DDL 和 DCL 语句。
有四种主要的语句类别:
-
DDL(数据定义语言):此语言允许您创建和修改数据库模式
-
DML(数据建模语言):此语言帮助您修改数据
-
DCL(数据控制语言):此语言定义了对对象的访问控制
-
TCL(事务控制语言):此语言控制事务。
第二种类型是我们期望以行的形式得到结果的语句;这些通常被称为查询。这种类型的语句通常通过Query
或QueryContext
方法执行。
使用准备好的语句
准备好的语句带来了安全性、效率和便利性。当然,可以使用它们与 Go 标准库一起使用;本示例将展示如何使用。
准备工作
通过在终端中调用go version
命令来验证 Go 是否已正确安装。如果命令失败,请按照本章第一篇中的准备工作部分进行操作。
按照本章第一篇中的说明设置 PostgreSQL 服务器。
如何做...
- 对您的示例数据库运行以下 SQL 脚本:
DROP TABLE IF EXISTS post;
CREATE TABLE post (
ID serial,
TITLE varchar(40),
CONTENT varchar(255),
CONSTRAINT pk_post PRIMARY KEY(ID)
);
SELECT * FROM post;
-
打开控制台并创建文件夹
chapter08/recipe04
。 -
导航到目录。
-
创建
prepared.go
文件,内容如下:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
const trunc = "TRUNCATE TABLE post;"
const ins = "INSERT INTO post(ID,TITLE,CONTENT)
VALUES ($1,$2,$3)"
var testTable = []struct {
ID int
Title string
Content string
}{
{1, "Title One", "Content of title one"},
{2, "Title Two", "Content of title two"},
{3, "Title Three", "Content of title three"},
}
func main() {
db := createConnection()
defer db.Close()
// Truncate table
_, err := db.Exec(trunc)
if err != nil {
panic(err)
}
stm, err := db.Prepare(ins)
if err != nil {
panic(err)
}
inserted := int64(0)
for _, val := range testTable {
fmt.Printf("Inserting record ID: %d\n", val.ID)
// Execute the prepared statement
r, err := stm.Exec(val.ID, val.Title, val.Content)
if err != nil {
fmt.Printf("Cannot insert record ID : %d\n",
val.ID)
}
if affected, err := r.RowsAffected(); err == nil {
inserted = inserted + affected
}
}
fmt.Printf("Result: Inserted %d rows.\n", inserted)
}
func createConnection() *sql.DB {
connStr := "postgres://postgres:postgres@
localhost:5432/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
}
-
通过
go run prepared.go
执行代码。 -
查看输出:
工作原理...
要创建准备好的语句,需要调用指向DB
结构的Prepare
方法。之后,使用给定的参数调用Stmt
指针上的Exec
或Query
方法。
准备好的语句是在DB
指针的范围内创建的,但是在连接池中的特定连接上。语句记住了使用过的连接,并且在调用时尝试使用相同的连接。如果连接忙或已关闭,则重新创建准备好的语句并在新连接上调用语句。
如果在打开的事务*Tx
中使用准备好的语句,则情况会发生变化,在这种情况下,准备好的语句绑定到与事务相关的一个连接。
请注意,事务中准备的语句不能与 DB 指针一起使用,反之亦然。
通常,准备好的语句的工作方式是在数据库端创建语句。数据库返回准备好的语句的标识符。准备好的语句在以下调用期间执行,并且只提供语句的参数。
取消挂起的查询
在某些情况下,您需要取消长时间运行的语句以限制资源的消耗,或者仅当结果不相关或语句运行时间过长时。自 Go 1.8 以来,取消查询是可能的。本配方解释了如何使用此功能。
准备工作
通过在终端中调用go version
命令验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。
按照本章第一个配方中提到的方式设置 PostgreSQL 服务器。
操作步骤...
- 对您的示例数据库运行以下 SQL 脚本:
DROP TABLE IF EXISTS post;
CREATE TABLE post (
ID serial,
TITLE varchar(40),
CONTENT varchar(255),
CONSTRAINT pk_post PRIMARY KEY(ID)
);
SELECT * FROM post;
INSERT INTO post(ID,TITLE,CONTENT) VALUES
(1,'Title One','Content One'),
(2,'Title Two','Content Two');
-
打开控制台并创建文件夹
chapter08/recipe05
。 -
导航到目录。
-
使用以下内容创建
cancelable.go
文件:
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq"
)
const sel = "SELECT * FROM post p CROSS JOIN
(SELECT 1 FROM generate_series(1,1000000)) tbl"
func main() {
db := createConnection()
defer db.Close()
ctx, canc := context.WithTimeout(context.Background(),
20*time.Microsecond)
rows, err := db.QueryContext(ctx, sel)
canc() //cancel the query
if err != nil {
fmt.Println(err)
return
}
defer rows.Close()
count := 0
for rows.Next() {
if rows.Err() != nil {
fmt.Println(rows.Err())
continue
}
count++
}
fmt.Printf("%d rows returned\n", count)
}
func createConnection() *sql.DB {
connStr := "postgres://postgres:postgres@
localhost:5432/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
}
-
通过
go run cancelable.go
执行代码。 -
查看输出:
工作原理...
database/sql
包提供了取消挂起语句的可能性。DB
结构指针的所有名为XXXContext
的方法都会消耗上下文,并且可以取消挂起的语句。
只有在驱动程序支持Context
变体时才能取消语句。如果不支持,将执行不带Context
的变体。
使用Context
变体和context.WithTimeout
,您可以创建语句调用的超时。
请注意,示例代码执行以错误pq: canceling statement due to user request
结束,这与调用查询后立即调用的CancelFunc
相对应。
读取查询结果元数据
除了数据本身,查询结果还包含与结果集相关的元数据。这包含有关列名、类型和数据的其他信息。本配方将解释如何检索数据。
准备工作
通过在终端中调用go version
命令验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。
按照本章第一个配方中提到的方式设置 PostgreSQL 服务器。
操作步骤...
- 对您的示例数据库运行以下 SQL 脚本:
DROP TABLE IF EXISTS post;
CREATE TABLE post (
ID serial,
TITLE varchar(40),
CONTENT varchar(255),
CONSTRAINT pk_post PRIMARY KEY(ID)
);
SELECT * FROM post;
INSERT INTO post(ID,TITLE,CONTENT) VALUES
(1,'Title One','Content One'),
(2,'Title Two','Content Two');
-
打开控制台并创建文件夹
chapter08/recipe06
。 -
导航到目录。
-
使用以下内容创建
metadata.go
文件:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
const sel = "SELECT * FROM post p"
func main() {
db := createConnection()
defer db.Close()
rs, err := db.Query(sel)
if err != nil {
panic(err)
}
defer rs.Close()
columns, err := rs.Columns()
if err != nil {
panic(err)
}
fmt.Printf("Selected columns: %v\n", columns)
colTypes, err := rs.ColumnTypes()
if err != nil {
panic(err)
}
for _, col := range colTypes {
fmt.Println()
fmt.Printf("%+v\n", col)
}
}
func createConnection() *sql.DB {
connStr := "postgres://postgres:postgres@
localhost:5432/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
}
-
通过
go run metadata.go
执行代码。 -
查看输出:
工作原理...
DB
结构指针的Query
和QueryContext
方法会导致Rows
结构指针。Rows
指针提供Columns
和ColumnTypes
方法,其中包含有关返回结果集结构的信息。
Columns
方法返回带有列名的字符串切片。
ColumnTypes
方法返回ColumnType
指针的切片,其中包含有关返回结果集的更丰富信息。上述代码打印出了ColumnType
指针公开的详细信息。
从查询结果中检索数据
在与数据库交互时,基本部分是通过执行查询来提取数据。本配方将说明使用标准库database/sql
包时如何执行此操作。
准备工作
验证 Go 是否已正确安装,通过在终端中调用go version
命令。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。
按照本章第一个配方中的说明设置 PostgreSQL 服务器。
操作步骤...
- 对样本数据库运行以下 SQL 脚本:
DROP TABLE IF EXISTS post;
CREATE TABLE post (
ID serial,
TITLE varchar(40),
CONTENT varchar(255),
CONSTRAINT pk_post PRIMARY KEY(ID)
);
SELECT * FROM post;
INSERT INTO post(ID,TITLE,CONTENT) VALUES
(1,'Title One','Content One'),
(2,NULL,'Content Two');
-
打开控制台并创建文件夹
chapter08/recipe07
。 -
导航到目录。
-
创建
data.go
文件,内容如下:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
const sel = `SELECT title,content FROM post;
SELECT 1234 NUM; `
const selOne = "SELECT title,content FROM post
WHERE ID = $1;"
type Post struct {
Name sql.NullString
Text sql.NullString
}
func main() {
db := createConnection()
defer db.Close()
rs, err := db.Query(sel)
if err != nil {
panic(err)
}
defer rs.Close()
posts := []Post{}
for rs.Next() {
if rs.Err() != nil {
panic(rs.Err())
}
p := Post{}
if err := rs.Scan(&p.Name, &p.Text); err != nil {
panic(err)
}
posts = append(posts, p)
}
var num int
if rs.NextResultSet() {
for rs.Next() {
if rs.Err() != nil {
panic(rs.Err())
}
rs.Scan(&num)
}
}
fmt.Printf("Retrieved posts: %+v\n", posts)
fmt.Printf("Retrieved number: %d\n", num)
row := db.QueryRow(selOne, 100)
or := Post{}
if err := row.Scan(&or.Name, &or.Text); err != nil {
fmt.Printf("Error: %s\n", err.Error())
return
}
fmt.Printf("Retrieved one post: %+v\n", or)
}
func createConnection() *sql.DB {
connStr := "postgres://postgres:postgres@
localhost:5432/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
}
-
通过
go run data.go
执行代码。 -
查看输出:
工作原理...
来自指向DB
结构的Query
方法的Rows
指针提供了从结果集中读取和提取数据的方法。
请注意,首先应调用Next
方法将光标移动到下一个结果行。Next
方法如果有其他行则返回true
,否则返回false
。
在通过Next
获取新行后,可以调用Scan
方法将数据提取到变量中。变量的数量必须与SELECT
中的列数匹配,否则Scan
方法无法提取数据。
代码的重要部分是,在每次调用Next
方法后,应调用Err
方法来查找在读取下一行时是否出现错误。
上述示例故意对第二条记录使用了NULL
值。NULL
数据库值无法提取到不可为空类型,例如string
,在这种情况下,必须使用NullString
类型。
为了完整起见,示例代码涵盖了QueryRow
方法,它与Query
方法略有不同。这个方法返回指向Row
结构的指针,该结构仅提供Scan
方法。请注意,只有在调用Scan
方法之后才能检测到没有行的情况。
将查询结果解析为映射
有时查询结果或表的结构不清晰,需要将结果提取到某种灵活的结构中。这就引出了这个配方,其中将介绍将值提取到与列名映射的灵活结构中。
准备工作
验证 Go 是否已正确安装,通过在终端中调用go version
命令。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。
按照本章第一个配方中的说明设置 PostgreSQL 服务器。
操作步骤...
- 对样本数据库运行以下 SQL 脚本:
DROP TABLE IF EXISTS post;
CREATE TABLE post (
ID serial,
TITLE varchar(40),
CONTENT varchar(255),
CONSTRAINT pk_post PRIMARY KEY(ID)
);
SELECT * FROM post;
INSERT INTO post(ID,TITLE,CONTENT) VALUES
(1,NULL,'Content One'),
(2,'Title Two','Content Two');
-
打开控制台并创建文件夹
chapter08/recipe08
。 -
导航到目录。
-
创建
querymap.go
文件,内容如下:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
const selOne = "SELECT id,title,content FROM post
WHERE ID = $1;"
func main() {
db := createConnection()
defer db.Close()
rows, err := db.Query(selOne, 1)
if err != nil {
panic(err)
}
cols, _ := rows.Columns()
for rows.Next() {
m := parseWithRawBytes(rows, cols)
fmt.Println(m)
m = parseToMap(rows, cols)
fmt.Println(m)
}
}
func parseWithRawBytes(rows *sql.Rows, cols []string)
map[string]interface{} {
vals := make([]sql.RawBytes, len(cols))
scanArgs := make([]interface{}, len(vals))
for i := range vals {
scanArgs[i] = &vals[i]
}
if err := rows.Scan(scanArgs...); err != nil {
panic(err)
}
m := make(map[string]interface{})
for i, col := range vals {
if col == nil {
m[cols[i]] = nil
} else {
m[cols[i]] = string(col)
}
}
return m
}
func parseToMap(rows *sql.Rows, cols []string)
map[string]interface{} {
values := make([]interface{}, len(cols))
pointers := make([]interface{}, len(cols))
for i := range values {
pointers[i] = &values[i]
}
if err := rows.Scan(pointers...); err != nil {
panic(err)
}
m := make(map[string]interface{})
for i, colName := range cols {
if values[i] == nil {
m[colName] = nil
} else {
m[colName] = values[i]
}
}
return m
}
func createConnection() *sql.DB {
connStr := "postgres://postgres:postgres@
localhost:5432/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
}
-
通过
go run querymap.go
执行代码。 -
查看输出:
工作原理...
请注意,上述代码表示了两种方法。parseWithRawBytes
函数使用了首选方法,但它高度依赖于驱动程序的实现。它的工作方式是创建与结果中列数相同长度的RawBytes
切片。因为Scan
函数需要值的指针,所以我们需要创建指向RawBytes
切片(字节切片的切片)的指针切片,然后将其传递给Scan
函数。
提取成功后,我们只需重新映射值。在示例代码中,我们将其转换为string
,因为如果RawBytes
是目标,驱动程序使用string
类型来存储值。请注意,存储值的形式取决于驱动程序的实现。
第二种方法parseToMap
在第一种方法不起作用的情况下是可用的。它几乎使用相同的方法,但值的切片被定义为空接口的切片。这种方法依赖于驱动程序。驱动程序应确定要分配给值指针的默认类型。
处理事务
事务控制是在处理数据库时需要牢记的常见事情。本配方将向您展示如何使用sql
包处理事务。
准备工作
通过在终端中调用go version
命令来验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。
设置 PostgreSQL 服务器,如本章第一个配方中所述。
如何做...
- 对您的示例数据库运行以下 SQL 脚本:
DROP TABLE IF EXISTS post;
CREATE TABLE post (
ID serial,
TITLE varchar(40),
CONTENT varchar(255),
CONSTRAINT pk_post PRIMARY KEY(ID)
);
SELECT * FROM post;
INSERT INTO post(ID,TITLE,CONTENT) VALUES
(1,'Title One','Content One'),
(2,NULL,'Content Two');
-
打开控制台并创建文件夹
chapter08/recipe09
。 -
导航到目录。
-
创建
transaction.go
文件,内容如下:
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
const selOne = "SELECT id,title,content FROM post
WHERE ID = $1;"
const insert = "INSERT INTO post(ID,TITLE,CONTENT)
VALUES (4,'Transaction Title','Transaction Content');"
type Post struct {
ID int
Title string
Content string
}
func main() {
db := createConnection()
defer db.Close()
tx, err := db.Begin()
if err != nil {
panic(err)
}
_, err = tx.Exec(insert)
if err != nil {
panic(err)
}
p := Post{}
// Query in other session/transaction
if err := db.QueryRow(selOne, 4).Scan(&p.ID,
&p.Title, &p.Content); err != nil {
fmt.Println("Got error for db.Query:" + err.Error())
}
fmt.Println(p)
// Query within transaction
if err := tx.QueryRow(selOne, 4).Scan(&p.ID,
&p.Title, &p.Content); err != nil {
fmt.Println("Got error for db.Query:" + err.Error())
}
fmt.Println(p)
// After commit or rollback the
// transaction need to recreated.
tx.Rollback()
}
func createConnection() *sql.DB {
connStr := "postgres://postgres:postgres@
localhost:5432/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
}
-
通过
go run transaction.go
执行代码。 -
看输出:
工作原理...
正如前面的代码所示,事务处理非常简单。DB
结构指针的Begin
方法创建具有默认隔离级别的事务(取决于驱动程序)。事务本质上保留在单个连接上,并由返回的Tx
结构指针表示。
指针Tx
实现了DB
结构指针可用的所有方法;唯一的例外是所有操作都在事务中完成(如果数据库能够在事务中处理语句)。通过在Tx
结构指针上调用Rollback
或Commit
方法结束事务。在此调用之后,事务结束,其他操作将以错误ErrTxDone
结束。
DB
结构指针上还有一个有用的方法叫做BeginTx
,它创建了事务Tx
结构指针,同时也增强了给定的上下文。如果上下文被取消,事务将被回滚(进一步的Commit
调用将导致错误)。BeginTx
还消耗了TxOptions
指针,这是可选的,可以定义隔离级别。
执行存储过程和函数
处理存储过程和函数总是比通常的语句更复杂,特别是如果过程包含自定义类型。标准库提供了处理这些的 API,但存储过程调用的支持程度取决于驱动程序的实现。本配方将展示一个非常简单的函数/过程调用。
准备工作
通过在终端中调用go version
命令来验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。
设置 PostgreSQL 服务器,如本章第一个配方中所述。
如何做...
- 对您的示例数据库运行以下 SQL 脚本:
CREATE OR REPLACE FUNCTION format_name
(firstname Text,lastname Text,age INT) RETURNS
VARCHAR AS $$
BEGIN
RETURN trim(firstname) ||' '||trim(lastname) ||' ('||age||')';
END;
$$ LANGUAGE plpgsql;
-
打开控制台并创建文件夹
chapter08/recipe10
。 -
导航到目录。
-
创建
procedure.go
文件,内容如下:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
)
const call = "select * from format_name($1,$2,$3)"
const callMySQL = "CALL simpleproc(?)"
type Result struct {
Name string
Category int
}
func main() {
db := createConnection()
defer db.Close()
r := Result{}
if err := db.QueryRow(call, "John", "Doe",
32).Scan(&r.Name); err != nil {
panic(err)
}
fmt.Printf("Result is: %+v\n", r)
}
func createConnection() *sql.DB {
connStr := "postgres://postgres:postgres@localhost:5432
/example?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
err = db.Ping()
if err != nil {
panic(err)
}
return db
}
-
通过
go run procedure.go
执行代码。 -
看输出:
工作原理...
存储过程的调用高度依赖于驱动程序和数据库。请注意,在 PostgreSQL 数据库上检索结果与查询表非常相似。调用DB
结构指针的Query
或QueryRow
方法,可以解析出结果行或行指针以获取值。
如果需要调用存储过程,MySQL 驱动程序将使用CALL
语句。
几乎所有驱动程序的一般问题都是存储过程的OUTPUT
参数。Go 1.9 增加了对这些参数的支持,但常用数据库的大多数驱动程序尚未实现这一功能。因此,解决方案可能是使用具有非标准 API 的驱动程序。
OUTPUT
参数应该工作的方式是,存储过程调用将使用database/sql
包中Named
函数的NamedArg
参数类型。NamedArg
结构体的Value
字段应该是Out
类型,其中包含Dest
字段,用于存放OUTPUT
参数的实际值。
第九章:来到服务器端
本章包含以下配方:
-
创建 TCP 服务器
-
创建 UDP 服务器
-
处理多个客户端
-
创建 HTTP 服务器
-
处理 HTTP 请求
-
创建 HTTP 中间件层
-
提供静态文件
-
提供使用模板生成的内容
-
处理重定向
-
处理 cookies
-
优雅地关闭 HTTP 服务器
-
提供安全的 HTTP 内容
-
解析表单变量
介绍
本章涵盖了从实现简单的 TCP 和 UDP 服务器到启动 HTTP 服务器的主题。这些配方将引导您从处理 HTTP 请求、提供静态内容,到提供安全的 HTTP 内容。
检查 Go 是否已正确安装。第一章的准备就绪部分中的检索 Golang 版本配方将有所帮助。
确保端口8080
和7070
没有被其他应用程序使用。
创建 TCP 服务器
在连接网络章节中,介绍了 TCP 连接的客户端部分。在本配方中,将描述服务器端。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe01
。 -
导航到该目录。
-
创建
servertcp.go
文件,内容如下:
package main
import (
"bufio"
"fmt"
"io"
"net"
)
func main() {
l, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
for {
fmt.Println("Waiting for client...")
conn, err := l.Accept()
if err != nil {
panic(err)
}
msg, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
panic(err)
}
_, err = io.WriteString(conn, "Received: "+string(msg))
if err != nil {
fmt.Println(err)
}
conn.Close()
}
}
- 通过
go run servertcp.go
执行代码:
-
打开另一个终端并执行
nc localhost 8080
。 -
写入任何文本,例如
Hello
。 -
查看输出:
工作原理...
可以使用net
包创建 TCP 服务器。net 包包含Listen
函数,用于创建TCPListener
,可以Accept
客户端连接。Accept
方法调用TCPListener
上的方法,直到接收到客户端连接。如果客户端连接成功,Accept
方法会返回TCPConn
连接。TCPConn
是连接到客户端的连接,用于读取和写入数据。
TCPConn
实现了Reader
和Writer
接口。可以使用所有写入和读取数据的方法。请注意,读取数据时有一个分隔符字符,否则,如果客户端强制关闭连接,则会收到 EOF。
请注意,此实现一次只能处理一个客户端。
创建 UDP 服务器
用户数据报协议(UDP)是互联网的基本协议之一。本篇将向您展示如何监听 UDP 数据包并读取内容。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe02
。 -
导航到该目录。
-
创建
serverudp.go
文件,内容如下:
package main
import (
"fmt"
"log"
"net"
)
func main() {
pc, err := net.ListenPacket("udp", ":7070")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
buffer := make([]byte, 2048)
fmt.Println("Waiting for client...")
for {
_, addr, err := pc.ReadFrom(buffer)
if err == nil {
rcvMsq := string(buffer)
fmt.Println("Received: " + rcvMsq)
if _, err := pc.WriteTo([]byte("Received: "+rcvMsq), addr);
err != nil {
fmt.Println("error on write: " + err.Error())
}
} else {
fmt.Println("error: " + err.Error())
}
}
}
- 通过
go run serverudp.go
启动服务器:
-
打开另一个终端并执行
nc -u localhost 7070
。 -
在终端中写入任何消息,例如
Hello
,然后按Enter。 -
查看输出:
工作原理...
与 TCP 服务器一样,可以使用net
包创建 UDP 服务器。使用ListenPacket
函数创建PacketConn
。
PacketConn
不像TCPConn
那样实现Reader
和Writer
接口。要读取接收到的数据包,应该使用ReadFrom
方法。ReadFrom
方法会阻塞,直到接收到数据包。然后返回客户端的Addr
(记住 UDP 不是基于连接的)。要响应客户端,可以使用PacketConn
的WriteTo
方法;这会消耗消息和Addr
,在这种情况下是客户端的Addr
。
处理多个客户端
前面的配方展示了如何创建 UDP 和 TCP 服务器。示例代码尚未准备好同时处理多个客户端。在本配方中,我们将介绍如何同时处理更多客户端。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe03
。 -
导航到该目录。
-
创建
multipletcp.go
文件,内容如下:
package main
import (
"fmt"
"log"
"net"
)
func main() {
pc, err := net.ListenPacket("udp", ":7070")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
buffer := make([]byte, 2048)
fmt.Println("Waiting for client...")
for {
_, addr, err := pc.ReadFrom(buffer)
if err == nil {
rcvMsq := string(buffer)
fmt.Println("Received: " + rcvMsq)
if _, err := pc.WriteTo([]byte("Received: "+rcvMsq), addr);
err != nil {
fmt.Println("error on write: " + err.Error())
}
} else {
fmt.Println("error: " + err.Error())
}
}
}
-
通过
go run multipletcp.go
执行代码。 -
打开另外两个终端并执行
nc localhost 8080
。 -
在两个打开的终端中写入一些内容并查看输出。以下两个图像是连接的客户端。
-
- 终端 1 连接到
localhost:8080
:
- 终端 1 连接到
-
- 终端 2 连接到
localhost:8080
:
- 终端 2 连接到
服务器运行的终端中的输出:
工作原理...
TCP 服务器的实现与本章的前一个配方创建 TCP 服务器相同。实现已增强,具有同时处理多个客户端的能力。请注意,我们现在在单独的goroutine
中处理接受的连接。这意味着服务器可以继续使用Accept
方法接受客户端连接。
因为 UDP 协议不是有状态的,也不保持任何连接,所以处理多个客户端的工作被移动到应用程序逻辑中,您需要识别客户端和数据包序列。只有向客户端写入响应才能使用 goroutines 并行化。
创建 HTTP 服务器
在 Go 中创建 HTTP 服务器非常容易,标准库提供了更多的方法来实现。让我们看看最基本的方法。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe04
。 -
导航到目录。
-
创建
httpserver.go
文件,内容如下:
package main
import (
"fmt"
"net/http"
)
type SimpleHTTP struct{}
func (s SimpleHTTP) ServeHTTP(rw http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(rw, "Hello world")
}
func main() {
fmt.Println("Starting HTTP server on port 8080")
// Eventually you can use
// http.ListenAndServe(":8080", SimpleHTTP{})
s := &http.Server{Addr: ":8080", Handler: SimpleHTTP{}}
s.ListenAndServe()
}
-
通过
go run httpserver.go
执行代码。 -
查看输出:
- 在浏览器中访问 URL
http://localhost:8080
,或使用curl
。应该显示Hello world
内容:
工作原理...
net/http
包包含了几种创建 HTTP 服务器的方法。最简单的方法是实现net/http
包中的Handler
接口。Handler
接口要求类型实现ServeHTTP
方法。这个方法处理请求和响应。
服务器本身以net/http
包中的Server
结构的形式创建。Server
结构需要Handler
和Addr
字段。通过调用ListenAndServe
方法,服务器开始在给定地址上提供内容。
如果使用Server
的Serve
方法,则必须提供Listener
。
net/http
包还提供了默认服务器,如果从net/http
包中调用ListenAndServe
作为函数,则可以使用。它消耗Handler
和Addr
,与Server
结构相同。在内部,创建了Server
。
处理 HTTP 请求
应用程序通常使用 URL 路径和 HTTP 方法来定义应用程序的行为。本配方将说明如何利用标准库来处理不同的 URL 和方法。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe05
。 -
导航到目录。
-
创建
handle.go
文件,内容如下:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/user", func(w http.ResponseWriter,
r *http.Request) {
if r.Method == http.MethodGet {
fmt.Fprintln(w, "User GET")
}
if r.Method == http.MethodPost {
fmt.Fprintln(w, "User POST")
}
})
// separate handler
itemMux := http.NewServeMux()
itemMux.HandleFunc("/items/clothes", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(w, "Clothes")
})
mux.Handle("/items/", itemMux)
// Admin handlers
adminMux := http.NewServeMux()
adminMux.HandleFunc("/ports", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(w, "Ports")
})
mux.Handle("/admin/", http.StripPrefix("/admin",
adminMux))
// Default server
http.ListenAndServe(":8080", mux)
}
-
通过
go run handle.go
执行代码。 -
在浏览器中或通过
curl
检查以下 URL:
-
http://localhost:8080/user
-
http://localhost:8080/items/clothes
-
http://localhost:8080/admin/ports
- 查看输出:
工作原理...
net/http
包包含ServeMux
结构,该结构实现了Handler
接口,可用于Server
结构,但还包含了如何定义不同路径处理的机制。ServeMux
指针包含HandleFunc
和Handle
方法,接受路径,HandlerFunc
函数处理给定路径的请求,或者另一个处理程序执行相同的操作。
参见前面的示例,了解如何使用这些。Handler
接口和HandlerFunc
需要实现带有请求和响应参数的函数。这样你就可以访问这两个结构。请求本身可以访问Headers
、HTTP 方法和其他请求参数。
创建 HTTP 中间件层
具有 Web UI 或 REST API 的现代应用程序通常使用中间件机制来记录活动或保护给定接口的安全性。在本示例中,将介绍实现这种中间件层。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe06
。 -
导航到目录。
-
创建具有以下内容的
middleware.go
文件:
package main
import (
"io"
"net/http"
)
func main() {
// Secured API
mux := http.NewServeMux()
mux.HandleFunc("/api/users", Secure(func(w http.ResponseWriter,
r *http.Request) {
io.WriteString(w, `[{"id":"1","login":"ffghi"},
{"id":"2","login":"ffghj"}]`)
}))
http.ListenAndServe(":8080", mux)
}
func Secure(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sec := r.Header.Get("X-Auth")
if sec != "authenticated" {
w.WriteHeader(http.StatusUnauthorized)
return
}
h(w, r) // use the handler
}
}
-
通过
go run middleware.go
执行代码。 -
使用
curl
检查 URLhttp://localhost:8080/api/users
,通过执行这两个命令(第一个不带X-Auth
头,第二个带X-Auth
头):
-
curl -X GET -I http://localhost:8080/api/users
-
curl -X GET -H "X-Auth: authenticated" -I http://localhost:8080/api/users
- 查看输出:
-
使用
X-User
头测试 URLhttp://localhost:8080/api/profile
。 -
查看输出:
工作原理...
在前面的示例中,中间件的实现利用了 Golang 的函数作为一等公民功能。原始的HandlerFunc
被包装成检查X-Auth
头的HandlerFunc
。然后使用Secure
函数来保护HandlerFunc
,并在ServeMux
的HandleFunc
方法中使用。
请注意,这只是一个简单的示例,但是您可以实现更复杂的解决方案。例如,用户身份可以从Header
令牌中提取,随后可以定义新类型的处理程序,如type AuthHandler func(u *User,w http.ResponseWriter, r *http.Request)
。然后,WithUser
函数为ServeMux
创建HandlerFunc
。
提供静态文件
几乎任何 Web 应用程序都需要提供静态文件。使用标准库可以轻松实现 JavaScript 文件、静态 HTML 页面或 CSS 样式表的提供。本示例将展示如何实现。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe07
。 -
导航到目录。
-
创建具有以下内容的文件
welcome.txt
:
Hi, Go is awesome!
- 创建文件夹
html
,导航到该文件夹并创建具有以下内容的文件page.html
:
<html>
<body>
Hi, I'm HTML body for index.html!
</body>
</html>
- 创建具有以下内容的
static.go
文件:
package main
import (
"net/http"
)
func main() {
fileSrv := http.FileServer(http.Dir("html"))
fileSrv = http.StripPrefix("/html", fileSrv)
http.HandleFunc("/welcome", serveWelcome)
http.Handle("/html/", fileSrv)
http.ListenAndServe(":8080", nil)
}
func serveWelcome(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "welcome.txt")
}
-
通过
go run static.go
执行代码。 -
使用浏览器或
curl
实用程序检查以下 URL:
-
http://localhost:8080/html/page.html
-
http://localhost:8080/welcome
- 查看输出:
工作原理...
net/http
包提供了ServeFile
和FileServer
函数,用于提供静态文件。ServeFile
函数只消耗给定文件路径参数的ResponseWriter
和Request
,并将文件内容写入响应。
FileServer
函数创建整个消耗FileSystem
参数的Handler
。前面的示例使用了Dir
类型,它实现了FileSystem
接口。FileSystem
接口需要实现Open
方法,该方法消耗字符串并返回给定路径的实际File
。
使用模板生成的内容
对于某些目的,不需要使用所有 JavaScript 创建高度动态的 Web UI,生成内容的静态内容可能已经足够。Go 标准库提供了一种构建动态生成内容的方法。本示例将引导您进入 Go 标准库模板化。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe08
。 -
导航到目录。
-
创建具有以下内容的文件
template.tpl
:
<html>
<body>
Hi, I'm HTML body for index.html!
</body>
</html>
- 创建文件
dynamic.go
,内容如下:
package main
import "net/http"
import "html/template"
func main() {
tpl, err := template.ParseFiles("template.tpl")
if err != nil {
panic(err)
}
http.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){
err := tpl.Execute(w, "John Doe")
if err != nil {
panic(err)
}
})
http.ListenAndServe(":8080", nil)
}
-
通过
go run dynamic.go
执行代码。 -
检查 URL
http://localhost:8080
并查看输出:
工作原理...
Go 标准库还包含用于模板化内容的包。html/template
和text/template
包提供了解析模板和使用它们创建输出的函数。解析是使用ParseXXX
函数或新创建的Template
结构指针的方法完成的。前面的示例使用了html/template
包的ParseFiles
函数。
模板本身是基于文本的文档或包含动态变量的文本片段。模板的使用基于将模板文本与包含模板中的变量值的结构进行合并。为了将模板与这些结构进行合并,有Execute
和ExecuteTemplate
方法。请注意,这些方法使用写入器接口,其中写入输出;在这种情况下使用ResponseWriter
。
模板语法和特性在文档中有很好的解释。
处理重定向
重定向是告诉客户端内容已经移动或需要在其他地方完成请求的常用方式。本教程描述了如何使用标准库实现重定向。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe09
。 -
导航到目录。
-
创建文件
redirect.go
,内容如下:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
log.Println("Server is starting...")
http.Handle("/secured/handle",
http.RedirectHandler("/login",
http.StatusTemporaryRedirect))
http.HandleFunc("/secured/hadlefunc",
func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
})
http.HandleFunc("/login", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintf(w, "Welcome user! Please login!\n")
})
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
-
通过
go run redirect.go
执行代码。 -
使用
curl -v -L http://localhost:8080/s
ecured/handle
以查看重定向是否有效:
工作原理...
net/http
包中包含了执行重定向的简单方法。可以利用RedirectHandler
。该函数接受请求将被重定向的URL
和将发送给客户端的状态码
。该函数本身将结果发送给Handler
,可以在ServeMux
的Handle
方法中使用(示例直接使用包中的默认方法)。
第二种方法是使用Redirect
函数,它可以为您执行重定向。该函数接受ResponseWriter
、请求指针和与RequestHandler
相同的 URL 和状态码,这些将发送给客户端。
重定向也可以通过手动设置Location
头并编写适当的状态码来完成。Go 库使开发人员能够轻松使用这一功能。
处理 cookies
Cookies 提供了一种在客户端方便地存储数据的方式。本教程演示了如何使用标准库设置、检索和删除 cookies。
如何做...
-
打开控制台并创建文件夹
chapter09/recipe10
。 -
导航到目录。
-
创建文件
cookies.go
,内容如下:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
const cookieName = "X-Cookie"
func main() {
log.Println("Server is starting...")
http.HandleFunc("/set", func(w http.ResponseWriter,
r *http.Request) {
c := &http.Cookie{
Name: cookieName,
Value: "Go is awesome.",
Expires: time.Now().Add(time.Hour),
Domain: "localhost",
}
http.SetCookie(w, c)
fmt.Fprintln(w, "Cookie is set!")
})
http.HandleFunc("/get", func(w http.ResponseWriter,
r *http.Request) {
val, err := r.Cookie(cookieName)
if err != nil {
fmt.Fprintln(w, "Cookie err: "+err.Error())
return
}
fmt.Fprintf(w, "Cookie is: %s \n", val.Value)
fmt.Fprintf(w, "Other cookies")
for _, v := range r.Cookies() {
fmt.Fprintf(w, "%s => %s \n", v.Name, v.Value)
}
})
http.HandleFunc("/remove", func(w http.ResponseWriter,
r *http.Request) {
val, err := r.Cookie(cookieName)
if err != nil {
fmt.Fprintln(w, "Cookie err: "+err.Error())
return
}
val.MaxAge = -1
http.SetCookie(w, val)
fmt.Fprintln(w, "Cookie is removed!")
})
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
-
通过
go run cookies.go
执行代码。 -
按照以下顺序访问 URL 并查看:
-
- 在浏览器中访问 URL
http://localhost:8080/set
的响应:
- 在浏览器中访问 URL
-
- 在浏览器中访问 URL
http://localhost:8080/get
的响应(响应包含可用的 cookies):
- 在浏览器中访问 URL
-
- 在浏览器中访问 URL
http://localhost:8080/remove
的响应(这将删除 cookie):
- 在浏览器中访问 URL
-
- 在浏览器中访问 URL
http://localhost:8080/get
的响应(证明 cookieX-Cookie
已被移除):
- 在浏览器中访问 URL
工作原理...
net/http
包还提供了操作 cookie 的函数和机制。示例代码介绍了如何设置/获取和删除 cookie。SetCookie
函数接受代表 cookie 的Cookie
结构指针,自然也接受ResponseWriter
。Name
、Value
、Domain
和过期时间直接在Cookie
结构中设置。在幕后,SetCookie
函数写入头文件以设置 cookie。
可以从Request
结构中检索 cookie 值。具有名称参数的Cookie
方法返回指向Cookie
的指针,如果请求中存在 cookie。
要列出请求中的所有 cookie,可以调用Cookies
方法。此方法返回Cookie
结构指针的切片。
为了让客户端知道应该删除 cookie,可以检索具有给定名称的Cookie
,并将MaxAge
字段设置为负值。请注意,这不是 Go 的特性,而是客户端应该工作的方式。
优雅关闭 HTTP 服务器
在第一章中,与环境交互,介绍了实现优雅关闭的机制。在这个示例中,我们将描述如何关闭 HTTP 服务器并给予它处理现有客户端的时间。
操作步骤...
-
打开控制台并创建文件夹
chapter09/recipe11
。 -
导航到目录。
-
创建名为
gracefully.go
的文件,内容如下:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){
fmt.Fprintln(w, "Hello world!")
})
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil {
log.Printf("Server error: %s\n", err)
}
}()
log.Println("Server listening on : " + srv.Addr)
stopChan := make(chan os.Signal)
signal.Notify(stopChan, os.Interrupt)
<-stopChan // wait for SIGINT
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second)
srv.Shutdown(ctx)
<-ctx.Done()
cancel()
log.Println("Server gracefully stopped")
}
-
通过
go run gracefully.go
执行代码。 -
等待服务器开始监听:
-
使用浏览器连接到
http://localhost:8080
;这将导致浏览器等待 10 秒钟的响应。 -
在 10 秒的间隔内,按下Ctrl + C发送
SIGINT
信号。 -
尝试从另一个标签页重新连接(服务器应该拒绝其他连接)。
-
在终端中查看输出:
工作原理...
net/http
包中的Server
提供了优雅关闭连接的方法。前面的代码在一个单独的goroutine
中启动 HTTP 服务器,并在一个变量中保留对Server
结构的引用。
通过调用Shutdown
方法,Server
开始拒绝新连接并关闭打开的监听器和空闲连接。然后它无限期地等待已经挂起的连接,直到这些连接变为空闲。在所有连接关闭后,服务器关闭。请注意,Shutdown
方法会消耗Context
。如果提供的Context
在关闭之前过期,则会返回来自Context
的错误,并且Shutdown
不再阻塞。
提供安全的 HTTP 内容
这个示例描述了创建 HTTP 服务器的最简单方式,它通过 TLS/SSL 层提供内容。
准备工作
准备私钥和自签名的 X-509 证书。为此,可以使用 OpenSSL 实用程序。通过执行命令openssl genrsa -out server.key 2048
,使用 RSA 算法生成私钥到文件server.key
。基于此私钥,可以通过调用openssl req -new -x509 -sha256 -key server.key -out server.crt -days 365
生成 X-509 证书。创建server.crt
文件。
操作步骤...
-
打开控制台并创建文件夹
chapter09/recipe12
。 -
导航到目录。
-
将创建的
server.key
和server.crt
文件放入其中。 -
创建名为
servetls.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
)
type SimpleHTTP struct{}
func (s SimpleHTTP) ServeHTTP(rw http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(rw, "Hello world")
}
func main() {
fmt.Println("Starting HTTP server on port 8080")
// Eventually you can use
// http.ListenAndServe(":8080", SimpleHTTP{})
s := &http.Server{Addr: ":8080", Handler: SimpleHTTP{}}
if err := s.ListenAndServeTLS("server.crt", "server.key");
err != nil {
panic(err)
}
}
-
通过
go run servetls.go
执行服务器。 -
访问 URL
https://localhost:8080
(使用 HTTPS 协议)。如果使用curl
实用程序,则必须使用--insecure
标志,因为我们的证书是自签名的,不受信任:
工作原理...
除了net/http
包中的ListenAndServe
函数之外,还存在用于通过 SSL/TLS 提供 HTTP 服务的 TLS 变体。通过Server
的ListenAndServeTLS
方法,可以提供安全的 HTTP 服务。ListenAndServeTLS
需要私钥和 X-509 证书的路径。当然,也可以直接使用net/http
包中的ListenAndServeTLS
函数。
解析表单变量
HTTP 的POST
表单是向服务器传递信息的一种常见方式,以结构化的方式。这个示例展示了如何在服务器端解析和访问这些信息。
如何做...
-
打开控制台,创建文件夹
chapter09/recipe12
。 -
导航到目录。
-
创建名为
form.go
的文件,内容如下:
package main
import (
"fmt"
"net/http"
)
type StringServer string
func (s StringServer) ServeHTTP(rw http.ResponseWriter,
req *http.Request) {
fmt.Printf("Prior ParseForm: %v\n", req.Form)
req.ParseForm()
fmt.Printf("Post ParseForm: %v\n", req.Form)
fmt.Println("Param1 is : " + req.Form.Get("param1"))
rw.Write([]byte(string(s)))
}
func createServer(addr string) http.Server {
return http.Server{
Addr: addr,
Handler: StringServer("Hello world"),
}
}
func main() {
s := createServer(":8080")
fmt.Println("Server is starting...")
if err := s.ListenAndServe(); err != nil {
panic(err)
}
}
-
通过
go run form.go
执行代码。 -
打开第二个终端,使用
curl
执行POST
:
curl -X POST -H "Content-Type: app
lication/x-www-form-urlencoded" -d "param1=data1¶m2=data2" "localhost:8080?
param1=overriden¶m3=data3"
- 在运行服务器的第一个终端中查看输出:
工作原理...
net/http
包的Request
结构包含Form
字段,其中包含了POST
表单变量和 URL 查询变量的合并。在前面的代码中,重要的一步是在Request
指针上调用ParseForm
方法。这个方法调用会将POST
表单值和查询值解析为一个Form
变量。请注意,如果在Form
字段上使用Get
方法,则会优先考虑参数的POST
值。Form
和PostForm
字段实际上都是url.Values
类型。
如果只需要访问POST
表单中的参数,可以使用Request
的PostForm
字段。这个字段只保留了POST
主体中的参数。
第十章:并发乐趣
本章包含以下教程:
-
使用 Mutex 同步对资源的访问
-
为并发访问创建 map
-
只运行一次代码块
-
在多个 goroutines 之间池化资源
-
使用 WaitGroup 同步 goroutines
-
从多个来源获取最快的结果
-
使用 errgroup 传播错误
介绍
并发行为的编程总是很困难的。Go 具有非常好的机制来管理并发,如通道。除了通道作为同步机制外,Go 标准库还提供了处理更传统核心方式的并发部分的包。本章描述了如何利用 sync 包来实现常见的同步任务。最后一个教程将展示如何简化一组 goroutines 的错误传播。
检查 Go 是否已正确安装。第一章的检索 Golang 版本教程中的准备就绪部分将对你有所帮助。
确保端口8080
和7070
没有被其他应用程序使用。
使用 Mutex 同步对资源的访问
如果代码使用并发访问被认为对并发使用不安全的任何资源,就需要实现同步机制来保护访问。除了使用通道,还可以利用互斥锁来实现这一目的。这个教程将向你展示如何做到这一点。
如何做...
-
打开控制台并创建文件夹
chapter10/recipe01
。 -
导航到目录。
-
创建文件
mutex.go
,内容如下:
package main
import (
"fmt"
"sync"
)
var names = []string{"Alan", "Joe", "Jack", "Ben",
"Ellen", "Lisa", "Carl", "Steve",
"Anton", "Yo"}
type SyncList struct {
m sync.Mutex
slice []interface{}
}
func NewSyncList(cap int) *SyncList {
return &SyncList{
sync.Mutex{},
make([]interface{}, cap),
}
}
func (l *SyncList) Load(i int) interface{} {
l.m.Lock()
defer l.m.Unlock()
return l.slice[i]
}
func (l *SyncList) Append(val interface{}) {
l.m.Lock()
defer l.m.Unlock()
l.slice = append(l.slice, val)
}
func (l *SyncList) Store(i int, val interface{}) {
l.m.Lock()
defer l.m.Unlock()
l.slice[i] = val
}
func main() {
l := NewSyncList(0)
wg := &sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(idx int) {
l.Append(names[idx])
wg.Done()
}(i)
}
wg.Wait()
for i := 0; i < 10; i++ {
fmt.Printf("Val: %v stored at idx: %d\n", l.Load(i), i)
}
}
-
通过
go run mutex.go
执行代码。 -
查看输出:
它是如何工作的...
同步原语Mutex
由sync
包提供。Mutex
作为一个锁,用于保护部分或资源。一旦goroutine
在Mutex
上调用Lock
并且Mutex
处于未锁定状态,Mutex
就会被锁定,goroutine
就可以独占地访问临界区。如果Mutex
处于锁定状态,goroutine
调用Lock
方法。这个goroutine
会被阻塞,需要等待Mutex
再次解锁。
请注意,在示例中,我们使用Mutex
来同步对切片原语的访问,这被认为是不安全的并发使用。
重要的事实是Mutex
在第一次使用后不能被复制。
为并发访问创建 map
在 Golang 中,map 原语应被视为不安全的并发访问。在上一个教程中,我们描述了如何使用 Mutex 同步对资源的访问,这也可以用于对 map 原语的访问。但是 Go 标准库还提供了专为并发访问设计的 map 结构。这个教程将说明如何使用它。
如何做...
-
打开控制台并创建文件夹
chapter10/recipe02
。 -
导航到目录。
-
创建文件
map.go
,内容如下:
package main
import (
"fmt"
"sync"
)
var names = []string{"Alan", "Joe", "Jack", "Ben",
"Ellen", "Lisa", "Carl", "Steve",
"Anton", "Yo"}
func main() {
m := sync.Map{}
wg := &sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(idx int) {
m.Store(fmt.Sprintf("%d", idx), names[idx])
wg.Done()
}(i)
}
wg.Wait()
v, ok := m.Load("1")
if ok {
fmt.Printf("For Load key: 1 got %v\n", v)
}
v, ok = m.LoadOrStore("11", "Tim")
if !ok {
fmt.Printf("Key 11 missing stored val: %v\n", v)
}
m.Range(func(k, v interface{}) bool {
key, _ := k.(string)
t, _ := v.(string)
fmt.Printf("For index %v got %v\n", key, t)
return true
})
}
-
通过
go run map.go
执行代码。 -
查看输出:
它是如何工作的...
sync
包中包含了Map
结构,该结构被设计用于从多个 Go 例程中并发使用。Map
结构及其方法模仿了 map 原语的行为。Store
方法相当于m[key] = val
语句。Load
方法相当于val, ok := m[key]
,Range
方法提供了遍历 map 的能力。请注意,Range
函数与Map
的当前状态一起工作,因此如果在运行Range
方法期间更改了值,则会反映这些更改,但前提是该键尚未被访问。Range
函数只会访问其键一次。
只运行一次代码块
在多个 goroutine 运行相同代码的情况下,例如,有一个初始化共享资源的代码块,Go 标准库提供了解决方案,将在下文中描述。
如何做...
-
打开控制台并创建文件夹
chapter10/recipe03
。 -
导航到目录。
-
创建文件
once.go
,内容如下:
package main
import (
"fmt"
"sync"
)
var names = []interface{}{"Alan", "Joe", "Jack", "Ben",
"Ellen", "Lisa", "Carl", "Steve",
"Anton", "Yo"}
type Source struct {
m *sync.Mutex
o *sync.Once
data []interface{}
}
func (s *Source) Pop() (interface{}, error) {
s.m.Lock()
defer s.m.Unlock()
s.o.Do(func() {
s.data = names
fmt.Println("Data has been loaded.")
})
if len(s.data) > 0 {
res := s.data[0]
s.data = s.data[1:]
return res, nil
}
return nil, fmt.Errorf("No data available")
}
func main() {
s := &Source{&sync.Mutex{}, &sync.Once{}, nil}
wg := &sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(idx int) {
// This code block is done only once
if val, err := s.Pop(); err == nil {
fmt.Printf("Pop %d returned: %s\n", idx, val)
}
wg.Done()
}(i)
}
wg.Wait()
}
-
使用
go run once.go
执行代码。 -
查看输出:
工作原理...
示例代码说明了在访问容器结构时数据的延迟加载。由于数据只应加载一次,因此在Pop
方法中使用了sync
包中的Once
结构。Once
只实现了一个名为Do
的方法,该方法消耗了一个无参数的func
,并且该函数在每个Once
实例的执行期间只执行一次。
Do
方法调用会阻塞,直到第一次运行完成。这一事实与Once
旨在用于初始化的事实相对应。
在多个 goroutine 之间池化资源
资源池是提高性能和节省资源的传统方式。通常,值得使用昂贵初始化的资源进行池化。Go 标准库提供了用于资源池的骨架结构,被认为对多个 goroutine 访问是安全的。本示例描述了如何使用它。
如何做...
-
打开控制台并创建文件夹
chapter10/recipe04
。 -
导航到目录。
-
创建文件
pool.go
,内容如下:
package main
import "sync"
import "fmt"
import "time"
type Worker struct {
id string
}
func (w *Worker) String() string {
return w.id
}
var globalCounter = 0
var pool = sync.Pool{
New: func() interface{} {
res := &Worker{fmt.Sprintf("%d", globalCounter)}
globalCounter++
return res
},
}
func main() {
wg := &sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(idx int) {
// This code block is done only once
w := pool.Get().(*Worker)
fmt.Println("Got worker ID: " + w.String())
time.Sleep(time.Second)
pool.Put(w)
wg.Done()
}(i)
}
wg.Wait()
}
-
使用
go run pool.go
执行代码。 -
查看输出:
工作原理...
sync
包包含了用于池化资源的结构。Pool
结构具有Get
和Put
方法,用于检索资源并将其放回池中。Pool
结构被认为对并发访问是安全的。
在创建Pool
结构时,需要设置New
字段。New
字段是一个无参数函数,应该返回指向池化项目的指针。如果需要初始化池中的新对象,则会调用此函数。
从前面示例的日志中可以看出,Worker
在返回到池中时被重用。重要的事实是,不应该对Get
检索的项目和Put
方法返回的项目做任何假设(比如我刚刚把三个对象放到池中,所以至少会有三个可用)。这主要是因为Pool
中的空闲项目可能随时被自动删除。
如果资源初始化很昂贵,资源池化通常是值得的。然而,资源的管理也带来了一些额外的成本。
使用 WaitGroup 同步 goroutine
在处理并发运行的代码分支时,程序在某个时刻需要等待并发运行的代码部分。本示例介绍了如何使用WaitGroup
等待运行的 goroutine。
如何做...
-
打开控制台并创建文件夹
chapter10/recipe05
。 -
导航到目录。
-
创建文件
syncgroup.go
,内容如下:
package main
import "sync"
import "fmt"
func main() {
wg := &sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
// Do some work
defer wg.Done()
fmt.Printf("Exiting %d\n", idx)
}(i)
}
wg.Wait()
fmt.Println("All done.")
}
-
使用
go run syncgroup.go
执行代码。 -
查看输出:
工作原理...
通过sync
包中的WaitGroup
结构,程序可以等待有限数量的 goroutine 完成运行。WaitGroup
结构实现了Add
方法,用于添加要等待的 goroutine 数量。然后在 goroutine 完成后,应调用Done
方法来减少要等待的 goroutine 数量。Wait
方法被调用时会阻塞,直到完成给定数量的Done
调用(通常在goroutine
结束时)。WaitGroup
应该与sync
包中的所有同步原语一样使用。在创建对象后,结构不应被复制。
从多个来源获取最快的结果
在某些情况下,例如,在整合来自多个来源的信息检索时,您只需要第一个结果,最快的结果,其他结果在那之后就不相关了。现实世界中的一个例子可能是提取货币汇率以计算价格。您有多个第三方服务,因为您需要尽快显示价格,所以只需要从任何服务接收到的第一个汇率。本教程将展示如何实现这种行为的模式。
如何做...
-
打开控制台并创建文件夹
chapter10/recipe06
。 -
导航到目录。
-
创建文件
first.go
,内容如下:
package main
import (
"context"
"fmt"
"sync"
"time"
)
type SearchSrc struct {
ID string
Delay int
}
func (s *SearchSrc) Search(ctx context.Context) <-chan string {
out := make(chan string)
go func() {
time.Sleep(time.Duration(s.Delay) * time.Second)
select {
case out <- "Result " + s.ID:
case <-ctx.Done():
fmt.Println("Search received Done()")
}
close(out)
fmt.Println("Search finished for ID: " + s.ID)
}()
return out
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
src1 := &SearchSrc{"1", 2}
src2 := &SearchSrc{"2", 6}
r1 := src1.Search(ctx)
r2 := src2.Search(ctx)
out := merge(ctx, r1, r2)
for firstResult := range out {
cancel()
fmt.Println("First result is: " + firstResult)
}
}
func merge(ctx context.Context, results ...<-chan string)
<-chan string {
wg := sync.WaitGroup{}
out := make(chan string)
output := func(c <-chan string) {
defer wg.Done()
select {
case <-ctx.Done():
fmt.Println("Received ctx.Done()")
case res := <-c:
out <- res
}
}
wg.Add(len(results))
for _, c := range results {
go output(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
-
通过
go run first.go
执行代码。 -
查看输出:
它是如何工作的...
上述代码提出了执行多个任务并输出一些结果的解决方案,我们只需要最快的一个。解决方案使用 Context
和取消函数来在获得第一个结果后调用取消。SearchSrc
结构提供了 Search
方法,该方法会导致写入结果的通道。请注意,Search
方法使用 time.Sleep
函数模拟延迟。对于来自 Search
方法的每个通道,合并函数触发写入最终输出通道的 goroutine
,该通道在 main
方法中读取。从 merge
函数产生的输出通道接收到第一个结果时,将调用存储在变量 cancel
中的 CancelFunc
来取消其余处理。
请注意,Search
方法仍然需要结束,即使其结果不会被处理;因此,需要处理以避免 goroutine
和通道泄漏。
使用 errgroup 传播错误
本教程将展示如何轻松使用 errgroup 扩展包来检测 goroutine 组中运行子任务的错误。
如何做...
-
打开控制台并创建文件夹
chapter10/recipe07
。 -
导航到目录。
-
创建文件
lines.go
,内容如下:
package main
import (
"bufio"
"context"
"fmt"
"log"
"strings"
"golang.org/x/sync/errgroup"
)
const data = `line one
line two with more words
error: This is erroneous line`
func main() {
log.Printf("Application %s starting.", "Error Detection")
scanner := bufio.NewScanner(strings.NewReader(data))
scanner.Split(bufio.ScanLines)
// For each line fire a goroutine
g, _ := errgroup.WithContext(context.Background())
for scanner.Scan() {
row := scanner.Text()
g.Go(func() error {
return func(s string) error {
if strings.Contains(s, "error:") {
return fmt.Errorf(s)
}
return nil
}(row)
})
}
// Wait until the goroutines finish
if err := g.Wait(); err != nil {
fmt.Println("Error while waiting: " + err.Error())
}
}
-
通过
go run lines.go
执行代码。 -
查看输出:
它是如何工作的...
golang.org/x/sync/errgroup
包有助于简化 goroutine 组的错误传播和上下文取消。Group
包含消耗无参数函数返回 error
的 Go 方法。此函数应包含应由执行的 goroutine
完成的任务。errgroup
的 Group
的 Wait
方法等待直到 Go 方法中执行的所有任务完成,如果其中任何一个返回 err
,则返回第一个非空错误。这样,就可以简单地从运行的 goroutine 组中传播错误。
请注意,Group
也是使用上下文创建的。Context
用作取消其他任务的机制,如果发生错误。在 goroutine
函数返回 error
后,内部实现会取消上下文,因此正在运行的任务也可能会被取消。
第十一章:提示和技巧
本章将涵盖以下示例:
-
日志定制
-
测试代码
-
对代码进行基准测试
-
创建子测试
-
测试 HTTP 处理程序
-
通过反射访问标签
-
对切片进行排序
-
将 HTTP 处理程序分成组
-
利用 HTTP/2 服务器推送
介绍
这最后一章添加了一些与测试、设计应用程序接口以及利用sort
和reflect
包相关的附加示例。
检查 Go 是否已正确安装。第一章中准备就绪部分的检索 Golang 版本示例,与环境交互将帮助您。
确保端口8080
未被其他应用程序使用。
日志定制
除了使用log
包中的默认记录器进行记录外,标准库还提供了一种根据应用程序或包的需求创建自定义记录器的方法。本示例将简要介绍如何创建自定义记录器。
如何做...
-
打开控制台并创建文件夹
chapter11/recipe01
。 -
导航到目录。
-
创建名为
logging.go
的文件,其中包含以下内容:
package main
import (
"log"
"os"
)
func main() {
custLogger := log.New(os.Stdout, "custom1: ",
log.Ldate|log.Ltime)
custLogger.Println("Hello I'm customized")
custLoggerEnh := log.New(os.Stdout, "custom2: ",
log.Ldate|log.Lshortfile)
custLoggerEnh.Println("Hello I'm customized logger 2")
}
-
通过
go run logging.go
执行代码。 -
查看输出:
它是如何工作的...
log
包提供了New
函数,简化了自定义记录器的创建。New
函数接受Writer
作为参数,该参数可以是实现Writer
接口的任何对象,以及以字符串形式的前缀和由标志组成的日志消息的形式。最后一个参数是最有趣的,因为通过它,您可以使用动态字段增强日志消息,例如日期和文件名。
请注意,前面的示例中,第一个记录器custLogger
配置了在日志消息前显示日期和时间的标志。第二个记录器custLoggerEnh
使用标志Ldate
和Lshortfile
来显示文件名和日期。
测试代码
测试和基准测试自然属于软件开发。作为一种现代语言,Go 支持从头开始进行这些操作。在这个示例中,将描述测试的基础知识。
如何做...
-
打开控制台并创建文件夹
chapter11/recipe02
。 -
导航到目录。
-
创建名为
sample_test.go
的文件,其中包含以下内容:
package main
import (
"strconv"
"testing"
)
func TestSampleOne(t *testing.T) {
expected := "11"
result := strconv.Itoa(10)
compare(expected, result, t)
}
func TestSampleTwo(t *testing.T) {
expected := "11"
result := strconv.Itoa(10)
compareWithHelper(expected, result, t)
}
func TestSampleThree(t *testing.T) {
expected := "10"
result := strconv.Itoa(10)
compare(expected, result, t)
}
func compareWithHelper(expected, result string, t *testing.T) {
t.Helper()
if expected != result {
t.Fatalf("Expected result %v does not match result %v",
expected, result)
}
}
func compare(expected, result string, t *testing.T) {
if expected != result {
t.Fatalf("Fail: Expected result %v does not match result %v",
expected, result)
}
t.Logf("OK: Expected result %v = %v",
expected, result)
}
-
通过
go test -v
执行测试。 -
在终端中查看输出:
它是如何工作的...
标准库的testing
包提供了对代码测试需求的支持。test
函数需要满足名称模式TestXXX
。默认情况下,测试工具会查找名为xxx_test.go
的文件。请注意,每个测试函数都需要接受T
指针参数,该参数提供了用于测试控制的有用方法。通过T
结构指针,可以设置测试的状态。例如,Fail
和FailNow
方法会导致测试失败。借助T
结构指针的帮助,可以通过调用Skip
、Skipf
或SkipNow
来跳过测试。
T
指针的有趣方法是Helper
方法。通过调用Helper
方法,当前函数被标记为辅助函数,如果在该函数内调用FailNow
(Fatal
),则测试输出将指向测试中调用该函数的代码行,如前面示例代码中所示。
请注意,如果测试工具未以详细模式运行(使用-v
标志),或者特定测试失败(仅适用于T
测试),则Log
方法(及其变体)将不可见。尝试在不使用-v
标志的情况下运行此示例代码。
另请参阅
-
以下示例涵盖了基准测试的基础知识
-
有关测试包的更详细描述,请参阅
golang.org/pkg/testing
中测试包的丰富文档。
对代码进行基准测试
上一个示例介绍了测试包的测试部分,在本示例中将介绍基准测试的基础知识。
如何做...
-
打开控制台并创建文件夹
chapter11/recipe03
。 -
导航到目录。
-
创建名为
sample_test.go
的文件,内容如下:
package main
import (
"log"
"testing"
)
func BenchmarkSampleOne(b *testing.B) {
logger := log.New(devNull{}, "test", log.Llongfile)
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
logger.Println("This si awesome")
}
b.StopTimer()
}
type devNull struct{}
func (d devNull) Write(b []byte) (int, error) {
return 0, nil
}
-
通过
go test -bench=
执行基准测试。 -
在终端中查看输出:
它是如何工作的...
除了纯测试支持外,测试包还提供了用于测量代码性能的机制。为此,使用B
结构指针作为参数,并且测试文件中的基准测试函数命名为BenchmarkXXXX
。
基准测试函数的关键部分是操作定时器和使用循环迭代计数器N
。
如您所见,定时器通过Reset
/Start
/StopTimer
方法进行操作。通过这些方法,基准测试的结果会受到影响。请注意,定时器在基准测试函数开始时开始运行,而ResetTimer
函数只是重新启动它。
B
的N
字段是测量循环中的迭代次数。N
值设置为足够高的值,以可靠地测量基准测试的结果。基准测试日志中显示迭代次数和每次迭代的测量时间。
另请参阅
-
下一个示例将展示如何在测试中创建子测试
-
有关基准测试的更多选项和信息,请查看此处的包文档:
golang.org/pkg/testing
创建子测试
在某些情况下,有用的是创建一组可能具有类似设置或清理代码的测试。这可以在没有为每个测试创建单独函数的情况下完成。
如何做...
-
打开控制台并创建文件夹
chapter11/recipe04
。 -
导航到目录。
-
创建名为
sample_test.go
的文件,内容如下:
package main
import (
"fmt"
"strconv"
"testing"
)
var testData = []int{10, 11, 017}
func TestSampleOne(t *testing.T) {
expected := "10"
for _, val := range testData {
tc := val
t.Run(fmt.Sprintf("input = %d", tc), func(t *testing.T) {
if expected != strconv.Itoa(tc) {
t.Fail()
}
})
}
}
-
通过
go test -v
执行测试。 -
在终端中查看输出:
它是如何工作的...
testing
包的T
结构还提供了Run
方法,可用于运行嵌套测试。Run
方法需要子测试的名称和将要执行的测试函数。例如,使用表驱动测试时,这种方法可能很有益。代码示例只是使用int
值的简单切片作为输入。
基准测试结构B
也包含相同的方法Run
,可以提供一种创建复杂基准测试后续步骤的方法。
另请参阅
在包文档中仍有很多内容要找出,golang.org/pkg/testing
。
测试 HTTP 处理程序
测试HTTP
服务器可能会很复杂。Go 标准库通过一个方便的包net/http/httptest
简化了这一点。本示例描述了如何利用此包来测试HTTP
处理程序。
如何做...
-
打开控制台并创建文件夹
chapter11/recipe05
。 -
导航到目录。
-
创建名为
sample_test.go
的文件,内容如下:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
)
const cookieName = "X-Cookie"
func HandlerUnderTest(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Domain: "localhost",
Expires: time.Now().Add(3 * time.Hour),
Name: cookieName,
})
r.ParseForm()
username := r.FormValue("username")
fmt.Fprintf(w, "Hello %s!", username)
}
func TestHttpRequest(t *testing.T) {
req := httptest.NewRequest("GET",
"http://unknown.io?username=John", nil)
w := httptest.NewRecorder()
HandlerUnderTest(w, req)
var res *http.Cookie
for _, c := range w.Result().Cookies() {
if c.Name == cookieName {
res = c
}
}
if res == nil {
t.Fatal("Cannot find " + cookieName)
}
content, err := ioutil.ReadAll(w.Result().Body)
if err != nil {
t.Fatal("Cannot read response body")
}
if string(content) != "Hello John!" {
t.Fatal("Content not matching expected value")
}
}
-
通过
go test
执行测试。 -
在终端中查看输出:
它是如何工作的...
对于Handler
或HandlerFunc
的测试,可以利用net/http/httptest
。该包提供了ResponseRecorder
结构,能够记录响应内容并将其提供回来以断言值。用于组装请求的是net/http
包的NewRequest
函数。
net/http/httptest
包还包含了在本地主机上监听系统选择端口的 HTTP 服务器版本。此实现旨在用于端到端测试。
通过反射访问标签
Go 语言允许给结构化字段打标签,附加额外信息。这些信息通常用作编码器的附加信息,或者对结构体进行任何类型的额外处理。这个示例将向你展示如何访问这些信息。
如何做...
-
打开控制台并创建文件夹
chapter11/recipe06
。 -
导航到目录。
-
创建文件
structtags.go
,内容如下:
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string `json:"p_name" bson:"pName"`
Age int `json:"p_age" bson:"pAge"`
}
func main() {
f := &Person{"Tom", 30}
describe(f)
}
func describe(f interface{}) {
val := reflect.TypeOf(f).Elem()
for i := 0; i < val.NumField(); i++ {
typeF := val.Field(i)
fieldName := typeF.Name
jsonTag := typeF.Tag.Get("json")
bsonTag := typeF.Tag.Get("bson")
fmt.Printf("Field : %s jsonTag: %s bsonTag: %s\n",
fieldName, jsonTag, bsonTag)
}
}
-
通过
go run structtags.go
执行代码。 -
在终端中查看输出:
它是如何工作的...
可以使用reflect
包提取struct
标签。通过调用TypeOf
,我们得到了Person
的指针Type
,随后通过调用Elem
,我们得到了指针指向的值的Type
。
结果的Type
让我们可以访问struct
类型Person
及其字段。通过遍历字段并调用Field
方法检索字段,我们可以获得StructField
。StructField
类型包含Tag
字段,该字段提供对struct
标签的访问。然后,StructTag
字段上的Get
方法返回特定的标签。
对切片进行排序
数据排序是一个非常常见的任务。Go 标准库通过 sort 包简化了排序。这个示例简要介绍了如何使用它。
如何做...
-
打开控制台并创建文件夹
chapter11/recipe07
。 -
导航到目录。
-
创建文件
sort.go
,内容如下:
package main
import (
"fmt"
"sort"
)
type Gopher struct {
Name string
Age int
}
var data = []Gopher{
{"Daniel", 25},
{"Tom", 19},
{"Murthy", 33},
}
type Gophers []Gopher
func (g Gophers) Len() int {
return len(g)
}
func (g Gophers) Less(i, j int) bool {
return g[i].Age > g[j].Age
}
func (g Gophers) Swap(i, j int) {
tmp := g[j]
g[j] = g[i]
g[i] = tmp
}
func main() {
sort.Slice(data, func(i, j int) bool {
return sort.StringsAreSorted([]string{data[i].Name,
data[j].Name})
})
fmt.Printf("Sorted by name: %v\n", data)
gophers := Gophers(data)
sort.Sort(gophers)
fmt.Printf("Sorted by age: %v\n", data)
}
-
通过
go run sort.go
执行代码。 -
在终端中查看输出:
它是如何工作的...
示例代码展示了如何舒适地使用sort
包对切片进行排序的两种方式。第一种方法更加临时,它使用了sort
包的Slice
函数。Slice
函数消耗要排序的切片和所谓的 less 函数,该函数定义了元素i
是否应该在元素j
之前排序。
第二种方法需要更多的代码和提前规划。它利用了sort
包的Interface
接口。该接口充当数据的代表,并要求其在排序数据上实现必要的方法:Len
(定义数据的数量)、Less
(less 函数)、Swap
(调用以交换元素)。如果数据值实现了这个接口,那么可以使用sort
包的Sort
函数。
原始类型切片float64
、int
和string
在sort
包中有涵盖。因此,可以使用现有的实现。例如,要对字符串切片进行排序,可以调用Strings
函数。
将 HTTP 处理程序分组
这个示例提供了关于如何将 HTTP 处理程序分离成模块的建议。
如何做...
-
打开控制台并创建文件夹
chapter11/recipe08
。 -
导航到目录。
-
创建文件
handlegroups.go
,内容如下:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
log.Println("Staring server...")
// Adding to mani Mux
mainMux := http.NewServeMux()
mainMux.Handle("/api/",
http.StripPrefix("/api", restModule()))
mainMux.Handle("/ui/",
http.StripPrefix("/ui", uiModule()))
if err := http.ListenAndServe(":8080", mainMux); err != nil {
panic(err)
}
}
func restModule() http.Handler {
// Separate Mux for all REST
restApi := http.NewServeMux()
restApi.HandleFunc("/users", func(w http.ResponseWriter,
r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `[{"id":1,"name":"John"}]`)
})
return restApi
}
func uiModule() http.Handler {
// Separate Mux for all UI
ui := http.NewServeMux()
ui.HandleFunc("/users", func(w http.ResponseWriter,
r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<html><body>Hello from UI!</body></html>`)
})
return ui
}
-
通过
go run handlegroups.go
执行代码。 -
查看输出:
- 访问浏览器 URL
http://localhost:8080/api/users
,输出应该如下所示:
- 同样,您可以测试
http://localhost:8080/ui/users
:
它是如何工作的...
为了将处理程序分离成模块,代码使用了ServeMux
来为每个模块(rest
和ui
)进行处理。给定模块的 URL 处理是相对定义的。这意味着如果Handler
的最终 URL 应该是/api/users
,那么模块内定义的路径将是/users
。模块本身将设置为/api/
URL。
通过利用StripPrefix
函数将模块插入到名为mainMux
的主ServeMux
指针中,模块被插入到主ServeMux
中。例如,通过StripPrefix("/api",restModule())
将由restModule
函数创建的 REST 模块插入到主ServeMux
中。然后模块内的处理 URL 将是/users
,而不是/api/users
。
利用 HTTP/2 服务器推送
HTTP/2 规范为服务器提供了在被请求之前推送资源的能力。本示例演示了如何实现服务器推送。
准备工作
准备私钥和自签名 X-509 证书。为此,可以使用openssl
实用程序。通过执行命令openssl genrsa -out server.key 2048
,使用 RSA 算法生成私钥文件server.key
。基于此私钥,可以通过调用openssl req -new -x509 -sha256 -key server.key -out server.crt -days 365
生成 X-509 证书。创建了server.crt
文件。
操作步骤...
-
打开控制台并创建文件夹
chapter11/recipe09
。 -
导航到目录。
-
创建文件
push.go
,内容如下:
package main
import (
"io"
"log"
"net/http"
)
func main() {
log.Println("Staring server...")
// Adding to mani Mux
http.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){
if p, ok := w.(http.Pusher); ok {
if err := p.Push("/app.css", nil); err != nil {
log.Printf("Push err : %v", err)
}
}
io.WriteString(w,
`<html>
<head>
<link rel="stylesheet" type="text/css" href="app.css">
</head>
<body>
<p>Hello</p>
</body>
</html>`
)
})
http.HandleFunc("/app.css", func(w http.ResponseWriter,
r *http.Request) {
io.WriteString(w,
`p {
text-align: center;
color: red;
}`)
})
if err := http.ListenAndServeTLS(":8080", "server.crt",
"server.key", nil);
err != nil {
panic(err)
}
}
-
通过
go run push.go
启动服务器。 -
打开浏览器,在 URL
https://localhost:8080
中打开开发者工具(查看Push
作为app.css
的发起者):
工作原理...
首先,注意 HTTP/2 需要安全连接。服务器推送非常简单实现。自 Go 1.8 以来,HTTP 包提供了Pusher
接口,可以在资源被请求之前用于Push
资产。如果客户端(通常是浏览器)支持 HTTP/2 协议并且与服务器的握手成功,Handler
或HandlerFunc
中的ResponseWriter
可以转换为Pusher
。Pusher
只提供Push
方法。Push
方法消耗目标(可以是绝对路径或绝对 URL)到资源和PushOptions
,可以提供额外选项(默认情况下可以使用 nil)。
在上面的示例中,查看浏览器中开发者工具的输出。推送的资源在 Initiator 列中具有值Push
。