首页 > 其他分享 >Go-标准库秘籍(全)

Go-标准库秘籍(全)

时间:2024-05-04 22:59:15浏览次数:19  
标签:... func err 秘籍 fmt 标准 go Go main

Go 标准库秘籍(全)

原文:zh.annas-archive.org/md5/F3FFC94069815F41B53B3D7D6E774406

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

感谢您给予本书机会!本书是一本指南,带您了解 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并注册,以便直接将文件发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

文件下载完成后,请确保您使用最新版本的解压缩软件解压或提取文件夹:

  • 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 句柄。这是一个例子:“验证您的GOPATHGOROOT环境变量是否设置正确。”

代码块设置如下:

package main
import (
  "log"
  "runtime"
)

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目会以粗体显示:

package main
import (
  "log"
  "runtime"
)

粗体:表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。

警告或重要说明看起来像这样。

提示和技巧看起来像这样。

章节

在本书中,您会经常看到几个标题(准备工作如何做它是如何工作的还有更多另请参阅)。

为了清晰地说明如何完成一个食谱,使用以下各节:

准备工作

本节告诉您食谱中会有什么,并描述如何设置食谱所需的任何软件或任何初步设置。

如何做…

本节包含了遵循食谱所需的步骤。

它是如何工作的…

本节通常包括对前一节发生的事情的详细解释。

还有更多…

本节包括有关食谱的其他信息,以使您对食谱更加了解。

另请参阅

本节为食谱提供了其他有用信息的链接。

第一章:与环境交互

在本章中,将涵盖以下配方:

  • 检索 Golang 版本

  • 访问程序参数

  • 使用 flag 包创建程序接口

  • 获取并设置带有默认值的环境变量

  • 检索当前工作目录

  • 获取当前进程 PID

  • 处理操作系统信号

  • 调用外部进程

  • 检索子进程信息

  • 从子进程读取/写入

  • 优雅地关闭应用程序

  • 使用功能选项进行文件配置

介绍

每个程序一旦被执行,就存在于操作系统的环境中。程序接收输入并向该环境提供输出。操作系统还需要与程序通信,让程序知道外部发生了什么。最后,程序需要做出适当的响应。

本章将带您了解系统环境的发现基础知识,通过程序参数对程序进行参数化,以及操作系统信号的概念。您还将学习如何执行和与子进程通信。

检索 Golang 版本

在构建程序时,最好记录环境设置、构建版本和运行时版本,特别是如果您的应用程序更复杂。这有助于您分析问题,以防出现故障。

除了构建版本和例如环境变量之外,编译二进制文件的 Go 版本可以包含在日志中。以下的步骤将向您展示如何将 Go 运行时版本包含在程序信息中。

准备就绪

安装并验证 Go 安装。以下步骤可能有所帮助:

  1. 在您的计算机上下载并安装 Go。

  2. 验证您的 GOPATHGOROOT 环境变量是否正确设置。

  3. 打开终端并执行 go version。如果得到带有版本名称的输出,则 Go 已正确安装。

  4. GOPATH/src 文件夹中创建存储库。

如何做...

以下步骤涵盖了解决方案:

  1. 打开控制台并创建文件夹 chapter01/recipe01

  2. 导航到目录。

  3. 创建 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())
        }

  1. 通过执行 go run main.go 运行代码。

  2. 在终端中查看输出:

它是如何工作的...

runtime 包包含许多有用的函数。要找出 Go 运行时版本,可以使用 Version 函数。文档说明该函数返回提交的哈希值,以及二进制构建时的日期或标签。

实际上,Version 函数返回 runtime/internal/sysVersion 常量。常量本身位于 $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.csvrole.csv是修改程序调用的参数(在这种情况下是要解析的文件)。

如何做...

  1. 打开控制台并创建文件夹 chapter01/recipe02

  2. 导航到目录。

  3. 创建 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)
          }
        }
  1. 通过执行 go build -o test 构建二进制文件。

  2. 执行命令./test arg1 arg2。(Windows 用户可以运行test.exe arg1 arg2)。

  3. 在终端中查看输出:

它是如何工作的...

Go 标准库提供了几种访问程序调用参数的方法。最通用的方法是通过 OS 包中的Args变量访问参数。

通过这种方式,您可以在字符串切片中获取命令行中的所有参数。这种方法的优点是参数的数量是动态的,这样您可以,例如,将要由程序处理的文件的名称传递给程序。

上面的示例只是回显传递给程序的所有参数。最后,假设二进制文件名为test,程序运行由终端命令./test arg1 arg2执行。

具体来说,os.Args[0]将返回./testos.Args[1:]返回不带二进制名称的其余参数。在现实世界中,最好不要依赖于传递给程序的参数数量,而是始终检查参数数组的长度。否则,如果给定索引上的参数不在范围内,程序将自然地发生恐慌。

还有更多...

如果参数被定义为标志,-flag value,则需要额外的逻辑来将值分配给标志。在这种情况下,使用flag包有更好的方法来解析这些标志。这种方法是下一个配方的一部分。

使用 flag 包创建程序接口

前面的配方描述了如何通过非常通用的方法访问程序参数。

这个配方将提供一种通过程序标志定义接口的方法。这种方法主导了基于 GNU/Linux、BSD 和 macOS 的系统。程序调用的示例可以是ls -l,在*NIX 系统上,它将列出当前目录中的文件。

Go 标志处理包不支持像ls -ll这样的标志组合,其中在单个破折号后有多个标志。每个标志必须是单独的。Go 标志包也不区分长选项和短选项。最后,-flag--flag是等效的。

如何做到...

  1. 打开控制台并创建文件夹chapter01/recipe03

  2. 导航到目录。

  3. 创建带有以下内容的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++
          }
        }
  1. 通过执行go build -o util来构建二进制文件。

  2. 从控制台执行./util -retry 2 -prefix=example -array=1,2

  3. 在终端中查看输出:

它是如何工作的...

对于代码中的标志定义,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)就是我们所需要的。如果环境变量不存在,将配置默认值也非常实用。这样,应用程序开发人员的生活就轻松多了。

本教程将演示如何读取、设置和取消设置环境变量。它还将向您展示如何在变量未设置时实现默认选项。

如何做…

  1. 打开控制台并创建文件夹chapter01/recipe04

  2. 导航到目录。

  3. 创建get.go文件,内容如下:

        package main

        import (
          "log"
          "os"
        )

        func main() {
          connStr := os.Getenv("DB_CONN")
          log.Printf("Connection string: %s\n", connStr)
        }
  1. 通过在终端中调用DB_CONN=db:/user@example && go run get.go来执行代码。

  2. 在终端中查看输出:

  1. 创建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)
        }
  1. 在终端中调用unset DB_CONN && go run lookup.go来执行代码。

  2. 在终端中查看输出:

  1. 创建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
        }
  1. 在终端中执行go run main.go来运行代码。

  2. 在终端中查看输出:

它是如何工作的…

环境变量可以通过os包中的GetenvSetenv函数来访问。这些函数的名称不言自明,不需要进一步的描述。

os包中还有一个有用的函数。LookupEnv函数提供两个值作为结果;变量的值,以及布尔值,定义变量在环境中是否设置。

os.Getenv函数的缺点是,即使在环境变量未设置的情况下,它也会返回空字符串。

这个缺点可以通过os.LookupEnv函数来克服,该函数返回环境变量的字符串值和一个布尔值,指示变量是否设置。

要实现检索环境变量或默认值,使用os.LookupEnv函数。简单地说,如果变量未设置,也就是第二个返回值是false,那么就返回默认值。该函数的使用是第 9 步的一部分。

检索当前工作目录

应用程序的另一个有用信息来源是目录,程序二进制文件所在的位置。有了这些信息,程序就可以访问与二进制文件一起放置的资源和文件。

自 Go 1.8 版本以来,本教程使用了 Go 的解决方案。这是首选方案。

如何做…

  1. 打开控制台并创建文件夹chapter01/recipe05

  2. 导航到目录。

  3. 创建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)
        }
  1. 通过命令go build -o binary构建二进制文件。

  2. 通过终端调用./binary来执行二进制文件。

  3. 查看输出。它应该显示在您的机器上的绝对路径:

它是如何工作的…

自 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,并将其与操作系统实用程序一起使用以获取更多信息。

如何做…

  1. 打开控制台并创建文件夹chapter01/recipe06

  2. 导航到目录。

  3. 使用以下内容创建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))
        }
  1. 通过执行go run main.go来运行代码。

  2. 在终端中查看输出:

工作原理…

os包中的Getpid函数返回进程的 PID。示例代码展示了如何从操作系统实用程序ps获取有关进程的更多信息。

在应用程序启动时打印 PID 可能很有用,这样在崩溃时也可以通过检索到的 PID 来调查原因。

处理操作系统信号

信号是操作系统与正在运行的进程通信的基本方式。最常见的两个信号是SIGINTSIGTERM。这些信号会导致程序终止。

还有一些信号,比如SIGHUPSIGHUP表示调用进程的终端已关闭,例如,程序可以决定转移到后台。

Go 提供了一种处理应用程序接收到信号时的行为的方法。本示例将提供一个实现处理的示例。

如何做…

  1. 打开控制台并创建文件夹chapter01/recipe07

  2. 导航到目录。

  3. 使用以下内容创建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)
        }
  1. 通过执行go run main.go来运行代码。

  2. 通过按下CTRL + C发送SIGINT信号给应用程序。

  3. 查看输出:

工作原理…

在资源被获取的应用程序中,如果立即终止可能会发生资源泄漏。最好处理信号并采取一些必要的步骤来释放资源。上述代码展示了如何做到这一点的概念。

signal包中的Notify函数将帮助我们处理接收到的信号。

如果在Notify函数中未指定信号作为参数,函数将捕获所有可能的信号。

请注意,signal包的Notify函数通过sChan通道与goroutine通信。Notify然后捕获定义的信号并将其发送到goroutine进行处理。最后,exitChan用于解析进程的退出代码。

重要的信息是,如果分配的通道未准备好,Notify函数将不会阻止信号。这样信号可能会被错过。为了避免错过信号,最好创建缓冲通道。

请注意,SIGKILLSIGSTOP信号可能无法被Notify函数捕获,因此无法处理这些信号。

调用外部进程

Go 二进制文件也可以用作各种实用程序的工具,并且可以使用go run来替代 bash 脚本。出于这些目的,通常会调用命令行实用程序。

在这个示例中,将提供如何执行和处理子进程的基础知识。

准备工作

测试以下命令是否在你的终端中工作:

  1. 测试ls(Windows 中为dir)命令是否存在于你的$PATH中。

  2. 你应该能够在终端中执行ls(Windows 中为dir)命令。

如何做…

以下步骤涵盖了解决方案:

  1. 打开控制台并创建文件夹chapter01/recipe08

  2. 导航到目录。

  3. 创建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())
          }
        }
  1. 通过执行go run run.go来运行代码。

  2. 在终端中查看输出:

  1. 创建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())
          }
        }
  1. 通过执行go run start.go来运行代码。

  2. 在终端中查看输出:

工作原理…

Go 标准库提供了一种简单的调用外部进程的方法。这可以通过os/exec包的Command函数来实现。

最简单的方法是创建Cmd结构并调用Run函数。Run函数执行进程并等待其完成。如果命令退出时出现错误,err值将不为空。

这更适合调用操作系统的实用程序和工具,这样程序不会挂起太久。

进程也可以异步执行。这可以通过调用Cmd结构的Start方法来实现。在这种情况下,进程被执行,但是主goroutine不会等待它结束。Wait方法可以用来等待进程结束。Wait方法完成后,进程的资源将被释放。

这种方法更适合执行长时间运行的进程和程序依赖的服务。

另请参阅

这个示例描述了如何简单地执行子进程。本章还提供了检索子进程信息从子进程读取/写入的示例,介绍了如何从子进程读取和写入,并获取有用的进程信息的步骤。

检索子进程信息

调用外部进程示例描述了如何同步和异步调用子进程。自然地,要处理进程行为,你需要更多地了解进程。这个示例展示了如何在子进程终止后获取 PID 和基本信息。

关于运行进程的信息只能通过syscall包获得,而且高度依赖于平台。

准备工作

测试sleep(Windows 中为timeout)命令是否存在于终端中。

如何做…

  1. 打开控制台并创建文件夹chapter01/recipe09

  2. 导航到目录。

  3. 创建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)
        }
  1. 通过执行go run main_running.go来运行代码。

  2. 在终端中查看输出:

  1. 创建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())
        }
  1. 通过执行go run main.go来运行代码。

  2. 在终端中查看输出:

工作原理…

os/exec标准库提供了执行进程的方法。使用Command,将返回Cmd结构。Cmd提供了对进程表示的访问。当进程正在运行时,你只能找到 PID。

你只能获取有关进程的少量信息。但是通过检索进程的 PID,你可以调用操作系统的实用程序来获取更多信息。

请记住,即使子进程正在运行,也可以获取其 PID。另一方面,只有在进程终止后,os包的ProcessState结构才可用。

另请参阅

本章中有与进程处理相关的从子进程中读取/写入调用外部进程的配方。

从子进程中读取/写入

每个执行的进程都有标准输出、输入和错误输出。Go 标准库提供了读取和写入这些内容的方法。

本配方将介绍如何读取进程的输出并写入子进程的输入的方法。

准备就绪

验证以下命令是否在终端中工作:

  1. 测试终端中是否存在ls(Windows 中的dir)命令。

  2. 您应该能够在终端中执行ls(Windows 中的dir)命令。

如何做…

  1. 打开控制台并创建文件夹chapter01/recipe10

  2. 导航到目录。

  3. 创建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))

       }
  1. 通过执行go run main_read_output.go来运行代码。

  2. 在终端中查看输出:

  1. 创建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()))

        }
  1. 通过执行go run main_read_stdout.go来运行代码。

  2. 在终端中查看输出:

  1. 创建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())
          }
        }
  1. 通过执行go run main_read.go来运行代码。

  2. 在终端中查看输出:

  1. 创建sample.go文件,内容如下:
        package main

        import (
          "bufio"
          "fmt"
          "os"
        )

        func main() {
          sc := bufio.NewScanner(os.Stdin)

          for sc.Scan() {
            fmt.Println(sc.Text())
          }
        }
  1. 创建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()

        }
  1. 通过执行go run main.go来运行代码。

  2. 在终端中查看输出:

工作原理…

os/exec包的Cmd结构提供了访问进程输出/输入的函数。有几种方法可以读取进程的输出。

读取进程输出的最简单方法之一是使用Cmd结构的OutputCombinedOutput方法(获取StderrStdout)。在调用此函数时,程序会同步等待子进程终止,然后将输出返回到字节缓冲区。

除了OutputOutputCombined方法外,Cmd结构提供了Stdout属性,可以将io.Writer分配给它。分配的写入器然后作为进程输出的目的地。它可以是文件、字节缓冲区或任何实现io.Writer接口的类型。

读取进程输出的最后一种方法是通过调用Cmd结构的StdoutPipe方法获取io.ReaderStdoutPipe方法在Stdout之间创建管道,进程在其中写入输出,并提供Reader,它作为程序读取进程输出的接口。这样,进程的输出被传送到检索到的io.Reader

向进程的stdin写入的方式相同。在所有选项中,将演示使用io.Writer的方式。

可以看到,有几种方法可以从子进程中读取和写入。使用stderrstdin的方式几乎与步骤 6-7 中描述的方式相同。最后,访问输入/输出的方法可以这样分为:

  • 同步(等待进程结束并获取字节):使用CmdOutputCombinedOutput方法。

  • IO:输出或输入以io.Writer/Reader的形式提供。XXXPipeStdXXX属性是这种方法的正确选择。

IO 类型更加灵活,也可以异步使用。

优雅地关闭应用程序

服务器和守护程序是长时间运行的程序(通常是几天甚至几周)。这些长时间运行的程序通常在开始时分配资源(数据库连接,网络套接字),并在资源存在的时间内保持这些资源。如果这样的进程被终止并且关闭未得到适当处理,可能会发生资源泄漏。为了避免这种行为,应该实现所谓的优雅关闭。

