CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • Go性能调优 说明
  • 1. Go语言性能导论
  • 2 数据结构与算法
  • 3 理解并发
  • 4 Go语言中的等效标准模板库算法
  • 5 Go语言中的矩阵和向量计算
  • 6 编写易读的Go代码
  • 7 Go语言中的模板编程
    • 理解Go generate命令
      • Protobuf的代码生成
      • Protobuf代码的运行结果
    • 链接工具链
    • 介绍用于配置编程的Cobra和Viper
      • Cobra/Viper的运行结果
    • 文本模板
    • HTML模板
    • 探索Sprig
      • 字符串函数
      • 字符串切片函数
      • 默认函数
    • 总结
  • 8 Go语言中的内存管理
  • 9 Go语言中的GPU并行化
  • 10 Go语言中的编译时评估
  • 11. 构建和部署Go代码
  • 12 Go代码性能分析
  • 13 跟踪Go代码
  • 14 集群与作业队列
  • 15 跨版本比较代码质量
目录

7 Go语言中的模板编程

# 7 Go语言中的模板编程

Go语言中的模板编程允许终端用户编写Go模板,以生成、操作和运行Go程序。Go语言具有清晰的静态依赖关系,这有助于进行元编程。Go语言中的模板编程,包括生成的二进制文件、命令行界面(CLI,Command-Line Interface )工具和模板库,都是该语言的核心要素,有助于我们编写可维护、可扩展且高性能的Go代码。

在本章中,我们将涵盖以下主题:

  • Go generate命令
  • Protobuf代码生成
  • 链接工具链
  • 使用Cobra和Viper进行配置元编程
  • 文本和HTML模板
  • 用于Go模板的Sprig库

所有这些主题都将帮助你更快、更高效地编写Go代码。在下一节中,我们将讨论Go generate命令,以及它在Go编程语言中的用途。

# 理解Go generate命令

从Go 1.4版本开始,该语言包含了一个有助于代码生成的工具,名为Go generate。Go generate会扫描源代码,查找要运行的通用命令。它独立于go build命令运行,因此必须在构建代码之前执行。Go generate由代码作者运行,而不是由编译后的二进制文件的用户运行。这个工具的运行方式与通常使用Makefile和Shell脚本的方式类似,但它是Go工具的一部分,我们无需引入任何其他依赖项。

Go generate会在代码库中搜索符合以下模式的行://go:generate command argument。

为了表明代码是生成的,生成的源文件应包含如下一行内容: ^// Code generated .* DO NOT EDIT\.$

Go generate在运行生成器时会使用一组变量:

  • $GOARCH:执行平台的架构
  • $GOOS:执行平台的操作系统
  • $GOFILE:文件名
  • $GOLINE:包含该指令的源文件的行号
  • $GOPACKAGE:包含该指令的文件所在的包名
  • $DOLLAR:字面值$

在Go语言中,我们可以将Go generate命令用于各种不同的用例。它们可以被视为Go语言内置的构建机制。虽然使用Go generate执行的操作也可以通过其他构建工具(如Makefile)来完成,但有了Go generate,就意味着在构建环境中无需其他依赖项。

这意味着所有的构建产物都包含在Go文件中,以确保项目之间的一致性。

# Protobuf的代码生成

在Go语言中,代码生成的一个实际用例是使用gRPC生成Protocol Buffers(一种结构化数据序列化方法)。Protocol Buffers是一种用于序列化结构化数据的新方法,通常用于在分布式系统中的服务之间传递数据,因为它比JSON或XML更高效。Protocol Buffers还可以在多个平台上的多种语言中扩展使用。它们带有结构化数据定义;一旦构建好了数据结构,就会生成可以读取和写入数据源的源代码。

首先,我们需要获取Protocol Buffers的最新版本:https://github.com/protocolbuffers/protobuf/releases。

在撰写本文时,该软件的稳定版本是3.8.0。安装此软件包后,我们需要使用go get github.com/golang/protobuf/protoc-gen-go命令拉取所需的Go依赖项。接下来,我们可以生成一个非常通用的协议定义:

syntax = "proto3";

package userinfo;

