通过示例学习-Go-语言-2023-十一-
通过示例学习 Go 语言 2023(十一)
Golang 中的枚举
目录
-
概述
-
示例
概述
IOTA 提供了一种自动创建 Golang 中枚举的方式。让我们来看一个例子。
示例
package main
import "fmt"
type Size uint8
const (
small Size = iota
medium
large
extraLarge
)
func main() {
fmt.Println(small)
fmt.Println(medium)
fmt.Println(large)
fmt.Println(extraLarge)
}
输出
0
1
2
3
在上述程序中,我们创建了一个新类型。
type Size uint8
然后我们声明了一些类型为Size的常量。第一个常量small被设置为 iota,因此它将被设置为零。
small Size = iota
这就是原因。
fmt.Println(small) >> outputs 0
fmt.Println(medium) >> outputs 1
fmt.Println(large) >> outputs 2
fmt.Println(extraLarge) >> outputs 3
没有 IOTA,我们必须显式设置每个枚举值的值。
package main
import "fmt"
type Size uint8
const (
small Size = 0
medium Size = 1
large Size = 2
extraLarge Size = 3
)
func main() {
fmt.Println(small)
fmt.Println(medium)
fmt.Println(large)
fmt.Println(extraLarge)
}
输出
0
1
2
3
我们还可以在 Size 类型上定义一个toString方法,以打印枚举的确切值。请参见下面的程序。
package main
import "fmt"
type Size int
const (
small = Size(iota)
medium
large
extraLarge
)
func main() {
var m Size = 1
m.toString()
}
func (s Size) toString() {
switch s {
case small:
fmt.Println("Small")
case medium:
fmt.Println("Medium")
case large:
fmt.Println("Large")
case extraLarge:
fmt.Println("Extra Large")
default:
fmt.Println("Invalid Size entry")
}
}
输出
medium
我们为Size类型定义了一个toString方法。它可以用来打印 Size 类型常量的字符串值。*
Go 中的错误(Golang)- 高级
这是 golang 综合教程系列的第二十七章。有关系列其他章节的信息,请参考这个链接 – Golang 综合教程系列
下一个教程 – 恐慌与恢复
上一个教程 – 错误 – 第一部分
现在让我们查看当前的教程。以下是当前教程的目录。
目录
-
概述
-
错误的包装
-
解包错误
-
检查两个错误是否相等
-
使用等于运算符 (==)")
-
使用错误包的 Is 函数
-
-
从错误接口表示的错误中获取基础错误
-
使用.({type})断言 assert")
-
使用错误包的 As 函数
-
-
结论
概述
在这篇文章中,我们将涵盖与 go 中错误相关的高级主题。
-
错误的包装与解包
-
错误比较
-
从错误中提取基础类型
-
As和Is函数的错误包
请首先参考下面的链接,该链接介绍了go 中的错误的基础知识。
golangbyexample.com/error-in-golang/
这篇文章将涵盖有关错误的基本内容,例如:
-
错误概述
-
错误接口
-
创建错误的不同方法。
-
忽略错误
错误的包装
在 go 中,错误也可以包装另一个错误。
错误的包装是什么意思?这意味着创建一个错误层级,其中一个特定实例的错误包装了另一个错误,并且该特定实例本身也可以被包装在另一个错误中。下面是包装错误的语法。
e := fmt.Errorf("... %w ...", ..., err, ...)
%w指令用于包装错误。 fmt.Errorf应仅与一个 %w 指令一起调用。让我们看一个例子。
package main
import (
"fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
return "Error One happended"
}
func main() {
e1 := errorOne{}
e2 := fmt.Errorf("E2: %w", e1)
e3 := fmt.Errorf("E3: %w", e2)
fmt.Println(e2)
fmt.Println(e3)
}
输出
E2: Error One happended
E3: E2: Error One happended
在上面的程序中,我们创建了一个结构体errorOne,它有一个Error方法,因此实现了error接口。然后我们创建了一个名为e1的errorOne结构体实例。接着我们将该实例e1包装到另一个错误e2中,如下所示。
e2 := fmt.Errorf("E2: %w", e1)
然后我们将e2包装到e3中,如下所示。
e3 := fmt.Errorf("E3: %w", e2)
因此我们创建了一个错误的层次结构,其中e3包装了e2,而e2又包装了e1。因此e3也通过传递方式包装了e1。当我们打印e2时,它也会打印来自e1的错误,并给出输出。
E2: Error One happended
当我们打印e3时,它会打印来自e2和e1的错误,并给出输出。
E3: E2: Error One happended
现在脑海中出现的问题是,包装错误的用例是什么。为了理解这一点,让我们看一个例子。
package main
import (
"fmt"
)
type notPositive struct {
num int
}
func (e notPositive) Error() string {
return fmt.Sprintf("checkPositive: Given number %d is not a positive number", e.num)
}
type notEven struct {
num int
}
func (e notEven) Error() string {
return fmt.Sprintf("checkEven: Given number %d is not an even number", e.num)
}
func checkPositive(num int) error {
if num < 0 {
return notPositive{num: num}
}
return nil
}
func checkEven(num int) error {
if num%2 == 1 {
return notEven{num: num}
}
return nil
}
func checkPostiveAndEven(num int) error {
if num > 100 {
return fmt.Errorf("checkPostiveAndEven: Number %d is greater than 100", num)
}
err := checkPositive(num)
if err != nil {
return err
}
err = checkEven(num)
if err != nil {
return err
}
return nil
}
func main() {
num := 3
err := checkPostiveAndEven(num)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Givennnumber is positive and even")
}
}
输出
checkEven: Given number 3 is not an even number
在上述程序中,我们有一个函数checkPostiveAndEven,它检查一个数字是否是偶数且为正数。它会调用checkEven函数来检查数字是否为偶数。然后它会调用checkPositive函数来检查数字是否为正数。如果一个数字既不是偶数也不是正数,就会引发错误。
在上述程序中,无法确定错误的堆栈跟踪。我们知道这个错误来自于checkEven函数,但哪个函数调用了checkEven函数在错误中并不清楚。这就是包装错误的重要性。当项目较大且有许多函数相互调用时,这变得更加有用。让我们通过包装错误来重写程序。
package main
import (
"fmt"
)
type notPositive struct {
num int
}
func (e notPositive) Error() string {
return fmt.Sprintf("checkPositive: Given number %d is not a positive number", e.num)
}
type notEven struct {
num int
}
func (e notEven) Error() string {
return fmt.Sprintf("checkEven: Given number %d is not an even number", e.num)
}
func checkPositive(num int) error {
if num < 0 {
return notPositive{num: num}
}
return nil
}
func checkEven(num int) error {
if num%2 == 1 {
return notEven{num: num}
}
return nil
}
func checkPostiveAndEven(num int) error {
if num > 100 {
return fmt.Errorf("checkPostiveAndEven: Number %d is greater than 100", num)
}
err := checkPositive(num)
if err != nil {
return fmt.Errorf("checkPostiveAndEven: %w", err)
}
err = checkEven(num)
if err != nil {
return fmt.Errorf("checkPostiveAndEven: %w", err)
}
return nil
}
func main() {
num := 3
err := checkPostiveAndEven(num)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Given number is positive and even")
}
}
输出
checkPostiveAndEven: checkEven: Given number 3 is not an even number
上述程序与之前的程序相同,只是在checkPostiveAndEven函数中,我们像下面这样包装错误。
fmt.Errorf("checkPostiveAndEven: %w", err)
所以输出更加清晰,错误信息也更加详细。输出清楚地提到了调用的顺序。
checkPostiveAndEven: checkEven: Given number 3 is not an even number
解包错误
在上节中,我们研究了如何包装错误。同样也可以解包错误。错误包的 Unwrap 函数可以用来解包错误。下面是该函数的语法。
func Unwrap(err error) error
如果err包装了另一个错误,那么将返回被包装的错误,否则Unwrap函数将返回 nil。
让我们看一个程序来说明同样的情况。
import (
"errors"
"fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
return "Error One happened"
}
func main() {
e1 := errorOne{}
e2 := fmt.Errorf("E2: %w", e1)
e3 := fmt.Errorf("E3: %w", e2)
fmt.Println(errors.Unwrap(e3))
fmt.Println(errors.Unwrap(e2))
fmt.Println(errors.Unwrap(e1))
}
输出
E2: Error One happended
Error One happended
在上述程序中,我们创建了一个结构体errorOne,它有一个Error方法,因此它实现了error接口。然后我们创建了一个名为e1的errorOne结构体实例。接着,我们将该实例e1包装到另一个错误e2中,如下所示。
e2 := fmt.Errorf("E2: %w", e1)
然后我们将e2包装到e3中,如下所示。
e3 := fmt.Errorf("E3: %w", e2)
因此。
fmt.Println(errors.Unwrap(e3))
将返回被包装的错误e2,因为e3包装了e2,输出将是
E2: Error One happened
同时。
fmt.Println(errors.Unwrap(e1))
将输出 nil,因为e1没有包装任何错误。
{nil}
检查两个错误是否相等
首先,错误的相等性是什么意思?正如你所知道的,错误在 Go 中由错误接口表示。在 Go 中,当两个接口相等时:
-
两者指向相同的基础类型。
-
基础值相等(或两个都是 nil)
所以上述两个要点同样适用于比较错误。有两种方法可以检查给定的错误是否相等。
使用相等运算符(==)。
==运算符可以用于比较两个 Go 语言中的错误。
使用错误包的Is函数。
golang.org/pkg/errors/
。使用 Is 函数比使用等于运算符更可取,因为它通过顺序展开第一个错误来检查相等性,并在每一步展开时与目标错误进行匹配。稍后我们将看到一个例子,以充分理解为什么这更可取。下面是 Is 函数的语法。
func Is(err, target error) bool
让我们看一个例子。
package main
import (
"errors"
"fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
return "Error One happended"
}
func main() {
var err1 errorOne
err2 := do()
if err1 == err2 {
fmt.Println("Equality Operator: Both errors are equal")
}
if errors.Is(err1, err2) {
fmt.Println("Is function: Both errors are equal")
}
}
func do() error {
return errorOne{}
}
输出
Equality Operator: Both errors are equal
Is function: Both errors are equal
在上面的程序中,我们创建了 errorOne 结构体,该结构体定义了 Error 方法,因此实现了 error 接口。我们创建了一个 err1 变量,它是 errorOne 结构体的一个实例。我们还创建了一个 do() 函数,该函数引发了一个 errorOne 类型的错误,并在主函数中捕获到 err2 变量中。
然后我们使用以下方法比较两个错误。
- 使用等于运算符
err1 == err2
- 使用 errors 包的 Is 函数
errors.Is(err1, err2)
两种方法都正确地输出错误是相等的,因为 err1 和 err2 相同。
-
引用相同的基础类型,即 errorOne。
-
拥有相同的基础值
我们在上面提到,使用 Is 函数比使用等于运算符更可取,因为它通过顺序展开第一个错误来检查相等性,并在每一步展开时与目标错误进行匹配。让我们看一个例子。
package main
import (
"errors"
"fmt"
)
type errorOne struct{}
func (e errorOne) Error() string {
return "Error One happended"
}
func main() {
err1 := errorOne{}
err2 := do()
if err1 == err2 {
fmt.Println("Equality Operator: Both errors are equal")
} else {
fmt.Println("Equality Operator: Both errors are not equal")
}
if errors.Is(err2, err1) {
fmt.Println("Is function: Both errors are equal")
}
}
func do() error {
return fmt.Errorf("E2: %w", errorOne{})
}
输出
Equality Operator: Both errors are not equal
Is function: Both errors are equal
上面的程序几乎与之前的程序相同,唯一的区别是,在 do() 函数中我们也在包装错误。
return fmt.Errorf("E2: %w", errorOne{})
- 等于运算符的输出
Equality Operator: Both errors are not equal
- 而 Is 函数的输出
Is function: Both errors are equal
这是因为 err2 返回了一个封装 errorOne 的实例,等于运算符无法捕获,但 Is 函数能够捕获。
从 error 接口表示的错误中获取基础错误
获取基础类型有两种方法
使用 .({type}) 断言
如果断言成功,则将返回相应的错误,否则将出现恐慌。下面是语法。
err := err.({type})
最好使用 ok 变量来防止在断言失败时出现恐慌。下面是相应的语法。如果错误的基础类型正确,ok 变量将被设置为 true。
err, ok := err.({type})
使用 errors 包的 As 函数
golang.org/pkg/errors/
。使用 As 函数比使用 .({type}) 断言更可取,因为它通过顺序展开第一个错误来检查匹配,并在每一步展开时与目标错误进行匹配。下面是 Is 函数的语法。
func As(err error, target interface{}) bool
As 函数将在第一个参数中找到第一个可以匹配目标的错误。一旦找到匹配项,它将把目标设置为该错误值。
让我们看一个例子。
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing.txt")
if e, ok := err.(*os.PathError); ok {
fmt.Printf("Using Assert: Error e is of type path error. Path: %v\n", e.Path)
} else {
fmt.Println("Using Assert: Error not of type path error")
}
var pathError *os.PathError
if errors.As(err, &pathError) {
fmt.Printf("Using As function: Error e is of type path error. Path: %v\n", pathError.Path)
}
}
func openFile(fileName string) error {
_, err := os.Open("non-existing.txt")
if err != nil {
return err
}
return nil
}
输出:
Using Assert: Error e is of type path error. Path: non-existing.txt
Using As function: Error e is of type path error. Path: non-existing.txt
在上面的程序中,我们有一个 openFile 函数,我们试图打开一个不存在的类型,因此它会引发错误。然后我们通过两种方式断言该错误。
- 使用 . 断言运算符。如果错误的基础类型是 *os.PathError,那么 ok 变量将被设置为 true,否则将被设置为 false。
e,ok := err.(*os.PathError); ok
- 使用 errors 包的As函数
errors.As(err, &pathError)
两种方法都正确地断言错误为类型os.PathError,因为openFile函数返回的错误是类型os.PathError。
我们上面提到,使用As函数比使用.({type})断言更可取,因为它通过顺序解包第一个错误并在每一步解包时与目标错误进行匹配。让我们看一个例子来理解这一点。
import (
"errors"
"fmt"
"os"
)
func main() {
var pathError *os.PathError
err := openFile("non-existing.txt")
if e, ok := err.(*os.PathError); ok {
fmt.Printf("Using Assert: Error e is of type path error. Error: %v\n", e)
} else {
fmt.Println("Using Assert: Error not of type path error")
}
if errors.As(err, &pathError) {
fmt.Printf("Using As function: Error e is of type path error. Error: %v\n", pathError)
}
}
func openFile(fileName string) error {
_, err := os.Open("non-existing.txt")
if err != nil {
return fmt.Errorf("Error opening: %w", err)
}
return nil
}
输出:
Using Assert: Error not of type path error
Using As function: Error e is of type path error. Error: open non-existing.txt: no such file or directory
上述程序与之前的程序几乎相同,唯一的区别在于在openFile函数中我们也对错误进行了包装。
return fmt.Errorf("Error opening: %w", err)
- 该断言输出
Using Assert: Error not of type path error
- 当As函数输出时
Using As function: Error e is of type path error. Error: open non-existing.txt: no such file or directory
这是因为openFile函数返回的错误包装了os.PathError错误,该错误未被点(‘.’)断言捕获,但被As*函数捕获。
结论
这就是关于 Golang 中错误的高级主题。希望你喜欢这篇文章。请在评论中分享反馈/改进/错误。
下一教程 – 恐慌与恢复
上一个教程 – 错误 – 第一部分
Go 中的错误 (Golang)
这是 Golang 综合教程系列的第二十六章。有关系列的其他章节,请参考此链接 – Golang 综合教程系列
下一教程 – 错误 - 第二部分
上一教程 – 选择语句
现在让我们来看看当前的教程。以下是当前教程的目录。
目录
-
概述
-
使用实现错误接口的类型
-
将错误作为类型的优点
-
创建错误的不同方式
-
使用 errors.New(“some_error_message”)")
-
使用 fmt.Errorf(“error is %s”, “some_error_message”).. ")
-
创建自定义错误
-
-
忽略错误
-
结论
概述
在本教程中,我们将学习如何在 Golang 中进行错误处理。与其他传统语言相比,Go 并没有异常和 try-catch 来处理错误。
Golang 中的错误处理可以通过两种方式进行。
-
使用实现 error 接口的类型 – 这是一种传统的表示错误的方式,也是习惯用法。
-
使用 panic 和 recover
在本文中我们只讨论第一部分。
使用实现错误接口的类型
Go 处理错误的方式是显式地将错误作为一个单独的值返回。
理解 Golang 中错误的关键是理解 error 接口。以下是 builtin 包中定义的 error 接口的样子。
type error interface {
Error() string
}
error 接口是 Golang 中表示错误条件的传统方式。如果它是 nil,则意味着没有错误。因此,在 Go 中,错误被视为一个值。这个值实现了 error 接口,可以传递给函数,可以从函数返回,也可以存储在变量中。
任何定义了Error方法的类型都被视为实现了error接口。正如我们之前提到的,Go 没有异常和 try-catch,因此处理错误条件的惯用方式是将错误作为最后一个返回值返回。然后可以检查该值是否为 nil。如果是 nil,则调用的函数没有返回错误,否则返回了错误。现在让我们看看一个演示我们刚才讨论内容的程序。
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("non-existing.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(file.Name() + "opened succesfully")
}
}
输出
open non-existing.txt: no such file or directory
在上面的程序中,我们调用os.Open函数打开一个不存在的文件。由于该文件不存在,它在输出中给出了上述错误消息。但是,这条错误消息是从哪里来的呢?让我们理解一下。
以下是os.Open函数的签名。
func Open(name string) (*File, error)
它接受一个参数作为输入,即要打开的文件路径。它有两个返回值。
-
如果文件存在,则返回文件结构的指针。
-
如果有错误,则返回类型为PathError,它实现了error*接口。
请查看PathError结构体类型这里- golang.org/src/os/error.go
type PathError struct {
Op string
Path string
Err error
}
PathError结构体的指针定义了Error()方法。
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
由于它定义了Error方法,因此PathError实现了error接口。因此,error可以作为os.Open函数的第二个返回值返回。现在注意,在main*函数中,我们通过仅与 nil 比较来检查错误的存在。
我们之所以与 nil 进行比较,是因为接口的默认零值是 nil,且error也是接口,其默认零值也是 nil。
另外,fmt.Println函数内部检查传递给它的类型是否实现了error接口。如果是,则会调用该类型的Error方法。这就是这一行的意思。
fmt.Println(err)
输出结果
open non-existing.txt: no such file or directory
使用任何类型作为错误
虽然我们在 Go 中有一个错误接口,并且我们期望 Go 中的每个错误都遵循这个接口,如上所见,但这仍然不是强制要求。在 Go 中,任何类型都可以被视为错误,但通常不推荐这样做,这也不是 Go 中惯用的编码方式。我们在这里提及是为了完整性和讨论。
使用错误作为类型的优点
-
它允许更好地控制错误处理。可以在每一步检查错误。
-
避免了 try-catch 和异常处理的丑陋代码。
创建错误的不同方法
现在让我们看看创建错误的不同方法。
使用 errors.New("some_error_message")
package main
import (
"errors"
"fmt"
)
func main() {
sampleErr := errors.New("error occured")
fmt.Println(sampleErr)
输出:
error occured
使用 fmt.Errorf("error is %s", "some_error_message")。
这种方式创建格式化的错误消息。
package main
import (
"fmt"
)
func main() {
sampleErr := fmt.Errorf("Err is: %s", "database connection issue")
fmt.Println(sampleErr)
}
输出:
Err is: database connection issue
创建自定义错误
下面的示例说明了自定义错误的使用。在下面的示例中。
-
inputError 是一个具有 Error()方法的结构体,因此它属于错误接口类型。
-
你还可以通过扩展其字段或添加新方法来为自定义错误添加额外信息。inputError 有一个名为missingFields的额外字段和一个getMissingFields函数。
-
我们可以使用类型断言将错误转换为输入错误。
示例:
package main
import "fmt"
type inputError struct {
message string
missingField string
}
func (i *inputError) Error() string {
return i.message
}
func (i *inputError) getMissingField() string {
return i.missingField
}
func main() {
err := validate("", "")
if err != nil {
if err, ok := err.(*inputError); ok {
fmt.Println(err)
fmt.Printf("Missing Field is %s\n", err.getMissingField())
}
}
}
func validate(name, gender string) error {
if name == "" {
return &inputError{message: "Name is mandatory", missingField: "name"}
}
if gender == "" {
return &inputError{message: "Gender is mandatory", missingField: "gender"}
}
return nil
}
输出:
Name is mandatory
Missing Field is name
忽略错误
下划线(‘_’)操作符可用于忽略函数调用返回的错误。在查看程序之前,重要的是要注意错误绝不应该被忽略。这并不是推荐的做法。让我们来看一个程序。
package main
import (
"fmt"
"os"
)
func main() {
file, _ := os.Open("non-existing.txt")
fmt.Println(file)
}
输出
{nil}
在上述程序中,我们使用了下划线操作符来忽略打开不存在的文件时的错误。这就是为什么函数返回的文件实例是 nil。因此,在使用函数返回的任何其他参数之前,最好检查是否有错误,因为它可能是 nil,这会导致不必要的问题,有时还可能导致恐慌。
结论
这就是关于 golang 中错误的全部内容。我们将在下一章讨论与错误相关的高级主题。
希望你喜欢这篇文章。请在评论中分享反馈/改进/错误。
下一篇教程 – 错误 - 第二部分
上一篇教程 – 选择语句
在 Go (Golang) 中的 defer 参数评估
目录
-
概述
-
示例
-
输出
概述
defer 参数在 defer 语句被评估时进行计算。
让我们看看一个程序。
示例
package main
import "fmt"
func main() {
sample := "abc"
defer fmt.Printf("In defer sample is: %s\n", sample)
sample = "xyz"
}
输出
In defer sample is: abc
在上面的程序中,当 defer 语句被评估时,sample 变量的值为 “abc”。在 defer 函数中,我们打印 sample 变量。在 defer 语句之后,我们将 sample 变量的值更改为 “xyz”。但是程序输出 “abc” 而不是 “xyz”,因为当 defer 参数被评估时,sample 变量的值为 “abc”。
在 Go (Golang)中进行后缀表达式求值
来源:
golangbyexample.com/evaluation-of-postfix-expression-golang/
目录
** 后缀表达式求值
- 实现
后缀表达式求值
在本教程中,我们将评估一个后缀表达式。
算法:
-
从左到右扫描后缀表达式。
-
如果当前扫描的字符是操作数,则将其推入堆栈。
-
如果当前扫描的字符是运算符,则从堆栈中弹出两个操作数。在这两个弹出的操作数上对运算符进行求值,并将结果推入堆栈。
-
当后缀表达式完全扫描后,堆栈中应该只有一个值,这就是后缀表达式的结果。
实现
以下是 Golang 中“中缀转后缀”的实现。
package main
import (
"fmt"
"math"
"strconv"
)
type Stack []int
//IsEmpty: check if stack is empty
func (st *Stack) IsEmpty() bool {
return len(*st) == 0
}
//Push a new value onto the stack
func (st *Stack) Push(data int) {
*st = append(*st, data) //Simply append the new value to the end of the stack
//fmt.Println("st after push", st)
}
//Remove top element of stack. Return false if stack is empty.
func (st *Stack) Pop() bool {
if st.IsEmpty() {
return false
} else {
index := len(*st) - 1 // Get the index of top most element.
*st = (*st)[:index] // Remove it from the stack by slicing it off.
//fmt.Println("st after pops", *st)
return true
}
}
//Return top element of stack. Return false if stack is empty.
func (st *Stack) Top() int {
if st.IsEmpty() {
return 0
} else {
index := len(*st) - 1 // Get the index of top most element.
element := (*st)[index] // Index onto the slice and obtain the element.
return element
}
}
//function to evaluate postfix expression
func evaluationPostfix(postfix string) int {
var intStack Stack
for _, char := range postfix {
opchar := string(char)
//fmt.Println(opchar)
if opchar >= "0" && opchar <= "9" {
i1, _ := strconv.Atoi(opchar)
//fmt.Println("Integer value is: ", i1)
intStack.Push(i1)
//fmt.Println(intStack)
} else {
opr1 := intStack.Top()
intStack.Pop()
opr2 := intStack.Top()
intStack.Pop()
switch char {
case '^':
x := math.Pow(float64(opr2), float64(opr1))
intStack.Push(int(x))
case '+':
intStack.Push(opr2 + opr1)
case '-':
intStack.Push(opr2 - opr1)
case '*':
intStack.Push(opr2 * opr1)
case '/':
intStack.Push(opr2 / opr1)
}
}
}
return intStack.Top()
}
func main() {
postfix := "2323⁵-212*+^*+4-"
evaluationReslt := evaluationPostfix(postfix)
fmt.Printf("evaluation of %s is %d", postfix, evaluationReslt)
}
输出:
2+3*(2³-5)^(2+1*2)-4 infix has 2323⁵-212*+^*+4- postfix
我们可以通过取消注释fmt.Println行来检查每次推送和弹出操作后的堆栈状态。
注意: 查看我们的 Golang 高级教程。本系列教程内容详尽,我们尽力覆盖所有概念并附有示例。本教程适合希望获得专业知识和扎实理解 Golang 的人 - Golang 高级教程
如果你有兴趣了解所有设计模式在 Golang 中的实现。如果是的话,这篇文章适合你 - 所有设计模式 Golang
Go (Golang) 中的恢复函数示例
目录
-
概述
-
示例
概述
Go 提供了一个内置函数recover用于从恐慌中恢复。下面是这个函数的签名
func recover() interface{}
defer函数是唯一在panic之后被调用的函数。因此,把recover函数放在defer函数中是有意义的。如果recover函数不在 defer 函数内,则无法停止panic。
例子
让我们看一个 recover 的例子
package main
import "fmt"
func main() {
a := []string{"a", "b"}
checkAndPrint(a, 2)
fmt.Println("Exiting normally")
}
func checkAndPrint(a []string, index int) {
defer handleOutOfBounds()
if index > (len(a) - 1) {
panic("Out of bound access for slice")
}
fmt.Println(a[index])
}
func handleOutOfBounds() {
if r := recover(); r != nil {
fmt.Println("Recovering from panic:", r)
}
}
输出
Recovering from panic: Out of bound access for slice
Exiting normally
在上面的程序中,我们有一个函数checkAndPrint,它检查并打印传递到参数中的索引处的切片元素。如果传递的索引大于数组的长度,则程序将会恐慌。我们在checkAndPrint函数的开始处添加了一个名为handleOutOfBounds的 defer 函数。这个函数包含对 recover 函数的调用,如下所示。
if r := recover(); r != nil {
fmt.Println("Recovering from panic:", r)
}
recover函数将捕获恐慌,我们还可以打印恐慌中的消息。
Recovering from panic: Out of bound access for slice
在恢复函数之后,程序继续运行,控制权返回到调用的函数,这里是main。这就是为什么我们得到输出为
Exiting normally
recover 函数返回传递给 panic 函数的值。因此,检查 recover 函数的返回值是一个好习惯。如果返回值非 nil,则没有发生恐慌,并且 recover 函数没有与恐慌一起被调用。这就是我们在 defer 函数handleOutOfBounds中有以下代码的原因。
if r := recover(); r != nil
如果r为 nil,那么没有发生恐慌。所以如果没有恐慌,则对 recover 的调用将返回 nil。
注意,如果 defer 函数和 recover 函数没有从恐慌函数中调用,则在这种情况下,恐慌也可以在被调用的函数中恢复。实际上,可能在调用栈的后续链中恢复。
让我们看一个示例。
package main
import "fmt"
func main() {
a := []string{"a", "b"}
checkAndPrintWithRecover(a, 2)
fmt.Println("Exiting normally")
}
func checkAndPrintWithRecover(a []string, index int) {
defer handleOutOfBounds()
checkAndPrint(a, 2)
}
func checkAndPrint(a []string, index int) {
if index > (len(a) - 1) {
panic("Out of bound access for slice")
}
fmt.Println(a[index])
}
func handleOutOfBounds() {
if r := recover(); r != nil {
fmt.Println("Recovering from panic:", r)
}
}
输出
Recovering from panic: Out of bound access for slice
Exiting normally
上面的程序与之前的程序几乎相同,唯一的不同是我们有一个额外的函数checkAndPrintWithRecover,其中包含对
-
带有 recover 的 defer 函数是handleOutOfBounds
-
调用checkAndPrint函数
基本上,checkAndPrint函数引发恐慌,但没有 recover 函数,而是调用 recover 的地方在checkAndPrintWithRecover函数中。但程序仍然能够从恐慌中恢复,因为恐慌也可以在被调用的函数中恢复,并且可以在链中继续恢复。
Go (Golang)中的可执行和非可执行模块
模块是包含嵌套 Go 包的目录。因此,模块本质上可以被视为仅包含嵌套包的包。现在,包可以是可执行包或实用包(非可执行)。类似于包,模块也可以分为两种类型。
-
可执行模块 – 我们已经知道main是 GoLang 中的可执行包。因此,包含 main 包的模块就是可执行模块。main 包将包含一个主函数,表示程序的开始。安装包含main包的模块时,它将在$GOBIN 目录中创建一个可执行文件。
-
非可执行模块或实用模块– 除main包外的任何包都是非可执行包。它不是自我可执行的。它仅包含可以被可执行包使用的实用函数和其他实用内容。因此,如果模块不包含main包,则它将是非可执行或实用模块。此模块旨在作为实用工具使用,将被其他模块导入。
让我们看一个例子以理解两者。我们需要创建一个可供他人使用的模块(非可执行模块或实用模块)和一个可以导入该模块的模块(可执行模块)。为此,让我们创建两个模块
-
sample.com/math模块 – 这将是非可执行模块或实用模块
-
school模块 – 这将是可执行模块。它将包含 main 包和主函数
school模块将调用sample.com/math模块的代码
首先创建将由school模块使用的sample.com/math模块
-
创建一个math目录
-
创建一个导入路径为sample.com/math的模块
go mod init sample.com/math
- 在math目录中创建一个名为math.go的文件,内容如下
package math
func Add(a, b int) int {
return a + b
}
现在让我们创建 school 模块
-
在与math目录并排的同一路径下创建一个school目录
-
创建一个模块名为school
go mod init school
- 现在让我们修改go.mod文件,以在 school 模块中导入 math 模块。为了导入未推送到 VCS 的本地模块,我们将使用 replace 目录。replace 目录将用您指定的路径替换模块路径。
module school
go 1.14
replace sample.com/math => ../math
- 创建文件school.go,将使用sample.com/math模块中的Add函数
package main
import (
"fmt"
"sample.com/math"
)
func main() {
fmt.Println(math.Add(2, 4))
}
现在执行go run
go run school.go
它能够调用sample.com/math模块的 Add 函数,并正确输出为 6。
所以本质上
-
sample.com/math是一个非可执行模块或实用模块
-
school模块是一个可执行模块
在 Go(Golang)中执行操作系统/系统命令
目录
-
概述
-
代码
概述
os/exec 包可用于从 Go 触发任何操作系统或系统命令。它有两个可以实现相同功能的函数。
-
命令 – 用于创建 cmd 对象
-
输出 – 它运行命令并返回标准输出
代码
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
out, err := exec.Command("pwd").Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
}
输出:
It will output the location of current working directory
在 Go (Golang) 的 Select 语句中执行多个案例
目录
-
概述
-
代码
概述
select 语句仅在发送或接收通道操作准备好的情况下执行其中一个案例。它不能执行多个案例,但有一个变通办法。我们可以在 set 语句外部放一个 for 循环。这个 for 循环会调用 select 语句,次数等于循环的迭代次数。
让我们看一个例子。
代码
package main
import "fmt"
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go goOne(ch1)
go goTwo(ch2)
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}
func goOne(ch chan string) {
ch <- "From goOne goroutine"
}
func goTwo(ch chan string) {
ch <- "From goTwo goroutine"
}
输出
From goOne goroutine
From goTwo goroutine
在上面的程序中,我们在 select 语句上放置了一个长度为二的 for 循环。因此,select 语句执行了两次,并打印出每个案例语句接收到的值。
这样我们可以执行多个案例语句,但请注意这并不是确定性的。如果对于特定的案例语句,在同一通道上有多个操作可用,那么该案例在所有迭代中都可以被执行。
从 Go (Golang) 执行 shell 文件
目录
-
概述:
-
代码:
概述:
os/exec 包可用于从 Go 中触发任何操作系统命令。也可以用于触发 .sh 文件。
- 首先,在同一目录下创建一个 sample.sh 文件。确保它以 #!/bin/sh 或 #!/bin/bash 开头。
sample.sh
#!/bin/sh
echo "Triggered from .go file" > out.out
- 使此 sample.sh 文件可执行
chmod +x sample.sh
代码:
在同一目录下创建一个 main.go 文件
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
out, err := exec.Command("/bin/sh", "sample.sh").Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
}
输出:
A out.out file will be created in the same directory
- bash*
Go 中结构体的导出与未导出字段
来源:
golangbyexample.com/exported-unexported-fields-struct-go/
Go 没有任何公共、私有或受保护的关键字。控制包外可见性的唯一机制是使用大写和小写格式。
-
大写标识符 是导出的。大写字母表示这是一个导出的标识符,可以在包外使用。
-
小写标识符 是未导出的。小写字母表示该标识符未导出,只能在同一包内访问。
因此,任何以大写字母开头的结构体都被导出到其他包。同样,任何以大写字母开头的结构体字段也是导出的,反之则不是。任何以大写字母开头的结构体方法也是导出的。让我们看一个示例,展示结构体、结构体字段和方法的导出与未导出。请见下面的 model.go 和 test.go,它们都属于 main 包。
-
结构体
-
结构体 Person 是导出的
-
结构体 company 是未导出的
-
-
结构体的字段
-
Person 结构体字段 Name 是导出的
-
Person 结构体字段 age 是未导出的,但 Name 是导出的
-
-
结构体的方法
-
Person 结构体的方法 GetAge() 是导出的
-
Person 结构体的方法 getName() 是未导出的
-
model.go
package main
import "fmt"
//Person struct
type Person struct {
Name string
age int
}
//GetAge of person
func (p *Person) GetAge() int {
return p.age
}
func (p *Person) getName() string {
return p.Name
}
type company struct {
}
让我们在同一 main 包中编写一个文件 test.go。见下文。
test.go
package main
import "fmt"
//Test function
func Test() {
//STRUCTURE IDENTIFIER
p := &Person{
Name: "test",
age: 21,
}
fmt.Println(p)
c := &company{}
fmt.Println(c)
//STRUCTURE'S FIELDS
fmt.Println(p.Name)
fmt.Println(p.age)
//STRUCTURE'S METHOD
fmt.Println(p.GetAge())
fmt.Println(p.getName())
}
运行此文件时,它能够访问 model.go 中的所有导出和未导出字段,因为它们都在同一包 main 中。没有编译错误,输出如下。
输出:
&{test 21}
&{}
test
21
21
test
现在让我们将上述文件 model.go 移动到一个名为 model 的不同包中。现在注意运行‘go build’时的输出。它会给出编译错误。所有编译错误都是因为 main 包中的 test.go 无法引用 model 包中未导出的 model.go 字段。
model.go
package model
//Person struct
type Person struct {
Name string
age int
}
func (p *Person) GetAge() int {
return p.age
}
func (p *Person) getName() string {
return p.Name
}
type company struct {
}
test.go
package main
import (
"fmt"
//This will path of your model package
"<somepath>/model"
)
//Test function
func main() {
//STRUCTURE IDENTIFIER
p := &model.Person{
Name: "test",
age: 21,
}
fmt.Println(p)
c := &model.company{}
fmt.Println(c)
//STRUCTURE'S FIELDS
fmt.Println(p.Name)
fmt.Println(p.age)
//STRUCTURE'S METHOD
fmt.Println(p.GetAge())
fmt.Println(p.getName())
}</somepath>
输出:
unknown field 'age' in struct literal of type model.Person
cannot refer to unexported name model.company
undefined: model.company
p.age undefined (cannot refer to unexported field or method age)
p.getName undefined (cannot refer to unexported field or method model.(*Person).getName)
在 Go(Golang)中从字符串中提取 URL
目录
-
概述
-
程序
概述
以下 Go 包可以用于从给定字符串中提取 URL
使用此包有两种方法
-
严格 – 在严格模式下,它只匹配带有方案的 URL
-
宽松 – 在宽松模式下,它匹配严格模式下的任何 URL,以及没有方案的任何 URL
你可以指定要过滤的方案。对此有一个函数
StrictMatchingScheme
程序
让我们首先看一个程序
package main
import (
"fmt"
"mvdan.cc/xurls/v2"
)
func main() {
xurlsStrict := xurls.Strict()
output := xurlsStrict.FindAllString("golangbyexample.com is https://golangbyexample.com", -1)
fmt.Println(output)
xurlsRelaxed := xurls.Relaxed()
output = xurlsRelaxed.FindAllString("The website is golangbyexample.com", -1)
fmt.Println(output)
output = xurlsRelaxed.FindAllString("golangbyexample.com is https://golangbyexample.com", -1)
fmt.Println(output)
}
输出
[https://golangbyexample.com]
[golangbyexample.com]
[golangbyexample.com https://golangbyexample.com]
注意在严格模式下,它不会将 golangbyexample.com 返回到输出中,因为该 URL 没有方案。
让我们看看另一个提取多个 URL 的程序
package main
import (
"fmt"
"mvdan.cc/xurls/v2"
)
func main() {
xurlsStrict := xurls.Strict()
input := "The webiste is https://golangbyexample.com:8000/tutorials/intro amd mail to mailto:contactus@golangbyexample.com"
output := xurlsStrict.FindAllString(input, -1)
fmt.Println(output)
}
输出
[https://golangbyexample.com:8000/tutorials/intro mailto:contactus@golangbyexample.com]
如果我们想将输出限制为特定方案,也可以做到。
package main
import (
"fmt"
"log"
"mvdan.cc/xurls/v2"
)
func main() {
xurlsStrict, err := xurls.StrictMatchingScheme("https")
if err != nil {
log.Fatalf("Some error occured. Error: %s", err)
}
input := "The webiste is https://golangbyexample.com:8000/tutorials/intro amd mail to mailto:contactus@golangbyexample.com"
output := xurlsStrict.FindAllString(input, -1)
fmt.Println(output)
}
输出
[https://golangbyexample.com:8000/tutorials/intro]
在上面的程序中,我们提供了 https 作为方案,这就是为什么我们只有一个输出
Go (Golang)中的外观设计模式
注意:如果想了解如何在 GO 中实现其他设计模式,请参阅此完整参考 – Go (Golang) 中的所有设计模式
目录
** 定义:
-
问题陈述:
-
使用时机:
-
UML 图:
-
映射
-
实践示例:
-
完整工作代码:
定义:
外观模式被归类为结构设计模式。该设计模式旨在隐藏底层系统的复杂性,并为客户端提供一个简单的接口。它为系统中许多接口提供统一的接口,从客户端的角度来看,使用起来更为简单。基本上,它为复杂系统提供了更高级别的抽象。
外观一词本身的意思是
建筑物的主要正面,面向街道或开放空间
仅显示建筑物的正面,所有潜在的复杂性隐藏在后面。
让我们通过一个简单的示例理解外观设计模式。在这个数字钱包的时代,当有人实际进行钱包借记/贷记时,后台发生了很多事情,客户端可能并不知情。以下列表展示了一些在贷记/借记过程中发生的活动。
-
检查账户
-
检查安全密码
-
贷方/借方余额
-
记录分类账条目
-
发送通知
如可以注意到,单个借方/贷方操作涉及许多事情。这就是外观模式发挥作用的地方。作为客户端,只需输入钱包号码、安全密码、金额并指定操作类型。其余的事情都在后台处理。在这里,我们创建一个WalletFacade,它为客户端提供了一个简单的接口,并负责处理所有潜在的操作。
问题陈述:
- 为了使用复杂系统,客户端必须了解潜在的细节。需要向客户端提供一个简单的接口,使其能够在不知道任何内部复杂细节的情况下使用复杂系统。
使用时机:
- 当你想以简化的方式展示一个复杂系统时。
– 就像在上面的贷记/借记钱包示例中,他们只需要知道一个接口,其他事情应由该接口处理。
UML 图:
以下是与下面给出的实践示例相对应的映射 UML 图
映射
下表表示 UML 图中演员与代码中实际实现演员之间的映射。
钱包外观 | walletFacade.go |
---|---|
账户 | account.go |
securityCode | securityCode.go |
钱包 | wallet.go |
账本 | ledger.go |
通知 | notification.go |
客户 | main.go |
实际示例:
walletFacade.go
package main
import "fmt"
type walletFacade struct {
account *account
wallet *wallet
securityCode *securityCode
notification *notification
ledger *ledger
}
func newWalletFacade(accountID string, code int) *walletFacade {
fmt.Println("Starting create account")
walletFacacde := &walletFacade{
account: newAccount(accountID),
securityCode: newSecurityCode(code),
wallet: newWallet(),
notification: ¬ification{},
ledger: &ledger{},
}
fmt.Println("Account created")
return walletFacacde
}
func (w *walletFacade) addMoneyToWallet(accountID string, securityCode int, amount int) error {
fmt.Println("Starting add money to wallet")
err := w.account.checkAccount(accountID)
if err != nil {
return err
}
err = w.securityCode.checkCode(securityCode)
if err != nil {
return err
}
w.wallet.creditBalance(amount)
w.notification.sendWalletCreditNotification()
w.ledger.makeEntry(accountID, "credit", amount)
return nil
}
func (w *walletFacade) deductMoneyFromWallet(accountID string, securityCode int, amount int) error {
fmt.Println("Starting debit money from wallet")
err := w.account.checkAccount(accountID)
if err != nil {
return err
}
err = w.securityCode.checkCode(securityCode)
if err != nil {
return err
}
err = w.wallet.debitBalance(amount)
if err != nil {
return err
}
w.notification.sendWalletDebitNotification()
w.ledger.makeEntry(accountID, "credit", amount)
return nil
}
account.go
package main
import "fmt"
type account struct {
name string
}
func newAccount(accountName string) *account {
return &account{
name: accountName,
}
}
func (a *account) checkAccount(accountName string) error {
if a.name != accountName {
return fmt.Errorf("Account Name is incorrect")
}
fmt.Println("Account Verified")
return nil
}
securityCode.go
package main
import "fmt"
type securityCode struct {
code int
}
func newSecurityCode(code int) *securityCode {
return &securityCode{
code: code,
}
}
func (s *securityCode) checkCode(incomingCode int) error {
if s.code != incomingCode {
return fmt.Errorf("Security Code is incorrect")
}
fmt.Println("SecurityCode Verified")
return nil
}
wallet.go
package main
import "fmt"
type wallet struct {
balance int
}
func newWallet() *wallet {
return &wallet{
balance: 0,
}
}
func (w *wallet) creditBalance(amount int) {
w.balance += amount
fmt.Println("Wallet balance added successfully")
return
}
func (w *wallet) debitBalance(amount int) error {
if w.balance < amount {
return fmt.Errorf("Balance is not sufficient")
}
fmt.Println("Wallet balance is Sufficient")
w.balance = w.balance - amount
return nil
}
ledger.go
package main
import "fmt"
type ledger struct {
}
func (s *ledger) makeEntry(accountID, txnType string, amount int) {
fmt.Printf("Make ledger entry for accountId %s with txnType %s for amount %d", accountID, txnType, amount)
return
}
notification.go
package main
import "fmt"
type notification struct {
}
func (n *notification) sendWalletCreditNotification() {
fmt.Println("Sending wallet credit notification")
}
func (n *notification) sendWalletDebitNotification() {
fmt.Println("Sending wallet debit notification")
}
main.go
package main
import (
"fmt"
"log"
)
func main() {
fmt.Println()
walletFacade := newWalletFacade("abc", 1234)
fmt.Println()
err := walletFacade.addMoneyToWallet("abc", 1234, 10)
if err != nil {
log.Fatalf("Error: %s\n", err.Error())
}
fmt.Println()
err = walletFacade.deductMoneyFromWallet("ab", 1234, 5)
if err != nil {
log.Fatalf("Error: %s\n", err.Error())
}
}
输出:
Starting create account
Account created
Starting add money to wallet
Account Verified
SecurityCode Verified
Wallet balance added successfully
Sending wallet credit notification
Make ledger entry for accountId abc with txnType credit for amount 10
Starting debit money from wallet
Account Verified
SecurityCode Verified
Wallet balance is Sufficient
Sending wallet debit notification
Make ledger entry for accountId abc with txnType debit for amount 5
完整工作代码:
package main
import (
"fmt"
"log"
)
type walletFacade struct {
account *account
wallet *wallet
securityCode *securityCode
notification *notification
ledger *ledger
}
func newWalletFacade(accountID string, code int) *walletFacade {
fmt.Println("Starting create account")
walletFacacde := &walletFacade{
account: newAccount(accountID),
securityCode: newSecurityCode(code),
wallet: newWallet(),
notification: ¬ification{},
ledger: &ledger{},
}
fmt.Println("Account created")
return walletFacacde
}
func (w *walletFacade) addMoneyToWallet(accountID string, securityCode int, amount int) error {
fmt.Println("Starting add money to wallet")
err := w.account.checkAccount(accountID)
if err != nil {
return err
}
err = w.securityCode.checkCode(securityCode)
if err != nil {
return err
}
w.wallet.creditBalance(amount)
w.notification.sendWalletCreditNotification()
w.ledger.makeEntry(accountID, "credit", amount)
return nil
}
func (w *walletFacade) deductMoneyFromWallet(accountID string, securityCode int, amount int) error {
fmt.Println("Starting debit money from wallet")
err := w.account.checkAccount(accountID)
if err != nil {
return err
}
err = w.securityCode.checkCode(securityCode)
if err != nil {
return err
}
err = w.wallet.debitBalance(amount)
if err != nil {
return err
}
w.notification.sendWalletDebitNotification()
w.ledger.makeEntry(accountID, "credit", amount)
return nil
}
type account struct {
name string
}
func newAccount(accountName string) *account {
return &account{
name: accountName,
}
}
func (a *account) checkAccount(accountName string) error {
if a.name != accountName {
return fmt.Errorf("Account Name is incorrect")
}
fmt.Println("Account Verified")
return nil
}
type securityCode struct {
code int
}
func newSecurityCode(code int) *securityCode {
return &securityCode{
code: code,
}
}
func (s *securityCode) checkCode(incomingCode int) error {
if s.code != incomingCode {
return fmt.Errorf("Security Code is incorrect")
}
fmt.Println("SecurityCode Verified")
return nil
}
type wallet struct {
balance int
}
func newWallet() *wallet {
return &wallet{
balance: 0,
}
}
func (w *wallet) creditBalance(amount int) {
w.balance += amount
fmt.Println("Wallet balance added successfully")
return
}
func (w *wallet) debitBalance(amount int) error {
if w.balance < amount {
return fmt.Errorf("Balance is not sufficient")
}
fmt.Println("Wallet balance is Sufficient")
w.balance = w.balance - amount
return nil
}
type ledger struct {
}
func (s *ledger) makeEntry(accountID, txnType string, amount int) {
fmt.Printf("Make ledger entry for accountId %s with txnType %s for amount %d\n", accountID, txnType, amount)
return
}
type notification struct {
}
func (n *notification) sendWalletCreditNotification() {
fmt.Println("Sending wallet credit notification")
}
func (n *notification) sendWalletDebitNotification() {
fmt.Println("Sending wallet debit notification")
}
func main() {
fmt.Println()
walletFacade := newWalletFacade("abc", 1234)
fmt.Println()
err := walletFacade.addMoneyToWallet("abc", 1234, 10)
if err != nil {
log.Fatalf("Error: %s\n", err.Error())
}
fmt.Println()
err = walletFacade.deductMoneyFromWallet("abc", 1234, 5)
if err != nil {
log.Fatalf("Error: %s\n", err.Error())
}
}
输出:
Starting create account
Account created
Starting add money to wallet
Account Verified
SecurityCode Verified
Wallet balance added successfully
Sending wallet credit notification
Make ledger entry for accountId abc with txnType credit for amount 10
Starting debit money from wallet
Account Verified
SecurityCode Verified
Wallet balance is Sufficient
Sending wallet debit notification
Make ledger entry for accountId abc with txnType debit for amount 5
- Go 中的设计模式 * Golang 中的设计模式 * 外观 * 外观设计模式 * Go 中的外观设计模式 *