在这种情况下,优雅意味着应用程序捕获终止信号(如果可能的话),并在终止之前尝试清理和释放分配的资源。这个食谱将向您展示如何实现优雅关闭。

食谱处理操作系统信号描述了捕获操作系统信号。相同的方法将用于实现优雅关闭。在程序终止之前,它将清理并执行一些其他活动。

如何做...

  1. 打开控制台并创建文件夹chapter01/recipe11

  2. 导航到目录。

  3. 创建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()
        }
  1. 通过执行go run main.go运行代码。

  2. 按下CTRL + C发送SIGINT信号。

  3. 等待终端输出如下:

  1. recipe11文件夹还应包含一个名为test_XXXX.log的文件,其中包含如下行:

它是如何工作的...

sigChan读取是阻塞的,因此程序会一直运行,直到通过通道发送信号。sigChanNotify函数发送信号的通道。

程序的主要代码在一个新的goroutine中运行。这样,当主函数在sigChan上被阻塞时,工作将继续。一旦从操作系统发送信号到进程,sigChan接收到信号并在从sigChan通道读取的行下面的代码执行。这段代码可以被视为清理部分。

请注意,步骤 7 的终端输出包含最终日志应用程序释放所有资源,这是清理部分的一部分。

另请参阅

有关信号捕获工作原理的详细描述在食谱处理操作系统信号中。

使用功能选项进行文件配置

这个食谱与 Go 标准库没有直接关系,但包括如何处理应用程序的可选配置。该食谱将在实际情况下使用函数选项模式与文件配置。

如何做...

  1. 打开控制台并创建文件夹chapter01/recipe12

  2. 导航到目录。

  3. 创建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())
        }
  1. 在同一文件夹中,创建名为config.json的文件,内容如下:
        {
          "consul_ip":"127.0.0.1"
        }
  1. 通过命令CONN_DB=oracle://local:5921 go run main.go执行代码。

  2. 查看输出:

它是如何工作的...

函数选项模式的核心概念是配置 API 包含功能参数。在这种情况下,NewClient函数接受各种数量的ConfigFunc参数,然后逐个应用于defaultClient结构。这样,可以以极大的灵活性修改默认配置。

查看FromFileFromEnv函数,它们返回ConfigFunc,实际上是访问文件或环境变量。

最后,您可以检查输出,该输出应用了配置选项和结果Client结构,其中包含来自文件和环境变量的值。

第二章:字符串和其他内容

本章中的配方有:

  • 在字符串中查找子字符串

  • 将字符串分解为单词

  • 使用分隔符连接字符串切片

  • 使用 writer 连接字符串

  • 使用 tabwriter 对齐文本

  • 替换字符串的一部分

  • 通过正则表达式模式在文本中查找子字符串

  • 从非 Unicode 字符集解码字符串

  • 控制大小写

  • 解析逗号分隔的数据

  • 管理字符串中的空格

  • 缩进文本文档

介绍

在开发人员的生活中,对字符串和基于字符串的数据进行操作是常见任务。本章介绍如何使用 Go 标准库处理这些任务。毫无疑问,使用标准库可以做很多事情。

检查 Go 是否已正确安装。第一章的准备就绪部分,与环境交互检索 Golang 版本配方将对您有所帮助。

在字符串中查找子字符串

在开发人员中,查找字符串中的子字符串是最常见的任务之一。大多数主流语言都在标准库中实现了这一点。Go 也不例外。本配方描述了 Go 实现这一功能的方式。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe01

  2. 导航到目录。

  3. 创建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)

        }
  1. 通过执行go run contains.go来运行代码。

  2. 在终端中查看输出:

它是如何工作的...

Go 库strings包含处理字符串操作的函数。这次可以使用Contains函数。Contains函数只是检查字符串是否包含给定的子字符串。实际上,Contains函数中使用了Index函数。

要检查字符串是否以子字符串开头,可以使用HasPrefix函数。要检查字符串是否以子字符串结尾,可以使用HasSuffix函数。

实际上,Contains函数是通过使用同一包中的Index函数实现的。可以猜到,实际实现方式是这样的:如果给定子字符串的索引大于-1,则Contains函数返回true

HasPrefixHasSuffix函数的工作方式不同:内部实现只是检查字符串和子字符串的长度,如果它们相等或字符串更长,则比较字符串的所需部分。

另请参阅

本配方描述了如何匹配精确的子字符串。通过正则表达式模式在文本中查找子字符串配方将帮助您了解如何使用正则表达式模式匹配。

将字符串分解为单词

将字符串分解为单词可能有些棘手。首先,决定单词是什么,分隔符是什么,是否有任何空格或其他字符。做出这些决定后,可以从strings包中选择适当的函数。本配方将描述常见情况。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe02

  2. 导航到目录。

  3. 创建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)
          }

        }
  1. 通过执行go run whitespace.go来运行代码。

  2. 在终端中查看输出:

  1. 创建另一个名为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)
          }

        }
  1. 通过执行go run anyother.go来运行代码。

  2. 在终端中查看输出:

  1. 创建另一个名为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)
           }

        }
  1. 通过执行go run specfunction.go来运行代码。

  2. 在终端中查看输出:

  1. 创建另一个名为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)
          }

        }
  1. 通过执行go run regex.go来运行代码。

  2. 在终端中查看输出:

它是如何工作的...

将字符串拆分为单词的最简单形式考虑任何空白字符作为分隔符。具体来说,空白字符由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 选择语句条件的问题。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe03

  2. 导航到目录。

  3. 创建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)

        }
  1. 通过执行go run join.go来运行代码。

  2. 在终端中查看输出:

  1. 创建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
        }
  1. 通过执行go run join.go来运行代码。

  2. 在终端中查看输出:

它是如何工作的...

为了将字符串切片连接成单个字符串,strings包的Join函数就在那里。简单地说,您需要提供需要连接的字符串切片。这样,您可以舒适地连接字符串切片。步骤1-5展示了使用Join函数的方法。

当然,可以通过迭代切片来手动实现连接。这样,您可以通过一些更复杂的逻辑自定义分隔符。步骤6-8只是表示手动连接如何与更复杂的决策逻辑一起使用,基于当前处理的字符串。

还有更多...

Join函数由bytes包提供,自然用于连接字节切片。

使用写入器连接字符串

除了内置的+运算符外,还有更多连接字符串的方法。本教程将描述使用bytes包和内置的copy函数更高效地连接字符串的方法。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe04

  2. 导航到目录。

  3. 创建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())
         }
  1. 通过执行go run concat_buffer.go来运行代码。

  2. 在终端中查看输出:

  1. 创建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[:]))

        }
  1. 在终端中执行go run concat_copy.go来运行代码。

  2. 在终端中查看输出:

它是如何工作的...

步骤1-5涵盖了将bytesBuffer作为性能友好的字符串连接解决方案的用法。Buffer结构实现了WriteString方法,可以用于有效地将字符串连接到底层字节切片中。

在所有情况下都不需要使用这种改进,只需要在程序将要连接大量字符串的情况下考虑一下(例如,在内存中的 CSV 导出和其他情况)。

在步骤6 - 8中介绍的内置的copy函数可以用于完成string的连接。这种方法对最终字符串长度有一些假设,或者可以实时完成。然而,如果结果写入的缓冲区的容量小于已写部分和要附加的字符串的总和,缓冲区必须扩展(通常是通过分配具有更大容量的新切片)。

还有更多...

仅供比较,这里有一个基准代码,比较了内置的+运算符、bytes.Buffer和内置的copy的性能:

  1. 在其中创建一个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()
        }
  1. 查看基准测试的结果:

使用 tabwriter 对齐文本

在某些情况下,输出(通常是数据输出)是通过制表文本完成的,这些文本以良好排列的单元格格式化。这种格式可以通过text/tabwriter包实现。该包提供了Writer过滤器,它将带有制表符的文本转换为格式良好的输出文本。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe05

  2. 导航到目录。

  3. 创建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()

        }
  1. 通过执行go run tabwriter.go来运行代码。

  2. 在终端中查看输出:

它是如何工作的...

通过调用NewWriter函数创建具有配置参数的Writer过滤器。由此Writer写入的所有数据都根据参数进行格式化。这里使用os.Stdout仅用于演示目的。

text/tabwriter包还提供了一些更多的配置选项,比如flag参数。最有用的是tabwriter.AlignRight,它配置了写入器在每一列中将内容对齐到右侧。

替换字符串的一部分

与字符串处理相关的另一个非常常见的任务是在字符串中替换子字符串。Go 标准库提供了Replace函数和Replacer类型,用于一次替换多个字符串。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe06

  2. 导航到目录。

  3. 创建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)
        }
  1. 通过执行go run replace.go来运行代码。

  2. 在终端中查看输出:

  1. 创建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)
        }
  1. 通过执行go run replacer.go来运行代码。

  2. 在终端中查看输出:

  1. 创建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)
        }
  1. 通过执行go run regexp.go来运行代码。

  2. 在终端中查看输出:

它是如何工作的...

strings包的Replace函数被广泛用于简单的替换。最后一个整数参数定义了将进行多少次替换(在-1的情况下,所有字符串都被替换。看到Replace的第二个用法,只有前两次出现被替换)。Replace函数的用法在步骤1 - 5中呈现。

除了Replace函数,Replacer结构也有WriteString方法。这个方法将使用Replacer中定义的所有替换写入给定的写入器。这种类型的主要目的是可重用性。它可以一次替换多个字符串,并且对并发使用是安全的;参见步骤6 - 8

替换子字符串,甚至匹配模式的更复杂方法,自然是使用正则表达式。Regex类型指针方法ReplaceAllString可以用于此目的。步骤9 - 11说明了regexp包的用法。

还有更多...

如果需要更复杂的逻辑来进行替换,那么regexp包可能是应该使用的包。

通过正则表达式模式在文本中查找子字符串

总是有一些任务,比如验证输入、在文档中搜索信息,甚至从给定字符串中清除不需要的转义字符。对于这些情况,通常使用正则表达式。

Go 标准库包含regexp包,涵盖了正则表达式的操作。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe07

  2. 导航到目录。

  3. 创建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)
          }

        }
  1. 通过执行go run regexp.go来运行代码。

  2. 在终端中查看输出:

工作原理...

FindStringFindAllString函数是在给定字符串中查找匹配模式的最简单方法。唯一的区别是RegexpFindString方法只会返回第一个匹配项。另一方面,FindAllString会返回一个包含所有匹配项的字符串切片。

Regexp类型提供了丰富的FindXXX方法。本教程仅描述了通常最有用的String变体。请注意,前面的代码使用了regexp包的MustCompile函数,如果正则表达式的编译失败,它会引发 panic。

另请参阅

除了这种复杂的正则表达式模式匹配,还可以仅匹配子字符串。这种方法在本章的在字符串中查找子字符串教程中有描述。

从非 Unicode 字符集解码字符串

一个鲜为人知的事实是,所有.go文件中的内容都是用 UTF-8 编码的。信不信由你,Unicode 并不是世界上唯一的字符集。例如,Windows-1250 编码在 Windows 用户中广泛传播。

在处理非 Unicode 字符串时,需要将内容转换为 Unicode。本教程演示了如何解码和编码非 Unicode 字符串。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe08

  2. 导航到目录。

  3. 创建内容为Gdańsk的文件win1250.txt。该文件必须以 windows-1250 字符集进行编码。如果不确定如何操作,只需跳到第 6 步,完成第 7 步后,将创建 windows-1250 编码的文件,然后可以将out.txt文件重命名并返回第 4 步。

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

        }
  1. 通过执行go run decode.go来运行代码。

  2. 在终端中查看输出:

  1. 创建名为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")

        }
  1. 通过执行go run encode.go来运行代码。

  2. 在 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包提供了ToLowerToUpperToTitleTitle函数。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe09

  2. 导航到目录。

  3. 创建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:]
        }
  1. 通过执行go run case.go来运行代码。

  2. 在终端中查看输出:

它是如何工作的...

请注意,Unicode 中的标题大小写映射与大写映射不同。不同之处在于字符数需要特殊处理。这些主要是连字和双字母,如fldzlj,以及一些多音调希腊字符。例如,U+01C7 (LJ)映射到U+01C8 (Lj),而不是U+01C9 (lj)

为了进行适当的不区分大小写比较,应该使用strings包中的EqualFold函数。该函数使用大小写折叠来规范化字符串并进行比较。

解析逗号分隔的数据

有多种表格数据格式。CSV(逗号分隔值)是用于数据传输和导出的最基本格式之一。没有定义 CSV 的标准,但格式本身在 RFC 4180 中有描述。

这个示例介绍了如何舒适地解析 CSV 格式的数据。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe10

  2. 导航到目录。

  3. 创建名为data.csv的文件,其中包含以下内容:

        "Name","Surname","Age"
        # this is comment in data
        "John","Mnemonic",20
        Maria,Tone,21
  1. 创建名为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)
          }
        }
  1. 通过执行go run data.go来运行代码。

  2. 在终端中查看输出:

  1. 创建名为data_uncommon.csv的文件,其中包含以下内容:
       Name;Surname;Age
       "John";Mnemonic;20
       "Maria";Tone;21
  1. 创建名为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)
         }
       }
  1. 通过执行go run data_uncommon.go来运行代码。

  2. 在终端中查看输出:

它是如何工作的...

与简单地逐行扫描输入并使用strings.Split和其他方法解析 CSV 格式不同,Go 提供了更好的方法。encoding/csv包中的NewReader函数返回Reader结构,该结构提供了读取 CSV 文件的 API。Reader结构保留了变量来配置read参数,根据您的需求。

ReaderFieldsPerRecord参数是一个重要的设置。这样可以验证每行的单元格数。默认情况下,当设置为0时,它设置为第一行中的记录数。如果设置为正值,则记录数必须匹配。如果设置为负值,则不进行单元格计数验证。

另一个有趣的配置是Comment参数,它允许您定义解析数据中的注释字符。在示例中,整行都会被忽略。

Go 1.10 现在禁止使用荒谬的逗号和注释设置。这意味着空值、回车、换行、无效符文和 Unicode 替换字符。还禁止将逗号和注释设置为相等。

管理字符串中的空白

字符串输入可能包含过多的空白、过少的空白或不合适的空白字符。本示例包括了如何处理这些并将字符串格式化为所需格式的提示。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe11

  2. 导航到目录。

  3. 创建名为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
        }
  1. 通过执行go run whitespace.go来运行代码。

  2. 查看输出:

它是如何工作的...

在代码处理之前修剪字符串是非常常见的做法,正如前面的代码所示,标准的 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和一些简单的数学运算来找出填充。最后,填充数字前的减号作为右填充,正数作为左填充。

另请参阅

有关如何使用正则表达式的更多提示,您可以在本章中查看通过正则表达式模式在文本中查找子字符串的示例。

对文本文档进行缩进

前面的示例描述了如何进行字符串填充和修剪空白。这个示例将指导您如何对文本文档进行缩进和取消缩进。将使用前面示例中的类似原则。

如何做...

  1. 打开控制台并创建文件夹chapter02/recipe12

  2. 创建名为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:]
         }
  1. 在终端中执行go run main.go来运行代码。

  2. 查看输出:

它是如何工作的...

缩进就像填充一样简单。在这种情况下,使用相同的格式选项。indent实现的更可读形式可以使用strings包的Repeat函数。上述代码中的IndentByRune函数应用了这种方法。

在这种情况下,取消缩进意味着删除给定数量的前导空格。在上述代码中,Unindent的实现会删除最少数量的前导空格或给定的缩进。

另请参阅

管理字符串中的空白示例也以更宽松的方式处理空格。

第三章:处理数字

本章的食谱有:

  • 将字符串转换为数字

  • 比较浮点数

  • 舍入浮点数

  • 浮点数运算

  • 格式化数字

  • 在二进制、八进制、十进制和十六进制之间转换

  • 使用正确的复数格式

  • 生成随机数

  • 操作复数

  • 在度和弧度之间转换

  • 取对数

  • 生成校验和

介绍

数字通常是每个应用程序的不可避免的部分——打印格式化的数字、转换基数表示等等。本章介绍了许多常见的操作。