service UserInfo {
  rpc PrintUserInfo (UserInfoRequest) returns (UserInfoResponse) {}
}

message UserInfoRequest {
  string user = 1;
  string email = 2;
}

message UserInfoResponse {
  string response = 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

之后,我们可以使用Go generate生成proto文件。在与.proto文件相同的目录中创建一个文件,其内容如下:

package userinfo
//go:generate protoc -I ../userinfo --go_out=plugins=grpc:../userinfo ../userinfo/userinfo.proto
1
2

这样,我们仅通过使用Go generate就可以生成一个Protobuf定义。在该目录中执行Go generate后,我们会得到一个userinfo.pb.go文件,其中包含所有Go格式的Protobuf定义。在使用gRPC生成客户端和服务器架构时,我们可以使用这些信息。

接下来,我们可以创建一个服务器,以使用之前添加的gRPC定义:

package main

import (
  "context"
  "log"
  "net"
  pb "github.com/HighPerformanceWithGo/7-metaprogramming-in-go/grpcExample/userinfo/userinfo"
  "google.golang.org/grpc"
)

type userInfoServer struct{}

func (s *userInfoServer) PrintUserInfo(ctx context.Context, in *pb.UserInfoRequest) (*pb.UserInfoResponse, error) {
  log.Printf("%s %s", in.User, in.Email)
  return &pb.UserInfoResponse{Response: "User Info: User Name: " + in.User + " User Email: " + in.Email}, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在初始化服务器结构体并拥有一个返回用户信息的函数后,我们可以设置gRPC服务器,使其在标准端口上监听并注册我们的服务器:

func main() {
  l, err := net.Listen("tcp", ":50051")
  if err != nil {
    log.Fatalf("Failed to listen %v", err)
  }
  s := grpc.NewServer()
  pb.RegisterUserInfoServer(s, &userInfoServer{})
  if err := s.Serve(l); err != nil {
    log.Fatalf("Couldn't create Server: %v", err)
  }
}
1
2
3
4
5
6
7
8
9
10
11

设置好服务器定义后,我们可以将重点放在客户端上。客户端包含所有常规导入,以及几个默认常量声明,如下所示:

package main

import (
  "context"
  "log"
  "time"
  pb "github.com/HighPerformanceWithGo/7-metaprogramming-in-go/grpcExample/userinfo/userinfo"
  "google.golang.org/grpc"
)

const (
  defaultGrpcAddress = "localhost:50051"
  defaultUser = "Gopher"
  defaultEmail = "Gopher@example.com"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

设置好导入和常量后,我们可以在主函数中使用它们,将这些值发送到服务器。我们设置一个默认超时时间为1秒的上下文,发起一个PrintUserInfo的Protobuf请求,获取响应并记录下来。以下是我们的Protobuf示例:

func main() {
  conn, err := grpc.Dial(defaultGrpcAddress, grpc.WithInsecure())
  if err != nil {
    log.Fatalf("did not connect: %v", err)
  }
  defer conn.Close()
  c := pb.NewUserInfoClient(conn)
  user := defaultUser
  email := defaultEmail
  ctx, cancel := context.WithTimeout(context.Background(), time.Second)
  defer cancel()
  r, err := c.PrintUserInfo(ctx, &pb.UserInfoRequest{User: user, Email: email})
  if err != nil {
    log.Fatalf("could not greet: %v", err)
  }
  log.Printf("%s", r.Response)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

我们可以看到这个Protobuf示例在实际运行。Protobuf是在分布式系统中发送消息的强大方式。谷歌经常提到Protobuf对其大规模系统的稳定性有多重要。我们将在下一节讨论Protobuf代码的运行结果。

# Protobuf代码的运行结果

有了协议定义、服务器和客户端后,我们可以一起执行它们,查看实际效果。首先,启动服务器:

接下来,执行客户端代码。我们可以看到在客户端代码中创建的默认用户名和电子邮件地址:

在服务器端,我们可以看到所做请求的日志:

gRPC是一种非常高效的协议:它使用HTTP/2和Protocol Buffers来快速序列化数据。客户端到服务器的单个连接可以用于多个调用,从而减少延迟并提高吞吐量。

在下一节中,我们将讨论链接工具链。

# 链接工具链

Go语言的链接工具中包含许多实用工具,允许我们将相关数据传递给可执行函数。使用这个工具,程序员能够为具有特定名称和值对的字符串设置值。在Go语言中,使用cmd/link包可以在链接时将信息传递给当前的Go程序。从工具链向可执行文件传递信息的方法是使用构建参数:

go build -ldflags '-X importpath.name=value'
1

例如,如果我们想从命令行获取程序的序列号,可以这样做:

package main

import (
  "fmt"
)

var SerialNumber = "unlicensed"

func main() {
  if SerialNumber == "ABC123" {
    fmt.Println("Valid Serial Number!")
  } else {
    fmt.Println("Invalid Serial Number")
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如上述输出所示,如果我们在不传入序列号的情况下尝试执行此程序,程序会提示我们序列号无效:

如果传入错误的序列号,我们会得到相同的结果:

如果传入正确的序列号,程序会提示我们的序列号有效:

在排查大型代码库问题时,能够在链接时向程序传递数据会很有用。当你需要部署已编译的二进制文件,但之后可能需要以非确定性的方式更新某个常用值时,这也很有帮助。

在下一节中,我们将讨论两个常用于配置编程的工具——Cobra和Viper。

# 介绍用于配置编程的Cobra和Viper

两个常用的Go语言库,即spf13/cobra和spf13/viper,用于配置编程。这两个库一起使用,可以创建具有许多可配置选项的命令行界面(CLI)二进制文件。Cobra可用于生成应用程序和命令文件,而Viper有助于为遵循12要素原则的Go应用程序读取和维护完整的配置解决方案。Cobra和Viper在一些最常用的Go项目中都有应用,包括Kubernetes和Docker。

要将这两个库一起用于创建一个命令行工具库,我们需要确保按如下方式嵌套项目目录:

创建好嵌套目录结构后,我们就可以开始设置主程序了。在main.go文件中,我们定义了date命令。为了便于调用cmd目录中编写的函数(这是Go语言的常见习惯用法),Cobra和Viper的main.go函数特意写得很简单。主包代码如下:

package main

import (
    "fmt"
    "os"
    "github.com/HighPerformanceWithGo/7-metaprogramming-in-go/clitooling/cmd"
)

func main() {
    if err := cmd.DateCommand.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

定义好主函数后,我们可以开始设置命令行工具的其余部分。首先导入所需的包:

package cmd

import (
    "fmt"
    "time"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var verbose bool
1
2
3
4
5
6
7
8
9
10

接下来,设置根date命令:

var DateCommand = &cobra.Command{
    Use: "date",
    Aliases: []string{"time"},
    Short: "Return the current date",
    Long: "Returns the current date in a YYYY-MM-DD HH:MM:SS format",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Current Date :\t", time.Now().Format("2006.01.02 15:04:05"))
        if viper.GetBool("verbose") {
            fmt.Println("Author :\t", viper.GetString("author"))
            fmt.Println("Version :\t", viper.GetString("version"))
        }
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13

设置好这个命令后,我们还可以设置一个子命令来显示许可信息,如以下代码示例所示。子命令是CLI工具的第二个参数,用于为CLI提供更多信息:

var LicenseCommand = &cobra.Command{
    Use: "license",
    Short: "Print the License",
    Long: "Print the License of this Command",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("License: Apache-2.0")
    },
}
1
2
3
4
5
6
7
8

最后,设置init()函数。在Go语言中,init()函数有多种用途:

  • 向用户显示初始信息
  • 初始化变量声明
  • 初始化与外部的连接(数据库连接池或消息代理初始化)

我们可以利用新学到的init()函数知识,在代码的最后部分初始化之前定义的Viper和Cobra命令:

func init() {
    DateCommand.AddCommand(LicenseCommand)
    viper.SetDefault("Author", "bob")
    viper.SetDefault("Version", "0.0.1")
    viper.SetDefault("license", "Apache-2.0")

    DateCommand.PersistentFlags().BoolP("verbose", "v", false, "Date Command Verbose")
    DateCommand.PersistentFlags().StringP("author", "a", "bob", "Date Command Author")
    viper.BindPFlag("author", DateCommand.PersistentFlags().Lookup("author"))
    viper.BindPFlag("verbose", DateCommand.PersistentFlags().Lookup("verbose"))
}
1
2
3
4
5
6
7
8
9
10
11

上述代码片段展示了Viper中常用的一些默认、持久和绑定标志。

# Cobra/Viper的运行结果

现在,我们已经实例化了所有功能,可以实际运行新代码了。

如果在调用新的main.go时不传入任何可选参数,只会看到最初在DateCommand运行块中定义的日期输出,如下代码输出所示:

如果在输入中添加其他标志,我们可以获取详细信息,并使用命令行标志更改包的作者,如下所示:

我们还可以通过添加为许可信息创建的子命令作为参数来查看它,如下所示:

我们已经了解了spf13 Cobra和Viper包的一小部分功能,但理解它们的基本原理很重要,它们用于在Go语言中构建可扩展的CLI工具。在下一节中,我们将讨论文本模板(text templating)。

# 文本模板

Go语言有一个内置的模板语言text/template,它可以结合数据实现模板,并生成基于文本的输出。我们使用结构体来定义想要在模板中使用的数据。与Go语言中的所有内容一样,输入文本定义为UTF - 8编码,可以以任何格式传入。我们使用双花括号{{}}来表示要对数据执行的操作。用.表示的游标,可用于向模板中添加数据。这些特性相结合,形成了一种强大的模板语言,使我们能够在许多代码中复用模板。

首先,初始化包,导入必要的依赖项,并定义要传入模板的数据结构体:

package main

import (
    "fmt"
    "os"
    "text/template"
)

func main() {
    type ToField struct {
        Date      string
        Name      string
        Email     string
        InOffice  bool
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

现在,我们可以使用前面提到的text/template定义来设置模板和输入结构:

const note = `
{{/* we can trim whitespace with a {- or a -} respectively */}}
Date: {{- .Date}}
To: {{- .Email | printf "%s"}}
{{.Name}},
{{if .InOffice }}
Thank you for your input yesterday at our meeting.  We are going to go ahead with what you've suggested.
{{- else }}
We were able to get results in our meeting yesterday.  I've emailed them to you.  Enjoy the rest of your time Out of Office!
{{- end}}
Thanks,
Bob
`

var tofield = []ToField{
    {"07-19-2019", "Mx. Boss", "boss@example.com", true},
    {"07-19-2019", "Mx. Coworker", "coworker@example.com", false},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

最后,执行模板并打印输出。我们的示例输出到标准输出,但也可以打印到文件、写入缓冲区或自动发送电子邮件:

t := template.Must(template.New("Email Body").Parse(note))
for _, k := range tofield {
    err := t.Execute(os.Stdout, k)
    if err != nil {
        fmt.Print(err)
    }
}
1
2
3
4
5
6
7

利用Go语言的文本模板系统,我们可以复用这些模板来生成高质量且一致的内容。由于有新的输入,我们可以调整模板并相应地得出结果。在下一节中,我们将讨论HTML模板。

# HTML模板

在Go语言中,我们还可以使用HTML模板,其方式类似于文本模板,用于为HTML页面生成动态结果。为此,我们需要初始化包,导入适当的依赖项,并设置一个数据结构来保存计划在HTML模板中使用的值,如下所示:

package main

import (
    "html/template"
    "net/http"
)

type UserFields struct {
    Name  string
    URL   string
    Email string
}
1
2
3
4
5
6
7
8
9
10
11
12

接下来,创建userResponse HTML模板:

var userResponse = `
<html>
<head></head>
<body>
<h1>Hello {{.Name}}</h1>
<p>You visited {{.URL}}</p>
<p>Hope you're enjoying this book!</p>
<p>We have your email recorded as {{.Email}}</p>
</body>
</html>
`
1
2
3
4
5
6
7
8
9
10
11

然后,创建一个HTTP请求处理程序:

func rootHandler(w http.ResponseWriter, r *http.Request) {
    requestedURL := string(r.URL.Path)
    userfields := UserFields{"Bob", requestedURL, "bob@example.com"}
    t := template.Must(template.New("HTML Body").Parse(userResponse))
    t.Execute(w, userfields)
    log.Printf("User " + userfields.Name + " Visited : " + requestedURL)
}
1
2
3
4
5
6
7

之后,初始化HTTP服务器:

func main() {
    s := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/", rootHandler)
    s.ListenAndServe()
}
1
2
3
4
5
6
7

然后,使用go run htmlTemplate.go调用我们的Web服务器。当在这个域名上请求页面时,会看到以下结果:

上述输出来自我们HTML模板的代码。这个示例可以进一步扩展,例如通过X-Forwarded-For头解析传入的IP地址请求,根据用户代理字符串获取终端用户的浏览器信息,或者使用任何其他特定的请求参数,以便为客户端提供更丰富的响应。在下一节中,我们将讨论Sprig,一个用于Go模板函数的库。

# 探索Sprig

Sprig是一个用于定义Go模板函数的库。该库包含许多扩展Go模板语言功能的函数。Sprig库遵循一些原则,这些原则有助于确定哪些函数可用于增强模板:

  • 仅支持简单数学运算
  • 仅处理传递给模板的数据,从不从外部源检索数据
  • 利用模板库中的函数构建最终布局
  • 从不覆盖Go核心模板功能

在以下小节中,我们将更深入地了解Sprig的功能。

# 字符串函数

Sprig有一组字符串函数,能够在模板中操作字符串。

在我们的示例中,将处理字符串" - bob smith"(注意其中的空格和短横线)。我们将进行以下操作:

  • 使用trim()函数去除空白字符
  • 将单词smith替换为strecansky
  • 去除-前缀
  • 将字符串转换为标题格式,即从bob strecansky转换为Bob Strecansky
  • 将字符串重复10次
  • 创建一个宽度为14个字符(我的名字的长度)的自动换行,并使用换行符分隔每一行

Sprig库可以在一行代码中完成这些操作,类似于bash shell中函数的管道操作。

首先,初始化包并导入必要的依赖项:

package main

import (
    "fmt"
    "os"
    "text/template"
    "github.com/Masterminds/sprig"
)
1
2
3
4
5
6
7
8

接下来,将字符串映射设置为接口类型,执行转换,并将模板渲染到标准输出:

func main() {
    inStr := map[string]interface{}{"Name": " - bob smith"}
    transform := `{{.Name | trim | replace "smith" "strecansky" | trimPrefix "-" | title | repeat 10 | wrapWith 14 "\n"}}`
    functionMap := sprig.TxtFuncMap()
    t := template.Must(template.New("Name Transformation").Funcs(functionMap).Parse(transform))
    err := t.Execute(os.Stdout, inStr)
    if err != nil {
        fmt.Printf("Couldn't create template: %s", err)
        return
    }
}
1
2
3
4
5
6
7
8
9
10
11

执行程序后,会看到字符串操作按预期进行:

像在示例中那样在模板中操作字符串,有助于我们快速处理传入模板中可能存在的问题,并即时进行调整。

# 字符串切片函数

正如我们在前面章节中看到的,能够操作字符串切片很有帮助。Sprig库帮助我们执行一些字符串切片操作。在我们的示例中,我们将根据“.”字符分割一个字符串。

首先,我们导入必要的库:

package main

import (
    "fmt"
    "os"
    "text/template"
    "github.com/Masterminds/sprig"
)
1
2
3
4
5
6
7
8

接下来,我们使用“.”作为分隔符分割模板字符串:

func main() {
    tpl := `{{$v := "Hands.On.High.Performance.In.Go" | splitn "." 5}}{{$v._3}}`
    functionMap := sprig.TxtFuncMap()
    t := template.Must(template.New("String Split").Funcs(functionMap).Parse(tpl))
    fmt.Print("String Split into Dict (word 3): ")
    err := t.Execute(os.Stdout, tpl)
    if err != nil {
        fmt.Printf("Couldn't create template: %s", err)
        return
    }
1
2
3
4
5
6
7
8
9
10

我们还可以使用sortAlpha函数将模板列表按字母顺序排序,如下所示:

    alphaSort := `{{ list "Foo" "Bar" "Baz" | sortAlpha}}`
    s := template.Must(template.New("sortAlpha").Funcs(functionMap).Parse(alphaSort))
    fmt.Print("\nAlpha Tuple: ")
    alphaErr := s.Execute(os.Stdout, tpl)
    if alphaErr != nil {
        fmt.Printf("Couldn't create template: %s", err)
        return
    }
    fmt.Print("\nString Slice Functions Completed\n")
}
1
2
3
4
5
6
7
8
9
10

这些字符串操作可以帮助我们整理模板函数中包含的字符串列表。

# 默认函数

Sprig的默认函数为模板函数返回默认值。我们可以检查特定数据结构的默认值,以及它们是否为空。每种数据类型对“空”的定义如下:

数据类型 空值定义
数值型 0
字符串型 ""(空字符串)
列表型 [](空列表)
字典型 {}(空字典)
布尔型 false
其他情况 Nil(也称为null)
结构体 没有空值定义;永远不会返回默认值

我们先进行导入:

package main

import (
    "fmt"
    "os"
    "text/template"
    "github.com/Masterminds/sprig"
)
1
2
3
4
5
6
7
8

接下来,我们设置空的和非空的模板变量:

func main() {
    emptyTemplate := map[string]interface{}{"Name": ""}
    fullTemplate := map[string]interface{}{"Name": "Bob"}
    tpl := `{{empty .Name}}`
    functionMap := sprig.TxtFuncMap()
    t := template.Must(template.New("Empty String").Funcs(functionMap).Parse(tpl))
1
2
3
4
5
6

然后,我们验证空模板和非空模板:

    fmt.Print("empty template: ")
    emptyErr := t.Execute(os.Stdout, emptyTemplate)
    if emptyErr != nil {
        fmt.Printf("Couldn't create template: %s", emptyErr)
        return
    }
    fmt.Print("\nfull template: ")
    fullErr := t.Execute(os.Stdout, fullTemplate)
    if emptyErr != nil {
        fmt.Printf("Couldn't create template: %s", fullErr)
        return
    }
    fmt.Print("\nEmpty Check Completed\n")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

当我们需要验证模板输入不为空时,这很有用。我们的输出结果符合预期:空模板被标记为true,而完整模板被标记为false:

我们还可以将JSON字面量编码为JSON字符串并进行格式化输出。如果你需要将一个由HTML创建的模板返回给终端用户一个JSON数组,这特别有用:

package main

import (
    "fmt"
    "os"
    "text/template"
    "github.com/Masterminds/sprig"
)

func main() {
    jsonDict := map[string]interface{}{"JSONExamples": map[string]interface{}{"foo": "bar", "bool": false, "integer": 7}}
    tpl := `{{.JSONExamples | toPrettyJson}}`
    functionMap := sprig.TxtFuncMap()
    t := template.Must(template.New("String Split").Funcs(functionMap).Parse(tpl))
    err := t.Execute(os.Stdout, jsonDict)
    if err != nil {
        fmt.Printf("Couldn't create template: %s", err)
        return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在我们的输出结果中,可以看到基于jsonDict输入的格式化后的JSON数据块:

在与内置的HTML/template和添加的content - encoding:json HTTP头一起使用时,这非常有用。

Sprig库有很多功能,我们将在本书的这部分内容中讨论其中一些。Sprig提供的完整功能列表可以在http://masterminds.github.io/sprig/上找到。

# 总结

在本章中,我们讨论了生成Go代码。我们介绍了如何生成最常见的Go代码之一——gRPC Protobuf。然后,我们讨论了使用链接工具链添加命令行参数,以及使用spf13/cobra和spf13/viper创建元编程的命令行界面(CLI)工具。最后,我们讨论了使用text/template、HTML/template和Sprig库进行模板编程。使用所有这些包将帮助我们编写可读、可复用、高性能的Go代码。从长远来看,这些模板还能为我们节省大量工作,因为它们往往具有可复用性和可扩展性。

在下一章中,我们将讨论如何优化内存资源管理。

上次更新: 2025/04/08, 19:40:35
6 编写易读的Go代码
8 Go语言中的内存管理

← 6 编写易读的Go代码 8 Go语言中的内存管理→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式