检查 Go 是否已正确安装。第一章的准备就绪部分,与环境交互,将对您有所帮助。

将字符串转换为数字

本食谱将向您展示如何将包含数字的字符串转换为数值类型(整数或浮点值)。

操作步骤...

  1. 打开控制台并创建文件夹chapter03/recipe01

  2. 导航到该目录。

  3. 创建包含以下内容的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)

        }
  1. 在终端中执行命令go run main.go

  2. 您将看到以下输出:

工作原理...

在前面示例代码中的主要函数是strconv包的ParseInt函数。该函数带有三个参数:输入、输入的基数和位大小。基数确定了如何解析数字。请注意,十六进制的基数(第二个参数)为 16,二进制的基数为 2。strconv包的Atoi函数实际上就是带有基数 10 的ParseInt函数。

ParseFloat函数将字符串转换为浮点数。第二个参数是bitSize的精度。bitSize = 64将导致float64bitSize = 32将导致float64,但可以在不改变其值的情况下转换为float32

比较浮点数

由于浮点数的表示方式,比较两个看似相同的数字时可能会出现不一致。与整数不同,IEEE 浮点数只是近似值。需要将数字转换为计算机可以以二进制形式存储的形式,这会导致轻微的精度或舍入偏差。例如,值 1.3 可以表示为 1.29999999999。可以通过一些容差进行比较。要比较任意精度的数字,可以使用big包。

操作步骤...

  1. 打开控制台并创建文件夹chapter03/recipe02

  2. 导航到该目录。

  3. 创建包含以下内容的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
        }
  1. 在终端中执行命令go run tolerance.go

  2. 您将看到以下输出:

  1. 创建包含以下内容的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)

        }
  1. 在终端中执行命令go run big.go

  2. 您将看到以下输出:

工作原理...

在不使用任何内置包的情况下进行浮点数比较的第一种方法(步骤 1-5)需要使用所谓的EPSILON常量。这是选择的足够小的增量(差异)的值,以便将两个数字视为相等。增量常数可以达到 1e-8 的数量级,这通常是足够的精度。

第二个选项更复杂,但对于进一步处理浮点数更有用。math/big包提供了可以配置为给定精度的Float类型。该包的优势在于精度可以比float64类型的精度高得多。出于说明目的,使用了较小的精度值来显示给定精度的四舍五入和比较。

请注意,当使用 16 位精度时,dadb数字相等,当使用 32 位精度时,它们不相等。最大可配置的精度可以从big.MaxPrec常量中获得。

四舍五入浮点数

将浮点数四舍五入为整数或特定精度必须正确进行。最常见的错误是将浮点类型float64转换为整数类型,并认为它已经处理好了。

一个例子可能是将数字 3.9999 转换为整数,并期望它变成值为 4 的整数。实际结果将是 3。在撰写本书时,Go 的当前版本(1.9.2)不包含Round函数。然而,在 1.10 版本中,Round函数已经在math包中实现。

如何做...

  1. 打开控制台并创建文件夹chapter03/recipe03

  2. 导航到该目录。

  3. 创建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
        }
  1. 通过在终端中运行go run round.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

将浮点数转换为整数实际上只是截断了浮点值。比如值 2 表示为 1.999999;在这种情况下,输出将是 1,这不是您期望的。

正确的浮点数四舍五入的方法是使用一个函数,该函数还会考虑小数部分。常用的四舍五入方法是向远离零的方向舍入(也称为商业舍入)。简而言之,如果数字包含小数部分的绝对值大于或等于 0.5,则将数字四舍五入,否则将向下舍入。

Round函数中,math包的Trunc函数截断了数字的小数部分。然后提取了数字的小数部分。如果值超过 0.5 的限制,那么就会加上与整数值相同的符号的 1。

Go 版本 1.10 使用了一个更快的实现,该实现在示例中提到。在 1.10 版本中,您可以直接调用math.Round函数来获得四舍五入的数字。

浮点数算术

如前面的示例所述,浮点数的表示也使算术变得复杂。对于一般目的,内置的float64上的操作已经足够。如果需要更高的精度,则需要使用math/big包。本示例将向您展示如何处理这个问题。

如何做...

  1. 打开控制台并创建文件夹chapter03/recipe04

  2. 导航到该目录。

  3. 创建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)

        }
  1. 通过在终端中运行go run main.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

big包提供了对高精度浮点数进行算术运算的支持。前面的示例说明了对数字的基本操作。请注意,代码将float64类型和big.Float类型的操作进行了比较。

通过使用高精度数字,使用big.Float类型是至关重要的。当big.Float转换回内置的float64类型时,高精度会丢失。

还有更多...

big包包含Float类型的更多操作。查看此包的文档(golang.org/pkg/math/big/#Float)以获取更多详细信息。

另请参阅

浮点数的比较和四舍五入在比较浮点数四舍五入浮点数示例中有提到。

格式化数字

如果数字转换为字符串,通常需要合理格式化。数字的格式化意味着数字以给定的数字和小数点打印出来。还可以选择值的表示。然而,与此密切相关的问题是数字格式的本地化。例如,一些语言使用逗号分隔的零。

如何做...

  1. 打开控制台并创建文件夹chapter03/recipe05

  2. 导航到目录。

  3. 创建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)

        }
  1. 在主终端中运行go run format.go来执行代码。

  2. 你将看到以下输出:

  1. 创建文件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)
        }
  1. 在主终端中运行go run localized.go来执行代码。

  2. 你将看到以下输出:

它是如何工作的...

代码示例显示了整数和浮点数的最常用选项。

Go 中的格式化源自 C 的printf函数。所谓的动词用于定义数字的格式化。例如,动词可以是%X,实际上是值的占位符。

除了基本格式化外,还有与本地习俗相关的格式化规则。根据区域设置进行格式化,包golang.org/x/text/message可以提供帮助。请参阅本食谱中的第二个代码示例。这样,可以对数字格式进行本地化。

还有更多...

有关所有格式选项,请参阅fmt包。strconv包在需要以不同基数格式化数字时也可能很有用。以下食谱描述了数字转换的可能性,但副作用是如何以不同基数格式化数字的选项。

在二进制、八进制、十进制和十六进制之间转换

在某些情况下,整数值可以用除十进制表示以外的其他表示。这些表示之间的转换很容易通过strconv包来完成。

如何做...

  1. 打开控制台并创建文件夹chapter03/recipe06

  2. 导航到目录。

  3. 创建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
        }
  1. 在主终端中运行go run convert.go来执行代码。

  2. 你将看到以下输出:

它是如何工作的...

strconv包提供了ParseIntFormatInt函数,这些函数可以说是互补的函数。函数ParseInt能够解析任何基数表示的整数。另一方面,函数FormatInt可以将整数格式化为任何给定的基数。

最后,可以将整数的字符串表示解析为内置的int64类型,然后将解析后的整数的字符串格式化为给定的基数表示。

使用正确的复数格式

在为用户显示消息时,如果句子更加人性化,交互会更加愉快。Go 包golang.org/x/text,即扩展包,包含了以正确方式格式化复数的功能。

准备工作

执行go get -x golang.org/x/text以获取扩展包,如果你还没有的话。

如何做...

  1. 打开控制台并创建文件夹chapter03/recipe07

  2. 导航到目录。

  3. 创建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()

        }
  1. 在主终端中运行go run plurals.go来执行代码。

  2. 你将看到以下输出:

它是如何工作的...

golang.org/x/text/message包含函数NewPrinter,接受语言标识并创建格式化的 I/O,与fmt包相同,但具有根据性别和复数形式翻译消息的能力。

message包的Set函数添加了翻译和复数选择。复数形式本身是根据Selectf函数设置的规则选择的。Selectf函数生成基于plural.Form或选择器的规则的catalog.Message类型。

上述示例代码使用了plural.Oneplural.Other形式,以及=x, <x选择器。这些与格式化动词%d匹配(也可以使用其他动词)。选择第一个匹配的情况。

还有更多...

有关选择器和形式的更多信息,请参阅golang.org/x/text/message包的文档。

生成随机数

本教程展示了如何生成随机数。这个功能由math/rand包提供。由math/rand生成的随机数被认为是不安全的,因为序列是可重复的,具有给定的种子。

要生成加密安全的数字,应使用crypto/rand包。这些序列是不可重复的。

如何做...

  1. 打开控制台并创建文件夹chapter03/recipe08

  2. 导航到目录。

  3. 创建具有以下内容的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()
        }
  1. 通过在主终端中运行go run rand.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

上述代码介绍了如何生成随机数的两种可能性。第一种选项使用math/rand包,这是不安全的,允许我们使用相同的种子号生成相同的序列。这种方法通常用于测试。这样做的原因是为了使序列可重现。

第二个选项,即加密安全选项,是使用crypto/rand包。API 使用Reader提供具有加密强大伪随机生成器实例。包本身具有默认的Reader,通常基于基于系统的随机数生成器。

操作复数

复数通常用于科学应用和计算。Go 将复数实现为原始类型。复数的特定操作是math/cmplx包的一部分。

如何做...

  1. 打开控制台并创建文件夹chapter03/recipe09

  2. 导航到目录。

  3. 创建具有以下内容的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)

        }
  1. 通过在主终端中运行go run complex.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

基本运算符是为原始类型complex实现的。复数的其他操作由math/cmplx包提供。如果需要高精度操作,则没有big实现。

另一方面,复数可以实现为实数,并且虚部由big.Float类型表示。

在度和弧度之间转换

三角函数运算和几何操作通常以弧度为单位进行;能够将这些转换为度数及其相反是非常有用的。本教程将向您展示如何处理这些单位之间的转换。

如何做...

  1. 打开控制台并创建文件夹chapter03/recipe10

  2. 导航到目录。

  3. 创建具有以下内容的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)
        }
  1. 通过在主终端中运行go run radians.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

Go 标准库不包含任何将弧度转换为度数及其相反的函数。但至少 Pi 常数是math包的一部分,因此可以按照示例代码中所示进行转换。

上述代码还介绍了定义具有附加方法的自定义类型的方法。这些方法通过方便的 API 简化了值的转换。

取对数

对数在科学应用以及数据可视化和测量中被使用。内置的math包包含了常用的对数基数。使用这些,你可以得到所有的基数。

操作步骤...

  1. 打开控制台并创建文件夹chapter03/recipe11

  2. 导航到目录。

  3. 创建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)
        }
  1. 在主终端中运行go run log.go来执行代码。

  2. 你将看到以下输出:

工作原理...

标准包math包含了所有常用对数的函数,因此你可以轻松地得到二进制、十进制和自然对数。查看Log函数,它通过助手定义的公式计算任何以x为底的y的对数:

标准库中对数的内部实现自然是基于近似值的。这个函数可以在$GOROOT/src/math/log.go文件中找到。

生成校验和

哈希,或者所谓的校验和,是快速比较任何内容的最简单方法。这个示例演示了如何创建文件内容的校验和。为了演示目的,将使用 MD5 哈希函数。

操作步骤...

  1. 打开控制台并创建文件夹chapter03/recipe12

  2. 导航到目录。

  3. 创建content.dat文件,内容如下:

        This is content to check
  1. 创建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))
        }
  1. 在主终端中运行go run checksum.go来执行代码。

  2. 你将看到以下输出:

  1. 创建sha_panic.go文件,内容如下:
        package main

        import (
          "crypto"
        )

        func main() {
          crypto.SHA1.New()
        }
  1. 在主终端中运行go run sha_panic.go来执行代码。

  2. 你将看到以下输出:

工作原理...

crypto包包含了众所周知的哈希函数的实现。MD5哈希函数位于crypto/md5包中。crypto包中的每个哈希函数都实现了Hash接口。注意Hash包含了Write方法。通过Write方法,它可以被用作Writer。这可以在FileMD5函数中看到。HashSum方法接受字节切片的参数,结果哈希值将放置在其中。

注意这一点。Sum方法不会计算参数的哈希值,而是将哈希计算到参数中。

另一方面,md5.Sum包函数可以直接用于生成哈希。在这种情况下,Sum函数的参数是计算出的哈希值。

自然地,crypto包实现了SHA变体和其他哈希函数。这些通常以相同的方式使用。哈希函数可以通过crypto包的常量crypto.Hash(例如,crypto.MD5.New())来访问,但是这种方式,给定函数的包也必须链接到构建的二进制文件中(可以使用空白导入,import _ "crypto/md5"),否则对New的调用将会导致恐慌。

hash包本身包含了 CRC 校验和等内容。

第四章:从前有座山

本章中的食谱有:

  • 查找今天的日期

  • 将日期格式化为字符串

  • 将字符串解析为日期

  • 将日期转换为纪元和反之亦然

  • 从日期中检索时间单位

  • 日期算术

  • 查找两个日期之间的差异

  • 在不同时区之间转换

  • 定期运行代码块

  • 等待一定时间

  • 超时长时间运行的操作

  • 序列化时间和日期

介绍

本章主要讨论与时间相关的任务和操作。Go 将所有这些集中在名为time的标准包中。使用此包,您可以获取当前时间和日期,将日期格式化为字符串,转换时区,创建定时器和创建滴答器。请记住,您可以实现和设计功能的方式总是很多,本章将只展示其中的一些方式。

验证 Go 是否正确安装。如果有任何问题,请参阅第一章中的检索 Golang 版本,并按照准备就绪部分的步骤进行操作。

查找今天的日期

获取当前日期是任何系统或应用程序的常见任务。让我们看看如何使用 Go 的标准库来完成这个任务。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe01

  2. 导航到目录。

  3. 创建名为today.go的文件,内容如下:

        package main

        import (
          "fmt"
          "time"
        )

        func main() {
          today := time.Now()
          fmt.Println(today)
        }
  1. 通过在主终端中运行go run today.go来执行代码。

  2. 您将看到以下输出:

工作原理...

内置包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输出的能力。有一些关于如何做到这一点的规则,我们将介绍一些有用的规则。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe02

  2. 导航到目录。

  3. 创建名为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")))
        }
  1. 通过在主终端中运行go run format.go来执行代码。

  2. 您将看到以下输出:

工作原理...

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实例。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe03

  2. 导航到目录。

  3. 创建包含以下内容的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)

        }
  1. 在主终端中运行go run parse.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

time包包含Parse函数,用于解析带有时间信息的字符串。

传入日期字符串的格式由参考日期给出,格式化为匹配的格式。请记住,参考时间是Jan 2 15:04:05 2006 MST

如果给定的时间字符串不包含有关时区的信息,则Parse函数的结果将始终为UTC

如果提供了时区信息,则时间始终是所提供时区的时间瞬间。

ParseInLocation函数接受第三个参数,即位置。如果时间字符串不包含任何时区信息,则时间将被解析为给定位置的Time实例。

将日期转换为时期,反之亦然

时期是描述时间点的通用系统。时期时间的开始被定义为00:00:00 1 Jan 1970 UTC。时期的值是自时间戳以来的秒数,减去那时以来的闰秒数。

time包和Time类型使您能够操作并找出 UNIX 时期时间。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe04

  2. 导航到目录。

  3. 创建包含以下内容的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)

        }
  1. 在主终端中运行go run epoch.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

time包包含Unix函数,它接受两个int64参数,即时期时间的秒数和纳秒数。这样,您可以从时期值获取Time实例。

要从Time实例获取时期值,可以调用与从时期创建Time相同名称的方法Unix。还有一个名为UnixNano的方法,它返回毫秒的计数,而不是秒。

从日期中检索时间单位

Time类型还提供了从实例中检索时间单位的 API。这意味着您可以找出实例表示的月份中的哪一天,或者一天中的哪个小时。本教程展示了如何获取这样的单位。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe05

  2. 导航到目录。

  3. 创建包含以下内容的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)

        }
  1. 在主终端中运行go run units.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

Time类型提供了提取时间单位的方法。前面的示例显示了提取星期几、月份和月份的日期。类似地,还可以提取小时、秒和其他单位。

自然地,API 直接未提供的单位需要从现有单位中派生出来。

日期算术

time包的Time类型还允许您对给定的日期和时间执行基本算术运算。这样,您可以找出过去和未来的日期。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe06

  2. 导航到目录。

  3. 创建包含以下内容的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)

        }
  1. 在主终端中运行go run arithmetics.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

time包的Time类型提供了两种操作日期和时间的基本方法。

第一种方法Add接受time.DurationAddDate。使用Add方法,您可以通过正号将时间向未来移动,并通过添加负号将时间向后移动。

第二种方法AddDate,消耗int64参数作为年、月和日,并添加更大的时间量。

请注意,AddDate会对结果进行标准化,与time.Date函数相同。标准化意味着将月份添加到 8 月 31 日将导致 10 月 1 日,因为接下来的一个月只有 30 天(9 月 31 日不存在)。

查找两个日期之间的差异

查找两个日期之间的差异并不是一项不寻常的任务。对于这个操作,Go 标准包time,分别是Time类型,提供了支持方法。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe07

  2. 导航到目录。

  3. 创建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)

        }
  1. 通过在主终端中运行go run diff.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

Time实例的Sub方法是找出两个日期之间差异的通用方法。结果是time.Duration,表示这些日期之间的纳秒计数。

请注意,如果差异超过了最大/最小time.Duration的限制,那么将返回最大或最小值。

函数SinceUntil只是计算现在和给定日期之间差异的一种更简洁的方式。它们的工作方式与它们的名称提示的一样。Since函数返回的结果与time.Now().Sub(t)相同;同样,Until返回的结果与t.Sub(time.Now())相同。

Sub方法自然也考虑了时区。因此,差异是相对于每个Time实例的位置返回的。

在不同时区之间转换

处理时区很困难。处理不同时区的一个好方法是将一个时区作为系统中的参考时区,并在需要时转换其他时区。这个配方向您展示了如何在不同时区之间进行时间转换。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe08

  2. 导航到目录。

  3. 创建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)

        }
  1. 通过在主终端中运行go run timezones.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

Time类型提供了In方法,它消耗指向time.Location的指针。返回的Time是原始的转换为给定时区的时间。请注意,Time实例被认为是不可变的,因此改变实例的方法会导致新的Time实例。

time包引用IANA 时区数据库作为位置的来源。LoadLocation函数查找ZONEINFO环境变量中的目录或 ZIP 文件。如果找不到,则在 UNIX 系统上搜索已知的安装位置。最后,它在$GOROOT/lib/time/zoneinfo.zip中查找。

定期运行代码块

除了日期和时间操作,time包还提供了对周期性和延迟代码执行的支持。通常,应用程序健康检查、活动检查或任何周期性作业都可以通过这种方式实现。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe09

  2. 导航到目录。

  3. 创建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")
       }
  1. 通过在主终端中运行go run ticker.go来执行代码。

  2. 等待几秒钟,然后按Ctrl + C发送SIGINT信号。

  3. 您将看到以下输出:

它是如何工作的...

Ticker持有C通道,用于传递周期性的滴答声。实例是根据滴答声之间的给定间隔创建的。间隔由time.Duration值定义。

打算定期执行的代码在无限循环中的 goroutine 中执行。从Ticker通道读取会阻塞循环,直到传递滴答声。

请注意,一旦调用Stop方法停止TickerC通道并不会关闭,它只是停止传递滴答声。因此,前面的代码包含了select结构,其中停止通道可以传递停止信号。这样就可以进行优雅的关闭。

等待一定时间

前面的示例描述了如何定期执行代码。本示例将向您展示如何延迟执行代码。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe10

  2. 导航到该目录。

  3. 创建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))

        }
  1. 在主终端中运行go run delay.go来执行代码。

  2. 您将看到以下输出:

工作原理是...

要执行带有一定延迟的代码,可以使用time包中的Timer。这个工作原理与前面的定期运行代码块中描述的相同。

Timer包含C通道,在给定时间后传递滴答声。之后,该通道不会再传递其他滴答声。

相同的功能由time包的AfterFunc函数提供。它只是简化了使用。请注意,这里不需要通道。示例代码使用sync.WaitGroup来等待给定的函数执行。

time.After是前面示例中的最后选择。该函数返回一个通道,在给定时间后传递滴答声。请注意TimerAfter函数之间的区别。Timer是可重用的结构(提供StopReset方法)。另一方面,After函数只能使用一次,因为它不提供任何重置选项。

超时长时间运行的操作

前面的示例描述了如何延迟执行代码的概念。相同的概念可以用来实现长时间运行操作的超时。本示例将说明如何实现这一点。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe11

  2. 导航到该目录。

  3. 创建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))
        }
  1. 在主终端中运行go run timeout.go来执行代码。

  2. 您将看到以下输出:

工作原理是...

在前面的代码中,长时间运行的操作的超时是通过time.After函数实现的,该函数提供在给定时间后传递的通道。

操作本身被包装到一个选择语句中,该语句在time.After通道和默认选项之间进行选择,执行操作。

请注意,您需要允许代码定期从time.After通道中读取,以了解超时是否已经超过。否则,如果默认的代码分支完全阻塞执行,就没有办法知道超时是否已经过去。

还有更多...

示例实现使用了time.After函数,但Timer函数也可以以相同的方式使用。内置库还使用context.WithTimeout来实现超时功能。

序列化时间和日期

在序列化日期和时间信息时,需要选择合适的格式。本示例将说明time包如何帮助选择合适的格式并正确进行序列化。

如何做...

  1. 打开控制台并创建文件夹chapter04/recipe12

  2. 导航到该目录。

  3. 创建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)

        }
  1. 在主终端中运行go run serialize.go来执行代码。

  2. 您将看到以下输出:

工作原理是...

Time函数实现了二进制、Gob 和 JSON 序列化的接口。JSON 格式被认为是非常通用的,因此展示了将值序列化为 JSON 的示例。请注意,Time函数以 RFC 3339(www.ietf.org/rfc/rfc3339.txt)的方式序列化值,该规范提出了所谓的互联网日期/时间格式。

另一种非常通用的序列化/保留时间的方法是使用纪元时间。纪元时间与时区无关,因为它是由自某一绝对时间点以来经过的秒/纳秒定义的。最后,它被表示为一个数字,因此没有理由对值进行序列化和反序列化。

第五章:进入和退出

本章包含以下教程:

  • 读取标准输入

  • 写入标准输出和错误

  • 通过名称打开文件

  • 将文件读入字符串

  • 读写不同的字符集

  • 在文件中寻找位置

  • 读写二进制数据

  • 同时写入多个写入器

  • 写入和读取之间的管道

  • 将对象序列化为二进制格式

  • 读写 ZIP 文件

  • 有效地解析大型 XML 文件

  • 从不完整的 JSON 数组中提取数据

介绍

本章将介绍典型的 I/O 操作和相关任务,以及各种输入源的写入和读取。我们将介绍 XML 处理、解压缩压缩文件以及使用随机访问文件。

检查 Go 是否已正确安装。第一章的准备就绪部分,与环境交互检索 Golang 版本教程将对您有所帮助。

读取标准输入

每个进程都拥有自己的标准输入、输出和错误文件描述符。stdin作为进程的输入。本教程描述了如何从stdin读取数据。

如何做...

  1. 打开控制台并创建文件夹chapter05/recipe01

  2. 导航到目录。

  3. 创建名为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)

       }
  1. 使用go run fmt.go执行代码。

  2. 输入John并按Enter

  3. 输入40并按Enter

  4. 您将看到以下输出:

  1. 创建名为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)
          }

        }
  1. 使用go run scanner.go执行代码。

  2. 输入Hello并按Enter

  3. 按下CTRL + C发送SIGINT

  4. 查看输出:

  1. 创建名为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))
       }
  1. 使用管道输入echo 'Go is awesome!' | go run reader.go执行代码。

  2. 查看输出:

工作原理...

Go 进程的stdin可以通过os包的Stdin获取。实际上,它是一个实现了Reader接口的File类型。从Reader读取非常容易。上述代码展示了从Stdin读取的三种常见方式。

第一个选项演示了fmt包的使用,该包提供了ScanScanfScanln函数。Scanf函数将输入读取到给定的变量中。Scanf的优点是可以确定扫描值的格式。Scan函数只是将输入读取到变量中(没有预定义的格式),而Scanln则像其名称一样,读取以换行符结束的输入。

Scanner是示例代码中显示的第二个选项,它提供了一种方便的扫描大量输入的方式。Scanner包含了Split函数,可以定义自定义的分割函数。例如,要从stdin扫描单词,可以使用bufio.ScanWords预定义的SplitFunc

通过Reader API 进行读取是最后介绍的方法。这种方法可以更好地控制输入的读取方式。

写入标准输出和错误

正如前面的教程所述,每个进程都有stdinstdoutstderr文件描述符。标准方法是使用stdout作为进程输出,stderr作为进程错误输出。由于这些是文件描述符,数据写入的目标可以是任何东西,从控制台到套接字。本教程将向您展示如何写入stdoutstderr

如何做...

  1. 打开控制台并创建文件夹chapter05/recipe02

  2. 导航到目录。

  3. 创建名为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")
         }
  1. 使用go run stdouterr.go执行代码。

  2. 查看输出:

工作原理...

与前面示例中的Stdin一样,StdoutStderr是文件描述符。因此,它们实现了Writer接口。

前面的示例展示了如何通过io.WriteString函数、Writer API 的使用以及fmt包和FprintXX函数来写入这些内容的几种方法。

通过名称打开文件

文件访问是一种非常常见的操作,用于存储或读取数据。本示例说明了如何使用标准库通过文件名和路径打开文件。

如何做...

  1. 打开控制台并创建文件夹chapter05/recipe03

  2. 导航到目录。

  3. 创建目录temp并在其中创建文件file.txt

  4. 编辑file.txt文件并将This file content写入文件。

  5. 使用以下内容创建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()

        }
  1. 文件结构应该如下所示:

  1. 使用go run openfile.go执行代码。

  2. 查看输出,temp文件夹中还应该有一个新文件test.txt

它是如何工作的...

os包提供了一种简单的打开文件的方式。函数Open通过路径打开文件,只以只读模式打开。另一个函数OpenFile更强大,需要文件路径、标志和权限。

标志常量在os包中定义,可以使用二进制 OR 运算符|组合它们。权限由os包常量(例如os.ModePerm)或数字表示法(如0777,权限为-rwxrwxrwx)设置。

将文件读取为字符串

在前面的示例中,我们看到了从Stdin读取和打开文件。在本示例中,我们将稍微结合这两者,并展示如何将文件读取为字符串。

如何做...

  1. 打开控制台并创建文件夹chapter05/recipe04

  2. 导航到目录。

  3. 创建目录temp并在其中创建文件file.txt

  4. 编辑file.txt文件并写入多行内容。

  5. 使用以下内容创建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))

        }
  1. 使用go run readfile.go执行代码。

  2. 查看输出:

它是如何工作的...

从文件中读取很简单,因为File类型实现了ReaderWriter接口。这样,所有适用于Reader接口的函数和方法都适用于File类型。前面的示例展示了如何使用Scanner读取文件并将内容写入字节缓冲区(这比字符串连接更高效)。这样,您可以控制从文件中读取的内容量。

第二种方法使用ioutil.ReadFile更简单,但应谨慎使用,因为它会读取整个文件。请记住,文件可能很大,可能会威胁应用程序的稳定性。

读取/写入不同的字符集

各种来源的输入可能以各种字符集的形式出现并不是例外。请注意,许多系统使用 Windows 操作系统,但也有其他系统。Go 默认期望程序中使用的字符串是基于 UTF-8 的。如果不是,则必须从给定的字符集解码以便能够处理该字符串。本示例将展示以 UTF-8 之外的字符集读取和写入文件。

如何做...

  1. 打开控制台并创建文件夹chapter05/recipe05

  2. 导航到目录。

  3. 使用以下内容创建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))
        }
  1. 使用go run charset.go执行代码。

  2. 查看输出:

它是如何工作的...

golang.org/x/text/encoding/charmap 包包含代表广泛使用的字符集的 Charmap 类型指针常量。Charmap 类型提供了为给定字符集创建编码器和解码器的方法。Encoder 创建编码 Writer,将写入的字节编码为所选字符集。类似地,Decoder 可以创建解码 Reader,从所选字符集解码所有读取的数据。

另请参阅

第二章,字符串和其他内容,还包含了编码/解码字符串到另一个字符集的教程从非 Unicode 字符集解码字符串

在文件中寻找位置

在某些情况下,您需要从文件的特定位置读取或写入,例如索引文件。本教程将向您展示如何在平面文件操作的上下文中使用位置寻找。

如何做...

  1. 打开控制台并创建文件夹 chapter05/recipe06

  2. 导航到目录。

  3. 创建名为 flatfile.txt 的文件,并包含以下内容:

 123.Jun.......Wong......
 12..Novak.....Jurgen....
 10..Thomas....Sohlich...
  1. 创建名为 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")
        }
  1. 使用 go run fileseek.go 执行代码。

  2. 查看输出:

  1. 以十六进制显示文件 xxd flatfile.txt

它是如何工作的...

前面的示例使用 flatfile 作为演示如何在文件中寻找、读取和写入的例子。通常,可以使用 Seek 方法来移动当前指针在 File 中的位置。它接受两个参数,即位置和如何计算位置,0 - 相对于文件原点,1 - 相对于当前位置,2 - 相对于文件末尾。这样,您可以在文件中移动光标。Seek 方法在前面代码中的 readLine 函数的实现中使用。

flatfile 是存储数据的最基本形式。记录结构具有固定长度,记录部分也是如此。示例中的平面文件结构是:ID - 4 个字符,FirstName - 10 个字符,LastName - 10 个字符。整个记录长度为 24 个字符,以换行符结束,即第 25 个字符。

os.File 还包含 ReadAt 和 WriteAt 方法。这些方法消耗要写入/读取的字节和开始的偏移量。这简化了在文件中特定位置的写入和读取。

请注意,示例假定每个符文只有一个字节,这对于特殊字符等可能并不正确。

读取和写入二进制数据

本教程描述了如何以二进制形式写入和读取任何类型。

如何做...

  1. 打开控制台并创建文件夹 chapter05/recipe07

  2. 导航到目录。

  3. 创建名为 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))
        }
  1. 通过 go run rwbinary.go 执行代码。

  2. 查看输出:

它是如何工作的...

可以使用 encoding/binary 包写入二进制数据。函数 Write 消耗应该写入数据的 Writer,字节顺序(BigEndian/LittleEndian),最后是要写入 Writer 的值。

要类似地读取二进制数据,可以使用 Read 函数。请注意,从二进制源读取数据并没有什么神奇之处。您需要确定从 Reader 中获取的数据是什么。如果不确定,数据可能会被获取到适合大小的任何类型中。

同时向多个写入器写入

当您需要将相同的输出写入多个目标时,内置包中提供了帮助。本教程展示了如何同时实现写入多个目标。

如何做...

  1. 打开控制台并创建文件夹 chapter05/recipe08

  2. 导航到目录。

  3. 创建名为 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())
        }
  1. 通过 go run multiwr.go 执行代码。

  2. 查看输出:

  1. 检查创建文件的内容:
 Hello, Go is awesome!

工作原理...

io包含MultiWriter函数,带有Writers的可变参数。当调用Writer上的Write方法时,数据将被写入所有底层的Writers

在写入器和读取器之间进行管道传输

进程之间的管道是使用第一个进程的输出作为其他进程的输入的简单方法。在 Go 中也可以使用相同的概念,例如,将数据从一个套接字传输到另一个套接字,创建隧道连接。本教程将向您展示如何使用 Go 内置库创建管道。

操作步骤如下...

  1. 打开控制台并创建文件夹chapter05/recipe09

  2. 导航到目录。

  3. 创建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)
          }

        }
  1. 通过go run pipe.go执行代码。

  2. 查看输出:

工作原理...

io.Pipe函数创建内存管道,并返回管道的两端,一端是PipeReader,另一端是PipeWriter。对PipeWriter的每次Write都会被阻塞,直到另一端的Read消耗。

该示例显示了从执行命令的输出到父程序的标准输出的管道输出。通过将pWriter分配给cmd.Stdout,子进程的标准输出被写入管道,goroutine中的io.Copy消耗写入的数据,将数据复制到os.Stdout

将对象序列化为二进制格式

除了众所周知的 JSON 和 XML 之外,Go 还提供了二进制格式gob。本教程将介绍如何使用gob包的基本概念。

操作步骤如下...

  1. 打开控制台并创建文件夹chapter05/recipe10

  2. 导航到目录。

  3. 创建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())

        }
  1. 通过go run gob.go执行代码。

  2. 查看输出:

工作原理...

gob序列化和反序列化需要编码器和解码器。gob.NewEncoder函数创建具有底层WriterEncoder。每次调用Encode方法都会将对象序列化为gob格式。gob格式本身是自描述的二进制格式。这意味着每个序列化的结构都以其描述为前缀。

要从序列化形式解码数据,必须通过调用gob.NewDecoder创建Decoder,并使用底层的Reader。然后,Decode接受应将数据反序列化到的结构的指针。

注意,gob 格式不需要源和目标类型完全匹配。有关规则,请参考encoding/gob包。

读取和写入 ZIP 文件

ZIP 压缩是一种广泛使用的压缩格式。通常使用 ZIP 格式来上传文件集或者导出压缩文件作为输出。本教程将向您展示如何使用标准库以编程方式处理 ZIP 文件。

操作步骤如下...

  1. 打开控制台并创建文件夹chapter05/recipe11

  2. 导航到目录。

  3. 创建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()
          }

        }
  1. 通过go run zip.go执行代码。

  2. 查看输出:

工作原理...

内置包zip包含NewWriterNewReader函数,用于创建zip.Writer以进行压缩,以及zip.Reader以进行解压缩。

ZIP 文件的每个记录都是使用创建的zip.WriterCreate方法创建的。然后使用返回的Writer来写入内容主体。

要解压文件,使用OpenReader函数创建 zipped 文件中记录的ReadCloser。创建的ReaderCloserFile字段是zip.File指针的切片。通过调用Open方法并读取返回的ReadCloser来获取文件的内容。

只需在Create方法的文件名中添加斜杠即可创建文件夹。例如folder/newfile.txt

有效解析大型 XML 文件

XML 是一种非常常见的数据交换格式。Go 库包含对解析 XML 文件的支持,方式与 JSON 相同。通常,使用与 XML 方案对应的结构,并借助此帮助一次解析 XML 内容。问题在于当 XML 文件太大而无法放入内存时,因此需要分块解析文件。这个示例将揭示如何处理大型 XML 文件并解析所需的信息。

如何做...

  1. 打开控制台并创建文件夹chapter05/recipe11

  2. 导航到目录。

  3. 创建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>
  1. 创建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)
        }
  1. 通过go run xml.go执行代码。

  2. 查看输出:

工作原理...

使用xml包的NewDecoder函数创建 XML 内容的Decoder

通过在Decoder上调用Token方法,接收xml.Tokenxml.Token是保存令牌类型的接口。可以根据类型定义代码的行为。示例代码测试解析的xml.StartElement是否是book元素之一。然后将数据部分解析为Book结构。这样,底层Decoder中的Reader中的指针位置将被结构数据移动,解析可以继续进行。

从不完整的 JSON 数组中提取数据

这个示例包含一个非常特定的用例,即您的程序从不可靠的来源消耗 JSON,而 JSON 包含一个具有开始标记[的对象数组,但数组中的项目数量非常大,而 JSON 的结尾可能已损坏。

如何做...

  1. 打开控制台并创建文件夹chapter05/recipe13

  2. 导航到目录。

  3. 创建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)
          }
  1. 通过go run json.go执行代码。

  2. 查看输出:

![

工作原理...

除了Unmarshall函数外,json包还包含Decoder API。使用NewDecoder可以创建Decoder。通过在解码器上调用Token方法,可以读取底层Reader并返回Token接口。这可以保存多个值。

其中之一是Delim类型,它是包含{[]}中之一的 rune。基于此,检测到 JSON 数组的开始。通过解码器上的More方法,可以检测到更多要解码的对象。

第六章:发现文件系统

本章包含以下示例:

  • 获取文件信息

  • 创建临时文件

  • 写入文件

  • 从多个 goroutine 写入文件

  • 列出目录

  • 更改文件权限

  • 创建文件和目录

  • 过滤文件列表

  • 比较两个文件

  • 解析用户主目录

介绍

本章将引导您完成文件和目录中的典型操作。我们还将介绍如何获取用户主目录并为其创建临时文件。

检查 Go 是否已正确安装。第一章的准备就绪部分中的检索 Golang 版本示例将对您有所帮助。

获取文件信息

如果您需要发现有关访问文件的基本信息,Go 的标准库提供了一种方法来完成这个任务。本示例展示了如何访问这些信息。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe01

  2. 导航到目录。

  3. 创建包含内容This is test file的示例test.file

  4. 创建包含以下内容的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())

        }
  1. 在主终端中运行go run fileinfo.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

os.File类型通过Stat方法提供对FileInfo类型的访问。FileInfo结构包含有关文件的所有基本信息。

创建临时文件

临时文件通常在运行测试用例时使用,或者如果您的应用程序需要一个存储短期内容的地方,例如用户数据上传和当前处理的数据。本示例将介绍创建此类文件或目录的最简单方法。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe02

  2. 导航到目录。

  3. 创建包含以下内容的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)

        }
  1. 在主终端中运行go run tempfile.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

ioutil包含TempFileTempDir函数。TempFile函数消耗目录和文件前缀。返回具有底层临时文件的os.File。请注意,调用者负责清理文件。前面的示例使用os.Remove函数来清理文件。

TempDir函数的工作方式相同。不同之处在于返回包含目录路径的string

临时file/dir名称由前缀和随机后缀组成。多个调用具有相同参数的TempFile/Dir函数的程序将不会获得相同的结果。

写入文件

写入文件是每个程序员的基本任务;Go 支持多种方法来完成这个任务。本示例将展示其中一些方法。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe03

  2. 导航到目录。

  3. 创建包含以下内容的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)
          }
        }
  1. 在主终端中运行go run writefile.go来执行代码。

  2. 检查创建的sample.file的内容:

它是如何工作的...

os.File类型实现了Writer接口,因此可以通过使用Writer接口的任何选项来写入文件。前面的示例使用了os.File类型的WriteString方法。通用的io.WriteString方法也可以使用。

从多个 goroutine 写入文件

本示例将向您展示如何安全地从多个 goroutine 写入文件。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe04

  2. 导航到目录。

  3. 创建包含以下内容的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()
        }
  1. 在主终端中运行go run syncwrite.go来执行代码。

  2. 检查创建的sample.file的内容:

它是如何工作的...

并发写入文件是一个可能导致文件内容不一致的问题。最好通过使用Mutex或任何其他同步原语来同步对文件的写入。这样,您可以确保一次只有一个 goroutine 能够写入文件。

上述代码创建了一个带有MutexWriter,它嵌入了Writer(在本例中是os.File),对于每个Write调用,内部锁定Mutex以提供排他性。写操作完成后,Mutex原语会自然解锁。

列出目录

这个示例将向您展示如何列出目录内容。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe05

  2. 导航到目录。

  3. 创建一个名为folder的目录。

  4. 创建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())
            }
          }
        }
  1. 在主终端中运行go run listdir.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

上面的示例中的文件夹列表使用了两种方法。第一种更简单的方法是使用listDirByReadDir函数,并利用ioutil包中的ReadDir函数。此函数返回表示实际目录内容的FileInfo结构的切片。请注意,ReadDir函数不会递归读取文件夹。实际上,ReadDir函数在内部使用os包中File类型的Readdir方法。

另一方面,更复杂的listDirByWalk使用filepath.Walk函数,该函数消耗要遍历的路径,并具有处理给定路径中的每个文件或文件夹的函数。主要区别在于Walk函数递归读取目录。这种方法的核心部分是WalkFunc类型,其函数是消耗列表的结果。请注意,该函数通过返回filepath.SkipDir错误来阻止基础文件夹上的递归调用。Walk函数还首先处理调用路径,因此您也需要处理这一点(在本例中,我们跳过打印并返回 nil,因为我们需要递归处理此文件夹)。

更改文件权限

这个示例说明了如何以编程方式更改文件权限。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe06

  2. 导航到目录。

  3. 创建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())

        }
  1. 在主终端中运行go run filechmod.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

os包中File类型的Chmod方法可用于更改文件权限。上面的示例只是创建文件并将权限更改为0777

只需注意fi.Mode()被调用两次,因为它提取了文件当前状态的权限(os.FileMode)。

更改权限的最简单方法是使用os.Chmod函数,它执行相同的操作,但您不需要在代码中获取File类型。

创建文件和目录

这个示例描述了在代码中创建文件和目录的几种一般方法。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe07

  2. 导航到目录。

  3. 创建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)
          }

        }
  1. 在主终端中运行go run create.go来执行代码。

  2. 列出chapter06/recipe07目录的内容:

它是如何工作的...

前面的示例代表了创建文件或目录的四种方法。os.Create函数是创建文件的最简单方法。使用此函数,您将以0666的权限创建文件。

如果需要使用任何其他权限配置创建文件,则应使用os包的OpenFile函数。

可以使用os包的Mkdir函数创建目录。这样,将创建具有给定权限的目录。第二个选项是使用MkdirAll函数。此函数还会创建目录,但如果给定路径包含不存在的目录,则会创建路径中的所有目录(它与 Unix 的mkdir实用程序的-p选项的工作方式相同)。

过滤文件列表

本教程向您展示了如何列出与给定模式匹配的文件路径。列表不必来自同一文件夹。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe08

  2. 导航到目录。

  3. 创建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)
            }
          }
        }
  1. 在主终端中运行go run filter.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

要获取与给定模式对应的过滤文件列表,可以使用filepath包中的Glob函数。有关模式语法,请参阅filepath.Match函数的文档(golang.org/pkg/path/filepath/#Match)。

请注意,filepath.Glob的返回结果是与匹配路径对应的字符串切片。

另请参阅

本章的列出目录教程展示了更通用的方法,其中可以使用filepath.Walk函数来列出和过滤路径。

比较两个文件

本教程为您提供了如何比较两个文件的提示。本教程将向您展示如何快速确定文件是否相同。本教程还将向您展示如何找到两者之间的差异。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe09

  2. 导航到目录。

  3. 创建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())
          }
        }
  1. 在主终端中运行go run comparison.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

可以通过几种方式来比较两个文件。本教程描述了两种基本方法。第一种方法是通过创建文件的校验和来比较整个文件。

第三章的生成校验和教程展示了如何创建文件的校验和。这种方式,getMD5SumString函数生成校验和字符串,它是 MD5 字节结果的十六进制表示。然后比较这些字符串。

第二种方法是逐行比较文件(在本例中是字符串内容)。如果行不匹配,则包括x标记。这是您可以比较二进制内容的方式,但您需要按字节块(字节切片)扫描文件。

解析用户主目录

例如,程序知道用户的主目录可能是有益的,例如,如果您需要存储自定义用户配置或与用户相关的任何其他数据。本教程将描述如何找到当前用户的主目录。

如何做...

  1. 打开控制台并创建文件夹chapter06/recipe10

  2. 导航到目录。

  3. 创建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)
        }
  1. 在主终端中运行go run home.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

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 地址。

如何做...

  1. 打开控制台并创建文件夹chapter07/recipe01

  2. 导航到目录。

  3. 创建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)
              }
            }

          }
        }
  1. 在主终端中运行go run interfaces.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

net 包包含Interfaces函数,它将网络接口列为Interface结构的切片。Interface结构具有Addrs方法,它列出可用的网络地址。这样,您可以按接口列出地址。

另一个选项是使用net包的InterfaceAddrs函数,它提供了实现Addr接口的结构体切片。这为您提供了获取所需信息的方法。

连接到远程服务器

基于 TCP 的协议是网络通信中最重要的协议。作为提醒,HTTP、FTP、SMTP 和其他协议都属于这一组。本示例让您了解如何一般连接到 TCP 服务器。

如何做...

  1. 打开控制台并创建文件夹chapter07/recipe02

  2. 导航到目录。

  3. 创建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)

       }
  1. 在主终端中运行go run tcpclient.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

net 包包含Dial函数,它消耗网络类型和地址。在前面的示例中,网络是tcp,地址是localhost:8080

一旦Dial函数成功,就会返回Conn类型,它作为已打开套接字的引用。Conn接口还定义了ReadWrite函数,因此它们可以用作写入和从套接字读取的WriterReader函数。最后,示例代码使用Scanner来获取响应。请注意,这种情况下Scanner可以工作是因为有换行符。否则,应该使用更通用的Read方法。在示例中,通过SetReadDeadline方法设置了Read截止日期。关键之处在于截止日期不是持续时间,而是Time。这意味着截止日期被设置为将来的时间点。如果您在循环中从套接字读取数据并需要将读取超时设置为 10 秒,则每次迭代都应包含类似于conn.SetReadDeadline(time.Now().Add(10*time.Second))的代码。

只是为了解释整个代码示例,使用了HTTP标准包中的 HTTP 服务器作为客户端的对应部分。这部分在另一个示例中有所涵盖。

通过 IP 地址解析域名,反之亦然

这个教程将介绍如何将 IP 地址转换为主机地址,反之亦然。

如何做到...

  1. 打开控制台并创建文件夹chapter07/recipe03

  2. 导航到目录。

  3. 创建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())
          }
        }
  1. 在主终端中运行go run lookup.go来执行代码。

  2. 你将看到以下输出:

它是如何工作的...

从 IP 地址解析域名可以使用net包中的LookupAddr函数来完成。要从域名找出 IP 地址,应用LookupIP函数。

连接到 HTTP 服务器

前面的教程连接到远程服务器让我们深入了解了如何在较低级别连接 TCP 服务器。在这个教程中,将展示如何在较高级别与 HTTP 服务器通信。

如何做到...

  1. 打开控制台并创建文件夹chapter07/recipe04

  2. 导航到目录。

  3. 创建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))
        }
  1. 在主终端中运行go run http.go来执行代码。

  2. 你将看到以下输出:

它是如何工作的...

连接到 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 的工具。这个教程将介绍其中一些主要功能。

如何做到...

  1. 打开控制台并创建文件夹chapter07/recipe05

  2. 导航到目录。

  3. 创建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))

        }
  1. 在主终端中运行go run url.go来执行代码。

  2. 你将看到以下输出:

它是如何工作的...

net/url包旨在帮助您操作和解析 URL。URL结构包含了组合 URL 所需的字段。通过URL结构的String方法,可以轻松地将其转换为简单的字符串。

当字符串表示可用且需要额外操作时,可以利用net/urlParse函数。这样,字符串可以转换为URL结构,并且可以修改底层 URL。

创建一个 HTTP 请求

这个教程将向您展示如何使用特定参数构造 HTTP 请求。

如何做到...

  1. 打开控制台并创建文件夹chapter07/recipe06

  2. 导航到目录。

  3. 创建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))

        }
  1. 在主终端中运行go run request.go来执行代码。

  2. 你将看到以下输出:

它是如何工作的...

构造请求的更复杂的方式在示例代码中呈现。使用net/http包的NewRequest方法返回Request结构的指针。该函数消耗方法的请求、URL 和请求的主体。注意表单的构建方式。使用url.Values结构而不是使用普通字符串。最后,调用Encode方法对给定的表单值进行编码。通过请求的http.Header属性设置头。

读取和写入 HTTP 头

前面的示例描述了如何一般创建 HTTP 请求。本示例将详细介绍如何读取和写入请求头。

如何做...

  1. 打开控制台并创建文件夹chapter07/recipe07

  2. 导航到目录。

  3. 创建包含以下内容的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)

        }
  1. 在主终端中运行go run headers.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

http包中的头实际上表示为map[string][]string,因此必须处理Header类型。前面的代码显示了如何设置和读取头值。关于头的重要事情是头键的值是string切片。因此,头中的每个键可以包含多个值。

Header类型的Set方法设置给定键下的单项切片。另一方面,Add方法将值附加到切片。

使用Get方法将从给定键下的切片中检索第一个值。如果需要整个切片,则需要将Header处理为映射。可以使用Del方法删除整个头键。

服务器和客户端都使用http包的RequestHeader类型,因此在服务器端和客户端端的处理方式相同。

处理 HTTP 重定向

在某些情况下,您需要更多控制重定向的处理方式。本示例将向您展示 Go 客户端实现的机制,以便您更多地控制处理 HTTP 重定向。

如何做...

  1. 打开控制台并创建文件夹chapter07/recipe08

  2. 导航到目录。

  3. 创建包含以下内容的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)
          }
        }
  1. 在主终端中运行go run redirects.go来执行代码。

  2. 您将看到以下输出:

它是如何工作...

http包的Client包含CheckRedirect字段。该字段是一个具有reqvia参数的函数。req是即将到来的请求,via指的是以前的请求。这样,您可以在重定向后修改请求。在前面的示例中,修改了Known-redirects头。

如果CheckRedirect函数返回错误,则返回带有包装错误的关闭主体的最后响应。如果返回http.ErrUseLastResponse,则返回最后的响应,但主体未关闭,因此可以读取它。

默认情况下,CheckRedirect属性为 nil。在这种情况下,它最多有 10 次重定向。超过此计数后,重定向将停止。

消费 RESTful API

RESTful API 是应用程序和服务器提供其服务访问的最常见方式。本示例将向您展示如何使用标准库中的 HTTP 客户端来消费它。

如何做...

  1. 打开控制台并创建文件夹chapter07/recipe09

  2. 导航到目录。

  3. 创建包含以下内容的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,
         }
       }
  1. 在主终端中运行go run rest.go来执行代码。

  2. 您将看到以下输出:

它是如何工作的...

前面的示例代码显示了 REST API 的样子以及如何使用它。请注意,decodeCitydecodeCities函数受益于请求的Body实现了Reader接口。结构的反序列化通过json.Decoder完成。

发送简单的电子邮件

本教程将简要介绍如何使用标准库连接到 SMTP 服务器并发送电子邮件。

准备工作

在本教程中,我们将使用谷歌 Gmail 账户发送电子邮件。通过一些配置,本教程也适用于其他 SMTP 服务器。

如何做...

  1. 打开控制台并创建文件夹chapter07/recipe10

  2. 导航到目录。

  3. 创建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)
          }

        }
  1. 在主终端中运行go run smtp.go来执行代码。

  2. 输入账户的电子邮件(谷歌账户)并按Enter

  3. 输入账户的密码并按Enter

  4. 在检查电子邮箱之前,您将看到以下输出:

工作原理...

smtp包提供了与 SMTP 服务器交互的基本功能。Dial函数提供客户端。客户端最重要的方法是Mail,用于设置发件人邮件,Rcpt,用于设置收件人邮件,以及Data,提供Writer,用于写入邮件内容。最后,Quit方法发送 QUIT 并关闭与服务器的连接。

前面的示例使用了安全连接到 SMTP 服务器,因此客户端的Auth方法用于设置身份验证,并调用StartTLS方法以启动与服务器的安全连接。

请注意,Auth结构是通过smtp包的PlainAuth函数单独创建的。

调用 JSON-RPC 服务

本教程将说明如何使用标准库调用 JSON-RPC 协议的过程。

如何做...

  1. 打开控制台并创建文件夹chapter07/recipe11

  2. 导航到目录。

  3. 创建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))
          }
        }
  1. 在主终端中运行go run jsonrpc.go来执行代码。

  2. 您将看到以下输出:

工作原理...

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的数据库

如何做...

  1. 打开控制台并创建文件夹chapter08/recipe01

  2. 导航到目录。

  3. 使用以下内容创建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")
       }
  1. 通过go run connect.go执行代码。

  2. 查看输出:

工作原理...

标准库包database/sql提供了Open函数,用于使用驱动程序名称和连接详细信息(在本例中为连接 URL)初始化与数据库的连接。请注意,Open函数不会立即创建连接,可能只会验证传递给函数的参数。

可以通过返回的DB结构指针中可用的Ping方法验证与数据库的连接。

驱动程序本身在driver包的init函数中初始化。驱动程序通过sql包的Register函数向驱动程序名称注册自身。github.com/lib/pq驱动程序将自身注册为postgres

验证连接

驱动程序实现中的数据库连接可能被池化,并且可能从池中拉出的连接已经断开。本配方将展示如何验证连接是否仍然有效。

准备就绪

通过在终端中调用go version命令验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备就绪部分进行操作。

如何做...

  1. 打开控制台并创建文件夹chapter08/recipe02

  2. 导航到目录。

  3. 使用以下内容创建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.")

        }
  1. 通过go run verify.go执行代码。

  2. 查看输出:

工作原理...

如前一篇中提到的连接数据库Open函数可能只是验证连接细节,但不一定立即连接数据库。实际连接到数据库通常是延迟加载的,并且是通过对数据库的第一次语句执行创建的。

DB结构的指针提供了Ping方法,通常对数据库进行幂等调用。Ping方法的变体是PingContext,它只是添加了取消或超时数据库调用的能力。请注意,如果Ping函数失败,连接将从数据库池中移除。

DB结构的指针还提供了Conn方法,用于从数据库池中检索连接。通过使用连接,您实际上保证使用相同的数据库会话。同样,DB结构的指针包含PingContext方法,Conn指针提供了PingContext方法来检查连接是否仍然活动。

执行语句

在以前的示例中,我们已经学习了如何连接和验证与数据库的连接。本示例将描述如何执行针对数据库的语句。

准备工作

通过在终端中调用go version命令来验证 Go 是否已正确安装。如果命令失败,请按照本章第一篇中的准备工作部分进行操作。

按照本章第一篇中的说明设置 PostgreSQL 服务器。

如何做...

  1. 对您的示例数据库运行以下 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;
  1. 打开控制台并创建文件夹chapter08/recipe03

  2. 导航到目录。

  3. 创建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
        }
  1. 通过go run statement.go执行代码。

  2. 查看输出:

工作原理...

通常,我们可以执行两种类型的语句。对于第一种类型的语句,我们不期望任何行作为结果,最终我们得到的是没有输出或者只是受影响的行数。这种类型的语句通过DB结构指针上的Exec方法执行。在前面的示例代码中,我们有TRUNCATEINSERT语句。但是这种方式也可以执行 DDL 和 DCL 语句。

有四种主要的语句类别:

  • DDL数据定义语言):此语言允许您创建和修改数据库模式

  • DML数据建模语言):此语言帮助您修改数据

  • DCL数据控制语言):此语言定义了对对象的访问控制

  • TCL事务控制语言):此语言控制事务。

第二种类型是我们期望以行的形式得到结果的语句;这些通常被称为查询。这种类型的语句通常通过QueryQueryContext方法执行。

使用准备好的语句

准备好的语句带来了安全性、效率和便利性。当然,可以使用它们与 Go 标准库一起使用;本示例将展示如何使用。

准备工作

通过在终端中调用go version命令来验证 Go 是否已正确安装。如果命令失败,请按照本章第一篇中的准备工作部分进行操作。

按照本章第一篇中的说明设置 PostgreSQL 服务器。

如何做...

  1. 对您的示例数据库运行以下 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;
  1. 打开控制台并创建文件夹chapter08/recipe04

  2. 导航到目录。

  3. 创建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
        }
  1. 通过go run prepared.go执行代码。

  2. 查看输出:

工作原理...

要创建准备好的语句,需要调用指向DB结构的Prepare方法。之后,使用给定的参数调用Stmt指针上的ExecQuery方法。

准备好的语句是在DB指针的范围内创建的,但是在连接池中的特定连接上。语句记住了使用过的连接,并且在调用时尝试使用相同的连接。如果连接忙或已关闭,则重新创建准备好的语句并在新连接上调用语句。

如果在打开的事务*Tx中使用准备好的语句,则情况会发生变化,在这种情况下,准备好的语句绑定到与事务相关的一个连接。

请注意,事务中准备的语句不能与 DB 指针一起使用,反之亦然。

通常,准备好的语句的工作方式是在数据库端创建语句。数据库返回准备好的语句的标识符。准备好的语句在以下调用期间执行,并且只提供语句的参数。

取消挂起的查询

在某些情况下,您需要取消长时间运行的语句以限制资源的消耗,或者仅当结果不相关或语句运行时间过长时。自 Go 1.8 以来,取消查询是可能的。本配方解释了如何使用此功能。

准备工作

通过在终端中调用go version命令验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。

按照本章第一个配方中提到的方式设置 PostgreSQL 服务器。

操作步骤...

  1. 对您的示例数据库运行以下 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');
  1. 打开控制台并创建文件夹chapter08/recipe05

  2. 导航到目录。

  3. 使用以下内容创建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
        }
  1. 通过go run cancelable.go执行代码。

  2. 查看输出:

工作原理...

database/sql包提供了取消挂起语句的可能性。DB结构指针的所有名为XXXContext的方法都会消耗上下文,并且可以取消挂起的语句。

只有在驱动程序支持Context变体时才能取消语句。如果不支持,将执行不带Context的变体。

使用Context变体和context.WithTimeout,您可以创建语句调用的超时。

请注意,示例代码执行以错误pq: canceling statement due to user request结束,这与调用查询后立即调用的CancelFunc相对应。

读取查询结果元数据

除了数据本身,查询结果还包含与结果集相关的元数据。这包含有关列名、类型和数据的其他信息。本配方将解释如何检索数据。

准备工作

通过在终端中调用go version命令验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。

按照本章第一个配方中提到的方式设置 PostgreSQL 服务器。

操作步骤...

  1. 对您的示例数据库运行以下 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');

  1. 打开控制台并创建文件夹chapter08/recipe06

  2. 导航到目录。

  3. 使用以下内容创建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
        }
  1. 通过go run metadata.go执行代码。

  2. 查看输出:

工作原理...

DB结构指针的QueryQueryContext方法会导致Rows结构指针。Rows指针提供ColumnsColumnTypes方法,其中包含有关返回结果集结构的信息。

Columns方法返回带有列名的字符串切片。

ColumnTypes方法返回ColumnType指针的切片,其中包含有关返回结果集的更丰富信息。上述代码打印出了ColumnType指针公开的详细信息。

从查询结果中检索数据

在与数据库交互时,基本部分是通过执行查询来提取数据。本配方将说明使用标准库database/sql包时如何执行此操作。

准备工作

验证 Go 是否已正确安装,通过在终端中调用go version命令。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。

按照本章第一个配方中的说明设置 PostgreSQL 服务器。

操作步骤...

  1. 对样本数据库运行以下 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');
  1. 打开控制台并创建文件夹chapter08/recipe07

  2. 导航到目录。

  3. 创建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
        }
  1. 通过go run data.go执行代码。

  2. 查看输出:

工作原理...

来自指向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 服务器。

操作步骤...

  1. 对样本数据库运行以下 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');
  1. 打开控制台并创建文件夹chapter08/recipe08

  2. 导航到目录。

  3. 创建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
        }
  1. 通过go run querymap.go执行代码。

  2. 查看输出:

工作原理...

请注意,上述代码表示了两种方法。parseWithRawBytes函数使用了首选方法,但它高度依赖于驱动程序的实现。它的工作方式是创建与结果中列数相同长度的RawBytes切片。因为Scan函数需要值的指针,所以我们需要创建指向RawBytes切片(字节切片的切片)的指针切片,然后将其传递给Scan函数。

提取成功后,我们只需重新映射值。在示例代码中,我们将其转换为string,因为如果RawBytes是目标,驱动程序使用string类型来存储值。请注意,存储值的形式取决于驱动程序的实现。

第二种方法parseToMap在第一种方法不起作用的情况下是可用的。它几乎使用相同的方法,但值的切片被定义为空接口的切片。这种方法依赖于驱动程序。驱动程序应确定要分配给值指针的默认类型。

处理事务

事务控制是在处理数据库时需要牢记的常见事情。本配方将向您展示如何使用sql包处理事务。

准备工作

通过在终端中调用go version命令来验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。

设置 PostgreSQL 服务器,如本章第一个配方中所述。

如何做...

  1. 对您的示例数据库运行以下 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');
  1. 打开控制台并创建文件夹chapter08/recipe09

  2. 导航到目录。

  3. 创建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
        }
  1. 通过go run transaction.go执行代码。

  2. 看输出:

工作原理...

正如前面的代码所示,事务处理非常简单。DB结构指针的Begin方法创建具有默认隔离级别的事务(取决于驱动程序)。事务本质上保留在单个连接上,并由返回的Tx结构指针表示。

指针Tx实现了DB结构指针可用的所有方法;唯一的例外是所有操作都在事务中完成(如果数据库能够在事务中处理语句)。通过在Tx结构指针上调用RollbackCommit方法结束事务。在此调用之后,事务结束,其他操作将以错误ErrTxDone结束。

DB结构指针上还有一个有用的方法叫做BeginTx,它创建了事务Tx结构指针,同时也增强了给定的上下文。如果上下文被取消,事务将被回滚(进一步的Commit调用将导致错误)。BeginTx还消耗了TxOptions指针,这是可选的,可以定义隔离级别。

执行存储过程和函数

处理存储过程和函数总是比通常的语句更复杂,特别是如果过程包含自定义类型。标准库提供了处理这些的 API,但存储过程调用的支持程度取决于驱动程序的实现。本配方将展示一个非常简单的函数/过程调用。

准备工作

通过在终端中调用go version命令来验证 Go 是否已正确安装。如果命令失败,请按照本章第一个配方中的准备工作部分进行操作。

设置 PostgreSQL 服务器,如本章第一个配方中所述。

如何做...

  1. 对您的示例数据库运行以下 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;
  1. 打开控制台并创建文件夹chapter08/recipe10

  2. 导航到目录。

  3. 创建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
        }
  1. 通过go run procedure.go执行代码。

  2. 看输出:

工作原理...

存储过程的调用高度依赖于驱动程序和数据库。请注意,在 PostgreSQL 数据库上检索结果与查询表非常相似。调用DB结构指针的QueryQueryRow方法,可以解析出结果行或行指针以获取值。

如果需要调用存储过程,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 版本配方将有所帮助。

确保端口80807070没有被其他应用程序使用。

创建 TCP 服务器

连接网络章节中,介绍了 TCP 连接的客户端部分。在本配方中,将描述服务器端。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe01

  2. 导航到该目录。

  3. 创建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()
          }
        }
  1. 通过go run servertcp.go执行代码:

  1. 打开另一个终端并执行nc localhost 8080

  2. 写入任何文本,例如Hello

  3. 查看输出:

工作原理...

可以使用net包创建 TCP 服务器。net 包包含Listen函数,用于创建TCPListener,可以Accept客户端连接。Accept方法调用TCPListener上的方法,直到接收到客户端连接。如果客户端连接成功,Accept方法会返回TCPConn连接。TCPConn是连接到客户端的连接,用于读取和写入数据。

TCPConn实现了ReaderWriter接口。可以使用所有写入和读取数据的方法。请注意,读取数据时有一个分隔符字符,否则,如果客户端强制关闭连接,则会收到 EOF。

请注意,此实现一次只能处理一个客户端。

创建 UDP 服务器

用户数据报协议(UDP)是互联网的基本协议之一。本篇将向您展示如何监听 UDP 数据包并读取内容。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe02

  2. 导航到该目录。

  3. 创建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())
            }
          }
        }
  1. 通过go run serverudp.go启动服务器:

  1. 打开另一个终端并执行nc -u localhost 7070

  2. 在终端中写入任何消息,例如Hello,然后按Enter

  3. 查看输出:

工作原理...

与 TCP 服务器一样,可以使用net包创建 UDP 服务器。使用ListenPacket函数创建PacketConn

PacketConn不像TCPConn那样实现ReaderWriter接口。要读取接收到的数据包,应该使用ReadFrom方法。ReadFrom方法会阻塞,直到接收到数据包。然后返回客户端的Addr(记住 UDP 不是基于连接的)。要响应客户端,可以使用PacketConnWriteTo方法;这会消耗消息和Addr,在这种情况下是客户端的Addr

处理多个客户端

前面的配方展示了如何创建 UDP 和 TCP 服务器。示例代码尚未准备好同时处理多个客户端。在本配方中,我们将介绍如何同时处理更多客户端。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe03

  2. 导航到该目录。

  3. 创建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())
            }
          }

        }
  1. 通过go run multipletcp.go执行代码。

  2. 打开另外两个终端并执行nc localhost 8080

  3. 在两个打开的终端中写入一些内容并查看输出。以下两个图像是连接的客户端。

    • 终端 1 连接到localhost:8080

    • 终端 2 连接到localhost:8080

服务器运行的终端中的输出:

工作原理...

TCP 服务器的实现与本章的前一个配方创建 TCP 服务器相同。实现已增强,具有同时处理多个客户端的能力。请注意,我们现在在单独的goroutine中处理接受的连接。这意味着服务器可以继续使用Accept方法接受客户端连接。

因为 UDP 协议不是有状态的,也不保持任何连接,所以处理多个客户端的工作被移动到应用程序逻辑中,您需要识别客户端和数据包序列。只有向客户端写入响应才能使用 goroutines 并行化。

创建 HTTP 服务器

在 Go 中创建 HTTP 服务器非常容易,标准库提供了更多的方法来实现。让我们看看最基本的方法。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe04

  2. 导航到目录。

  3. 创建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()
        }
  1. 通过go run httpserver.go执行代码。

  2. 查看输出:

  1. 在浏览器中访问 URL http://localhost:8080,或使用curl。应该显示Hello world内容:

工作原理...

net/http包包含了几种创建 HTTP 服务器的方法。最简单的方法是实现net/http包中的Handler接口。Handler接口要求类型实现ServeHTTP方法。这个方法处理请求和响应。

服务器本身以net/http包中的Server结构的形式创建。Server结构需要HandlerAddr字段。通过调用ListenAndServe方法,服务器开始在给定地址上提供内容。

如果使用ServerServe方法,则必须提供Listener

net/http包还提供了默认服务器,如果从net/http包中调用ListenAndServe作为函数,则可以使用。它消耗HandlerAddr,与Server结构相同。在内部,创建了Server

处理 HTTP 请求

应用程序通常使用 URL 路径和 HTTP 方法来定义应用程序的行为。本配方将说明如何利用标准库来处理不同的 URL 和方法。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe05

  2. 导航到目录。

  3. 创建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)

        }
  1. 通过go run handle.go执行代码。

  2. 在浏览器中或通过curl检查以下 URL:

  • http://localhost:8080/user

  • http://localhost:8080/items/clothes

  • http://localhost:8080/admin/ports

  1. 查看输出:

工作原理...

net/http包包含ServeMux结构,该结构实现了Handler接口,可用于Server结构,但还包含了如何定义不同路径处理的机制。ServeMux指针包含HandleFuncHandle方法,接受路径,HandlerFunc函数处理给定路径的请求,或者另一个处理程序执行相同的操作。

参见前面的示例,了解如何使用这些。Handler接口和HandlerFunc需要实现带有请求和响应参数的函数。这样你就可以访问这两个结构。请求本身可以访问Headers、HTTP 方法和其他请求参数。

创建 HTTP 中间件层

具有 Web UI 或 REST API 的现代应用程序通常使用中间件机制来记录活动或保护给定接口的安全性。在本示例中,将介绍实现这种中间件层。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe06

  2. 导航到目录。

  3. 创建具有以下内容的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
          }

        }
  1. 通过go run middleware.go执行代码。

  2. 使用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

  1. 查看输出:

  1. 使用X-User头测试 URLhttp://localhost:8080/api/profile

  2. 查看输出:

工作原理...

在前面的示例中,中间件的实现利用了 Golang 的函数作为一等公民功能。原始的HandlerFunc被包装成检查X-Auth头的HandlerFunc。然后使用Secure函数来保护HandlerFunc,并在ServeMuxHandleFunc方法中使用。

请注意,这只是一个简单的示例,但是您可以实现更复杂的解决方案。例如,用户身份可以从Header令牌中提取,随后可以定义新类型的处理程序,如type AuthHandler func(u *User,w http.ResponseWriter, r *http.Request)。然后,WithUser函数为ServeMux创建HandlerFunc

提供静态文件

几乎任何 Web 应用程序都需要提供静态文件。使用标准库可以轻松实现 JavaScript 文件、静态 HTML 页面或 CSS 样式表的提供。本示例将展示如何实现。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe07

  2. 导航到目录。

  3. 创建具有以下内容的文件welcome.txt

        Hi, Go is awesome!
  1. 创建文件夹html,导航到该文件夹并创建具有以下内容的文件page.html
        <html>
          <body>
            Hi, I'm HTML body for index.html!
          </body>
        </html>
  1. 创建具有以下内容的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")
        }
  1. 通过go run static.go执行代码。

  2. 使用浏览器或curl实用程序检查以下 URL:

  • http://localhost:8080/html/page.html

  • http://localhost:8080/welcome

  1. 查看输出:

工作原理...

net/http包提供了ServeFileFileServer函数,用于提供静态文件。ServeFile函数只消耗给定文件路径参数的ResponseWriterRequest,并将文件内容写入响应。

FileServer函数创建整个消耗FileSystem参数的Handler。前面的示例使用了Dir类型,它实现了FileSystem接口。FileSystem接口需要实现Open方法,该方法消耗字符串并返回给定路径的实际File

使用模板生成的内容

对于某些目的,不需要使用所有 JavaScript 创建高度动态的 Web UI,生成内容的静态内容可能已经足够。Go 标准库提供了一种构建动态生成内容的方法。本示例将引导您进入 Go 标准库模板化。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe08

  2. 导航到目录。

  3. 创建具有以下内容的文件template.tpl

        <html>
          <body>
            Hi, I'm HTML body for index.html!
          </body>
        </html>
  1. 创建文件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)
        }
  1. 通过go run dynamic.go执行代码。

  2. 检查 URL http://localhost:8080并查看输出:

工作原理...

Go 标准库还包含用于模板化内容的包。html/templatetext/template包提供了解析模板和使用它们创建输出的函数。解析是使用ParseXXX函数或新创建的Template结构指针的方法完成的。前面的示例使用了html/template包的ParseFiles函数。

模板本身是基于文本的文档或包含动态变量的文本片段。模板的使用基于将模板文本与包含模板中的变量值的结构进行合并。为了将模板与这些结构进行合并,有ExecuteExecuteTemplate方法。请注意,这些方法使用写入器接口,其中写入输出;在这种情况下使用ResponseWriter

模板语法和特性在文档中有很好的解释。

处理重定向

重定向是告诉客户端内容已经移动或需要在其他地方完成请求的常用方式。本教程描述了如何使用标准库实现重定向。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe09

  2. 导航到目录。

  3. 创建文件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)
          }
        }
  1. 通过go run redirect.go执行代码。

  2. 使用curl -v -L http://localhost:8080/s

ecured/handle以查看重定向是否有效:

工作原理...

net/http包中包含了执行重定向的简单方法。可以利用RedirectHandler。该函数接受请求将被重定向的URL和将发送给客户端的状态码。该函数本身将结果发送给Handler,可以在ServeMuxHandle方法中使用(示例直接使用包中的默认方法)。

第二种方法是使用Redirect函数,它可以为您执行重定向。该函数接受ResponseWriter、请求指针和与RequestHandler相同的 URL 和状态码,这些将发送给客户端。

重定向也可以通过手动设置Location头并编写适当的状态码来完成。Go 库使开发人员能够轻松使用这一功能。

处理 cookies

Cookies 提供了一种在客户端方便地存储数据的方式。本教程演示了如何使用标准库设置、检索和删除 cookies。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe10

  2. 导航到目录。

  3. 创建文件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)
          }
        }
  1. 通过go run cookies.go执行代码。

  2. 按照以下顺序访问 URL 并查看:

    • 在浏览器中访问 URL http://localhost:8080/set的响应:

    • 在浏览器中访问 URL http://localhost:8080/get的响应(响应包含可用的 cookies):

    • 在浏览器中访问 URL http://localhost:8080/remove的响应(这将删除 cookie):

    • 在浏览器中访问 URL http://localhost:8080/get的响应(证明 cookie X-Cookie已被移除):

工作原理...

net/http包还提供了操作 cookie 的函数和机制。示例代码介绍了如何设置/获取和删除 cookie。SetCookie函数接受代表 cookie 的Cookie结构指针,自然也接受ResponseWriterNameValueDomain和过期时间直接在Cookie结构中设置。在幕后,SetCookie函数写入头文件以设置 cookie。

可以从Request结构中检索 cookie 值。具有名称参数的Cookie方法返回指向Cookie的指针,如果请求中存在 cookie。

要列出请求中的所有 cookie,可以调用Cookies方法。此方法返回Cookie结构指针的切片。

为了让客户端知道应该删除 cookie,可以检索具有给定名称的Cookie,并将MaxAge字段设置为负值。请注意,这不是 Go 的特性,而是客户端应该工作的方式。

优雅关闭 HTTP 服务器

在第一章中,与环境交互,介绍了实现优雅关闭的机制。在这个示例中,我们将描述如何关闭 HTTP 服务器并给予它处理现有客户端的时间。

操作步骤...

  1. 打开控制台并创建文件夹chapter09/recipe11

  2. 导航到目录。

  3. 创建名为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")
        }
  1. 通过go run gracefully.go执行代码。

  2. 等待服务器开始监听:

  1. 使用浏览器连接到http://localhost:8080;这将导致浏览器等待 10 秒钟的响应。

  2. 在 10 秒的间隔内,按下Ctrl + C发送SIGINT信号。

  3. 尝试从另一个标签页重新连接(服务器应该拒绝其他连接)。

  4. 在终端中查看输出:

工作原理...

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文件。

操作步骤...

  1. 打开控制台并创建文件夹chapter09/recipe12

  2. 导航到目录。

  3. 将创建的server.keyserver.crt文件放入其中。

  4. 创建名为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)
            }
          }
  1. 通过go run servetls.go执行服务器。

  2. 访问 URL https://localhost:8080(使用 HTTPS 协议)。如果使用curl实用程序,则必须使用--insecure标志,因为我们的证书是自签名的,不受信任:

工作原理...

除了net/http包中的ListenAndServe函数之外,还存在用于通过 SSL/TLS 提供 HTTP 服务的 TLS 变体。通过ServerListenAndServeTLS方法,可以提供安全的 HTTP 服务。ListenAndServeTLS需要私钥和 X-509 证书的路径。当然,也可以直接使用net/http包中的ListenAndServeTLS函数。

解析表单变量

HTTP 的POST表单是向服务器传递信息的一种常见方式,以结构化的方式。这个示例展示了如何在服务器端解析和访问这些信息。

如何做...

  1. 打开控制台,创建文件夹chapter09/recipe12

  2. 导航到目录。

  3. 创建名为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)
          }
        }
  1. 通过go run form.go执行代码。

  2. 打开第二个终端,使用curl执行POST

 curl -X POST -H "Content-Type: app
lication/x-www-form-urlencoded" -d "param1=data1&param2=data2" "localhost:8080?
param1=overriden&param3=data3"
  1. 在运行服务器的第一个终端中查看输出:

工作原理...

net/http包的Request结构包含Form字段,其中包含了POST表单变量和 URL 查询变量的合并。在前面的代码中,重要的一步是在Request指针上调用ParseForm方法。这个方法调用会将POST表单值和查询值解析为一个Form变量。请注意,如果在Form字段上使用Get方法,则会优先考虑参数的POST值。FormPostForm字段实际上都是url.Values类型。

如果只需要访问POST表单中的参数,可以使用RequestPostForm字段。这个字段只保留了POST主体中的参数。

第十章:并发乐趣

本章包含以下教程:

  • 使用 Mutex 同步对资源的访问

  • 为并发访问创建 map

  • 只运行一次代码块

  • 在多个 goroutines 之间池化资源

  • 使用 WaitGroup 同步 goroutines

  • 从多个来源获取最快的结果

  • 使用 errgroup 传播错误

介绍

并发行为的编程总是很困难的。Go 具有非常好的机制来管理并发,如通道。除了通道作为同步机制外,Go 标准库还提供了处理更传统核心方式的并发部分的包。本章描述了如何利用 sync 包来实现常见的同步任务。最后一个教程将展示如何简化一组 goroutines 的错误传播。

检查 Go 是否已正确安装。第一章检索 Golang 版本教程中的准备就绪部分将对你有所帮助。

确保端口80807070没有被其他应用程序使用。

使用 Mutex 同步对资源的访问

如果代码使用并发访问被认为对并发使用不安全的任何资源,就需要实现同步机制来保护访问。除了使用通道,还可以利用互斥锁来实现这一目的。这个教程将向你展示如何做到这一点。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe01

  2. 导航到目录。

  3. 创建文件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)
          }

        }
  1. 通过go run mutex.go执行代码。

  2. 查看输出:

它是如何工作的...

同步原语Mutexsync包提供。Mutex作为一个锁,用于保护部分或资源。一旦goroutineMutex上调用Lock并且Mutex处于未锁定状态,Mutex就会被锁定,goroutine就可以独占地访问临界区。如果Mutex处于锁定状态,goroutine调用Lock方法。这个goroutine会被阻塞,需要等待Mutex再次解锁。

请注意,在示例中,我们使用Mutex来同步对切片原语的访问,这被认为是不安全的并发使用。

重要的事实是Mutex在第一次使用后不能被复制。

为并发访问创建 map

在 Golang 中,map 原语应被视为不安全的并发访问。在上一个教程中,我们描述了如何使用 Mutex 同步对资源的访问,这也可以用于对 map 原语的访问。但是 Go 标准库还提供了专为并发访问设计的 map 结构。这个教程将说明如何使用它。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe02

  2. 导航到目录。

  3. 创建文件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
          })

        }
  1. 通过go run map.go执行代码。

  2. 查看输出:

它是如何工作的...

sync包中包含了Map结构,该结构被设计用于从多个 Go 例程中并发使用。Map结构及其方法模仿了 map 原语的行为。Store方法相当于m[key] = val语句。Load方法相当于val, ok := m[key]Range方法提供了遍历 map 的能力。请注意,Range函数与Map的当前状态一起工作,因此如果在运行Range方法期间更改了值,则会反映这些更改,但前提是该键尚未被访问。Range函数只会访问其键一次。

只运行一次代码块

在多个 goroutine 运行相同代码的情况下,例如,有一个初始化共享资源的代码块,Go 标准库提供了解决方案,将在下文中描述。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe03

  2. 导航到目录。

  3. 创建文件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()
        }
  1. 使用go run once.go执行代码。

  2. 查看输出:

工作原理...

示例代码说明了在访问容器结构时数据的延迟加载。由于数据只应加载一次,因此在Pop方法中使用了sync包中的Once结构。Once只实现了一个名为Do的方法,该方法消耗了一个无参数的func,并且该函数在每个Once实例的执行期间只执行一次。

Do方法调用会阻塞,直到第一次运行完成。这一事实与Once旨在用于初始化的事实相对应。

在多个 goroutine 之间池化资源

资源池是提高性能和节省资源的传统方式。通常,值得使用昂贵初始化的资源进行池化。Go 标准库提供了用于资源池的骨架结构,被认为对多个 goroutine 访问是安全的。本示例描述了如何使用它。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe04

  2. 导航到目录。

  3. 创建文件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()
        }
  1. 使用go run pool.go执行代码。

  2. 查看输出:

工作原理...

sync包包含了用于池化资源的结构。Pool结构具有GetPut方法,用于检索资源并将其放回池中。Pool结构被认为对并发访问是安全的。

在创建Pool结构时,需要设置New字段。New字段是一个无参数函数,应该返回指向池化项目的指针。如果需要初始化池中的新对象,则会调用此函数。

从前面示例的日志中可以看出,Worker在返回到池中时被重用。重要的事实是,不应该对Get检索的项目和Put方法返回的项目做任何假设(比如我刚刚把三个对象放到池中,所以至少会有三个可用)。这主要是因为Pool中的空闲项目可能随时被自动删除。

如果资源初始化很昂贵,资源池化通常是值得的。然而,资源的管理也带来了一些额外的成本。

使用 WaitGroup 同步 goroutine

在处理并发运行的代码分支时,程序在某个时刻需要等待并发运行的代码部分。本示例介绍了如何使用WaitGroup等待运行的 goroutine。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe05

  2. 导航到目录。

  3. 创建文件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.")
        }
  1. 使用go run syncgroup.go执行代码。

  2. 查看输出:

工作原理...

通过sync包中的WaitGroup结构,程序可以等待有限数量的 goroutine 完成运行。WaitGroup结构实现了Add方法,用于添加要等待的 goroutine 数量。然后在 goroutine 完成后,应调用Done方法来减少要等待的 goroutine 数量。Wait方法被调用时会阻塞,直到完成给定数量的Done调用(通常在goroutine结束时)。WaitGroup应该与sync包中的所有同步原语一样使用。在创建对象后,结构不应被复制。

从多个来源获取最快的结果

在某些情况下,例如,在整合来自多个来源的信息检索时,您只需要第一个结果,最快的结果,其他结果在那之后就不相关了。现实世界中的一个例子可能是提取货币汇率以计算价格。您有多个第三方服务,因为您需要尽快显示价格,所以只需要从任何服务接收到的第一个汇率。本教程将展示如何实现这种行为的模式。

如何做...

  1. 打开控制台并创建文件夹 chapter10/recipe06

  2. 导航到目录。

  3. 创建文件 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
        }
  1. 通过 go run first.go 执行代码。

  2. 查看输出:

它是如何工作的...

上述代码提出了执行多个任务并输出一些结果的解决方案,我们只需要最快的一个。解决方案使用 Context 和取消函数来在获得第一个结果后调用取消。SearchSrc 结构提供了 Search 方法,该方法会导致写入结果的通道。请注意,Search 方法使用 time.Sleep 函数模拟延迟。对于来自 Search 方法的每个通道,合并函数触发写入最终输出通道的 goroutine,该通道在 main 方法中读取。从 merge 函数产生的输出通道接收到第一个结果时,将调用存储在变量 cancel 中的 CancelFunc 来取消其余处理。

请注意,Search 方法仍然需要结束,即使其结果不会被处理;因此,需要处理以避免 goroutine 和通道泄漏。

使用 errgroup 传播错误

本教程将展示如何轻松使用 errgroup 扩展包来检测 goroutine 组中运行子任务的错误。

如何做...

  1. 打开控制台并创建文件夹 chapter10/recipe07

  2. 导航到目录。

  3. 创建文件 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())
          }

        }
  1. 通过 go run lines.go 执行代码。

  2. 查看输出:

它是如何工作的...

golang.org/x/sync/errgroup 包有助于简化 goroutine 组的错误传播和上下文取消。Group 包含消耗无参数函数返回 error 的 Go 方法。此函数应包含应由执行的 goroutine 完成的任务。errgroupGroupWait 方法等待直到 Go 方法中执行的所有任务完成,如果其中任何一个返回 err,则返回第一个非空错误。这样,就可以简单地从运行的 goroutine 组中传播错误。

请注意,Group 也是使用上下文创建的。Context 用作取消其他任务的机制,如果发生错误。在 goroutine 函数返回 error 后,内部实现会取消上下文,因此正在运行的任务也可能会被取消。

第十一章:提示和技巧

本章将涵盖以下示例:

  • 日志定制

  • 测试代码

  • 对代码进行基准测试

  • 创建子测试

  • 测试 HTTP 处理程序

  • 通过反射访问标签

  • 对切片进行排序

  • 将 HTTP 处理程序分成组

  • 利用 HTTP/2 服务器推送

介绍

这最后一章添加了一些与测试、设计应用程序接口以及利用sortreflect包相关的附加示例。

检查 Go 是否已正确安装。第一章准备就绪部分的检索 Golang 版本示例,与环境交互将帮助您。

确保端口8080未被其他应用程序使用。

日志定制

除了使用log包中的默认记录器进行记录外,标准库还提供了一种根据应用程序或包的需求创建自定义记录器的方法。本示例将简要介绍如何创建自定义记录器。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe01

  2. 导航到目录。

  3. 创建名为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")

        }
  1. 通过go run logging.go执行代码。

  2. 查看输出:

它是如何工作的...

log包提供了New函数,简化了自定义记录器的创建。New函数接受Writer作为参数,该参数可以是实现Writer接口的任何对象,以及以字符串形式的前缀和由标志组成的日志消息的形式。最后一个参数是最有趣的,因为通过它,您可以使用动态字段增强日志消息,例如日期和文件名。

请注意,前面的示例中,第一个记录器custLogger配置了在日志消息前显示日期和时间的标志。第二个记录器custLoggerEnh使用标志LdateLshortfile来显示文件名和日期。

测试代码

测试和基准测试自然属于软件开发。作为一种现代语言,Go 支持从头开始进行这些操作。在这个示例中,将描述测试的基础知识。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe02

  2. 导航到目录。

  3. 创建名为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)
        }
  1. 通过go test -v执行测试。

  2. 在终端中查看输出:

它是如何工作的...

标准库的testing包提供了对代码测试需求的支持。test函数需要满足名称模式TestXXX。默认情况下,测试工具会查找名为xxx_test.go的文件。请注意,每个测试函数都需要接受T指针参数,该参数提供了用于测试控制的有用方法。通过T结构指针,可以设置测试的状态。例如,FailFailNow方法会导致测试失败。借助T结构指针的帮助,可以通过调用SkipSkipfSkipNow来跳过测试。

T指针的有趣方法是Helper方法。通过调用Helper方法,当前函数被标记为辅助函数,如果在该函数内调用FailNowFatal),则测试输出将指向测试中调用该函数的代码行,如前面示例代码中所示。

请注意,如果测试工具未以详细模式运行(使用-v标志),或者特定测试失败(仅适用于T测试),则Log方法(及其变体)将不可见。尝试在不使用-v标志的情况下运行此示例代码。

另请参阅

  • 以下示例涵盖了基准测试的基础知识

  • 有关测试包的更详细描述,请参阅golang.org/pkg/testing中测试包的丰富文档。

对代码进行基准测试

上一个示例介绍了测试包的测试部分,在本示例中将介绍基准测试的基础知识。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe03

  2. 导航到目录。

  3. 创建名为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
        }
  1. 通过go test -bench=执行基准测试。

  2. 在终端中查看输出:

它是如何工作的...

除了纯测试支持外,测试包还提供了用于测量代码性能的机制。为此,使用B结构指针作为参数,并且测试文件中的基准测试函数命名为BenchmarkXXXX

基准测试函数的关键部分是操作定时器和使用循环迭代计数器N

如您所见,定时器通过Reset/Start/StopTimer方法进行操作。通过这些方法,基准测试的结果会受到影响。请注意,定时器在基准测试函数开始时开始运行,而ResetTimer函数只是重新启动它。

BN字段是测量循环中的迭代次数。N值设置为足够高的值,以可靠地测量基准测试的结果。基准测试日志中显示迭代次数和每次迭代的测量时间。

另请参阅

  • 下一个示例将展示如何在测试中创建子测试

  • 有关基准测试的更多选项和信息,请查看此处的包文档:golang.org/pkg/testing

创建子测试

在某些情况下,有用的是创建一组可能具有类似设置或清理代码的测试。这可以在没有为每个测试创建单独函数的情况下完成。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe04

  2. 导航到目录。

  3. 创建名为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()
              }
            })
          }
        }
  1. 通过go test -v执行测试。

  2. 在终端中查看输出:

它是如何工作的...

testing包的T结构还提供了Run方法,可用于运行嵌套测试。Run方法需要子测试的名称和将要执行的测试函数。例如,使用表驱动测试时,这种方法可能很有益。代码示例只是使用int值的简单切片作为输入。

基准测试结构B也包含相同的方法Run,可以提供一种创建复杂基准测试后续步骤的方法。

另请参阅

在包文档中仍有很多内容要找出,golang.org/pkg/testing

测试 HTTP 处理程序

测试HTTP服务器可能会很复杂。Go 标准库通过一个方便的包net/http/httptest简化了这一点。本示例描述了如何利用此包来测试HTTP处理程序。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe05

  2. 导航到目录。

  3. 创建名为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")
          }
        }
  1. 通过go test执行测试。

  2. 在终端中查看输出:

它是如何工作的...

对于HandlerHandlerFunc的测试,可以利用net/http/httptest。该包提供了ResponseRecorder结构,能够记录响应内容并将其提供回来以断言值。用于组装请求的是net/http包的NewRequest函数。

net/http/httptest包还包含了在本地主机上监听系统选择端口的 HTTP 服务器版本。此实现旨在用于端到端测试。

通过反射访问标签

Go 语言允许给结构化字段打标签,附加额外信息。这些信息通常用作编码器的附加信息,或者对结构体进行任何类型的额外处理。这个示例将向你展示如何访问这些信息。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe06

  2. 导航到目录。

  3. 创建文件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)
          }
        }
  1. 通过go run structtags.go执行代码。

  2. 在终端中查看输出:

它是如何工作的...

可以使用reflect包提取struct标签。通过调用TypeOf,我们得到了Person的指针Type,随后通过调用Elem,我们得到了指针指向的值的Type

结果的Type让我们可以访问struct类型Person及其字段。通过遍历字段并调用Field方法检索字段,我们可以获得StructFieldStructField类型包含Tag字段,该字段提供对struct标签的访问。然后,StructTag字段上的Get方法返回特定的标签。

对切片进行排序

数据排序是一个非常常见的任务。Go 标准库通过 sort 包简化了排序。这个示例简要介绍了如何使用它。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe07

  2. 导航到目录。

  3. 创建文件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)

        }
  1. 通过go run sort.go执行代码。

  2. 在终端中查看输出:

它是如何工作的...

示例代码展示了如何舒适地使用sort包对切片进行排序的两种方式。第一种方法更加临时,它使用了sort包的Slice函数。Slice函数消耗要排序的切片和所谓的 less 函数,该函数定义了元素i是否应该在元素j之前排序。

第二种方法需要更多的代码和提前规划。它利用了sort包的Interface接口。该接口充当数据的代表,并要求其在排序数据上实现必要的方法:Len(定义数据的数量)、Less(less 函数)、Swap(调用以交换元素)。如果数据值实现了这个接口,那么可以使用sort包的Sort函数。

原始类型切片float64intstringsort包中有涵盖。因此,可以使用现有的实现。例如,要对字符串切片进行排序,可以调用Strings函数。

将 HTTP 处理程序分组

这个示例提供了关于如何将 HTTP 处理程序分离成模块的建议。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe08

  2. 导航到目录。

  3. 创建文件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
         }
  1. 通过go run handlegroups.go执行代码。

  2. 查看输出:

  1. 访问浏览器 URLhttp://localhost:8080/api/users,输出应该如下所示:

  1. 同样,您可以测试http://localhost:8080/ui/users

它是如何工作的...

为了将处理程序分离成模块,代码使用了ServeMux来为每个模块(restui)进行处理。给定模块的 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文件。

操作步骤...

  1. 打开控制台并创建文件夹chapter11/recipe09

  2. 导航到目录。

  3. 创建文件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)
           }

         }
  1. 通过go run push.go启动服务器。

  2. 打开浏览器,在 URL https://localhost:8080 中打开开发者工具(查看Push作为app.css的发起者):

工作原理...

首先,注意 HTTP/2 需要安全连接。服务器推送非常简单实现。自 Go 1.8 以来,HTTP 包提供了Pusher接口,可以在资源被请求之前用于Push资产。如果客户端(通常是浏览器)支持 HTTP/2 协议并且与服务器的握手成功,HandlerHandlerFunc中的ResponseWriter可以转换为PusherPusher只提供Push方法。Push方法消耗目标(可以是绝对路径或绝对 URL)到资源和PushOptions,可以提供额外选项(默认情况下可以使用 nil)。

在上面的示例中,查看浏览器中开发者工具的输出。推送的资源在 Initiator 列中具有值Push

标签:...,func,err,秘籍,fmt,标准,go,Go,main
From: https://www.cnblogs.com/apachecn/p/18172881

相关文章

  • Go-编程学习手册(全)
    Go编程学习手册(全)原文:zh.annas-archive.org/md5/5FC2C8948F5CEA11C4D0D293DBBCA039译者:飞龙协议:CCBY-NC-SA4.0前言Go是一种开源编程语言,让程序员可以轻松构建可靠且可扩展的程序。它通过提供简单的语法来实现这一点,使得使用并发习语和强大的标准库编写正确且可预测的代......
  • Go-和安全(全)
    Go和安全(全)原文:zh.annas-archive.org/md5/7656FC72AAECE258C02033B14E33EA12译者:飞龙协议:CCBY-NC-SA4.0前言本书涵盖了Go编程语言,并解释了如何将其应用于网络安全行业。所涵盖的主题对于红队和蓝队都很有用,也适用于希望编写安全代码的开发人员,以及希望保护其网络、主......
  • Go-Web-开发学习手册(全)
    GoWeb开发学习手册(全)原文:zh.annas-archive.org/md5/2756E08144D91329B3B7569E0C2831DA译者:飞龙协议:CCBY-NC-SA4.0前言感谢您购买本书。我们希望通过本书中的示例和项目,您能从GoWeb开发新手变成一个能够承担面向生产的严肃项目的人。因此,本书在相对较高的水平上涉及......
  • Go-编程实用手册(全)
    Go编程实用手册(全)原文:zh.annas-archive.org/md5/62FC08F1461495F0676A88A03EA0ECBA译者:飞龙协议:CCBY-NC-SA4.0前言本书将通过解决开发人员常见的问题来帮助您学习Go编程语言。您将首先安装Go二进制文件,并熟悉开发应用程序所需的工具。然后,您将操作字符串,并将它们用......
  • 【转载】Godot-GDExtension C++ 环境搭建 (Docker+MinGW/跨平台)
    本文原链接见 Godot-GDExtensionC++环境搭建(Docker+MinGW/跨平台)|Convexwf'sKirakiraBlog。Godot在4.X之后推出了GDExtension,通过第三方绑定扩展功能,目前官方支持的语言只有C++。通过使用GDExtensionC++编写扩展插件,可以作为库文件在Godot中交互使用。GDExten......
  • Django - 模型与数据库
    目录模型定义与数据迁移模型定义数据迁移模型定义与数据迁移模型定义ORM框架是一种程序技术,用于实现面向对面变成语言中不同类型系统的数据之间的转换。#index\model.pyfromdjango.dbimportmodels#Createyourmodelshere.classPersonInfo(models.Model):id......
  • Docker Build - ERROR: RUN go mod tidy
     =>ERROR[build13/14]RUNgomodtidy29.3s------>[build13/14]RUNgomodtidy:0.270go:findingmoduleforpackagegithub.......
  • 深入学习和理解Django视图层:处理请求与响应
    title:深入学习和理解Django视图层:处理请求与响应date:2024/5/417:47:55updated:2024/5/417:47:55categories:后端开发tags:Django请求处理响应生成模板渲染表单处理中间件异常处理第一章:Django框架概述1.1什么是Django?Django是一个高级的PythonWeb......
  • Golang:go-humanize将文件大小转换成Kb、Mb、Gb适合人类阅读的单位
    Golang:go-humanize将文件大小转换成Kb、Mb、Gb适合人类阅读的单位原创 吃个大西瓜 CodingBigTree 2024-05-0408:30 云南​最近去了昆明的教场中路体验了满屏蓝花楹,感受到了梦幻般的世界,随手拍了一张图,分享给大家,有时间可以去一趟,体验一次,顺便说一下,美女很多喔 ......
  • 2024-05-04:用go语言,给定一个起始索引为0的字符串s和一个整数k。 要进行分割操作,直到字
    2024-05-04:用go语言,给定一个起始索引为0的字符串s和一个整数k。要进行分割操作,直到字符串s为空:选择s的最长前缀,该前缀最多包含k个不同字符;删除该前缀,递增分割计数。如果有剩余字符,它们保持原来的顺序。在操作之前,可以修改字符串s中的一个字符为另一个小写英文字母。在最佳情......