微软Go学习教程(下半部分)
Posted Harris-H
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微软Go学习教程(下半部分)相关的知识,希望对你有一定的参考价值。
微软Go学习教程(下半部分)
https://docs.microsoft.com/zh-cn/learn/modules/go-data-types/4-structs
0.结构
有时,你需要在一个结构中表示字段的集合。 例如,要编写工资核算程序时,需要使用员工数据结构。 在 Go 中,可使用结构将可能构成记录的不同字段组合在一起。
Go 中的结构也是一种数据结构,它可包含零个或多个任意类型的字段,并将它们表示为单个实体。
在本部分,我们将探索结构为何很重要以及如何使用它们。
若要声明结构,需要使用 struct
关键字,还要使用希望新的数据类型具有的字段及其类型的列表。 例如,若要定义员工结构,可使用以下代码:
type Employee struct
ID int
FirstName string
LastName string
Address string
package main
import "fmt"
type Employee struct
ID int
FirstName string
LastName string
Address string
func main()
var john Employee
employee := Employee1001, "John", "Doe", "Doe's Street"
employee1 := EmployeeLastName: "Doe", FirstName: "John"
fmt.Println(john,employee,employee1)
输出
0 1001 John Doe Doe's Street 0 John Doe
请注意从上述声明中看,为每个字段分配值的顺序不重要。 此外,如果未指定任何其他字段的值,也并不重要。 Go 将根据字段数据类型分配默认值。
若要访问结构的各个字段,可使用点表示法 (.
) 做到这一点,如下例所示:
最后,可使用 &
运算符生成指向结构的指针,如以下代码所示:
package main
import "fmt"
type Employee struct
ID int
FirstName string
LastName string
Address string
func main()
employee := EmployeeLastName: "Doe", FirstName: "John"
fmt.Println(employee)
employeeCopy := &employee
employeeCopy.FirstName = "David"
fmt.Println(employee)
结构嵌套
通过 Go 中的结构,可将某结构嵌入到另一结构中。 有时,你需要减少重复并重用一种常见的结构。 例如,假设你想要重构之前的代码,使其具有两种数据类型,一种针对员工,一种针对合同工。 你可具有一个包含公共字段的 Person
结构,如下例所示:
请注意如何在无需指定 Person
字段的情况下访问 Employee
结构中的 FirstName
字段,因为它会自动嵌入其所有字段。 但在你初始化结构时,必须明确要给哪个字段分配值。
package main
import "fmt"
type Person struct
ID int
FirstName string
LastName string
Address string
type Employee struct
Person
ManagerID int
type Contractor struct
Person
CompanyID int
func main()
employee := Employee
Person: Person
FirstName: "John",
,
employee.LastName = "Doe"
fmt.Println(employee.FirstName)
用 JSON 编码和解码结构
最后,可使用结构来对 JSON 中的数据进行编码和解码。 Go 对 JSON 格式提供很好的支持,该格式已包含在标准库包中。
你还可执行一些操作,例如重命名结构中字段的名称。 例如,假设你不希望 JSON 输出显示 FirstName
而只显示 name
,或者忽略空字段, 可使用如下例所示的字段标记:
type Person struct
ID int
FirstName string `json:"name"`
LastName string
Address string `json:"address,omitempty"`
然后,若要将结构编码为 JSON,请使用 json.Marshal
函数。 若要将 JSON 字符串解码为数据结构,请使用 json.Unmarshal
函数。 下例将所有内容组合在一起,将员工数组编码为 JSON,并将输出解码为新的变量:
1.处理错误
编写程序时,需要考虑程序失败的各种方式,并且需要管理失败。 无需让用户看到冗长而混乱的堆栈跟踪错误。 让他们看到有关错误的有意义的信息更好。 正如你所看到的,Go 具有 panic
和 recover
之类的内置函数来管理程序中的异常或意外行为。 但错误是已知的失败,你的程序应该可以处理它们。
Go 的错误处理方法只是一种只需要 if
和 return
语句的控制流机制。 例如,在调用函数以从 employee
对象获取信息时,可能需要了解该员工是否存在。 Go 处理此类预期错误的一贯方法如下所示:
employee, err := getInformation(1000)
if err != nil
// Something is wrong. Do something.
注意 getInformation
函数返回了 employee
结构,还返回了错误作为第二个值。 该错误可能为 nil
。 如果错误为 nil
,则表示成功。 如果错误不是 nil
,则表示失败。 非 nil
错误附带一条错误消息,你可以打印该错误消息,也可以记录该消息(更可取)。 这是在 Go 中处理错误的方式。 下一部分将介绍一些其他策略。
你可能会注意到,Go 中的错误处理要求你更加关注如何报告和处理错误。 这正是问题的关键。 让我们看一些其他示例,以帮助你更好地了解 Go 的错误处理方法。
我们将使用用于结构的代码片段来练习各种错误处理策略:
错误处理策略
func getInformation(id int) (*Employee, error)
employee, err := apiCallEmployee(1000)
if err != nil
return nil, err // Simply return the error to the caller.
return employee, nil
你可能还需要在传播错误之前添加更多信息。 为此,可以使用 fmt.Errorf()
函数,该函数与我们之前看到的函数类似,但它返回一个错误。 例如,你可以向错误添加更多上下文,但仍返回原始错误,如下所示:
func getInformation(id int) (*Employee, error)
employee, err := apiCallEmployee(1000)
if err != nil
return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
return employee, nil
另一种策略是在错误为暂时性错误时运行重试逻辑。 例如,可以使用重试策略调用函数三次并等待两秒钟,如下所示:
func getInformation(id int) (*Employee, error)
for tries := 0; tries < 3; tries++
employee, err := apiCallEmployee(1000)
if err == nil
return employee, nil
fmt.Println("Server is not responding, retrying ...")
time.Sleep(time.Second * 2)
return nil, fmt.Errorf("server has failed to respond to get the employee information")
最后,可以记录错误并对最终用户隐藏任何实现详细信息,而不是将错误打印到控制台。 我们将在下一模块介绍日志记录。 现在,让我们看看如何创建和使用自定义错误。
创建可重用的错误
有时错误消息数会增加,你需要维持秩序。 或者,你可能需要为要重用的常见错误消息创建一个库。 在 Go 中,你可以使用 errors.New()
函数创建错误并在若干部分中重复使用这些错误,如下所示:
var ErrNotFound = errors.New("Employee not found!")
func getInformation(id int) (*Employee, error)
if id != 1001
return nil, ErrNotFound
employee := EmployeeLastName: "Doe", FirstName: "John"
return &employee, nil
getInformation
函数的代码外观更优美,而且如果需要更改错误消息,只需在一个位置更改即可。 另请注意,惯例是为错误变量添加 Err
前缀。
最后,如果你具有错误变量,则在处理调用方函数中的错误时可以更具体。 errors.Is()
函数允许你比较获得的错误的类型,如下所示:
employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound)
fmt.Printf("NOT FOUND: %v\\n", err)
else
fmt.Print(employee)
用于错误处理的推荐做法
在 Go 中处理错误时,请记住下面一些推荐做法:
- 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。
- 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。
- 创建尽可能多的可重用错误变量。
- 了解使用返回错误和 panic 之间的差异。 不能执行其他操作时再使用 panic。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。
- 在记录错误时记录尽可能多的详细信息(我们将在下一部分介绍记录方法),并打印出最终用户能够理解的错误。
2.日志
日志在程序中发挥着重要作用,因为它们是在出现问题时你可以检查的信息源。 通常,发生错误时,最终用户只会看到一条消息,指示程序出现问题。 从开发人员的角度来看,我们需要简单错误消息以外的更多信息。 这主要是因为我们想要再现该问题以编写适当的修补程序。 在本模块中,你将学习日志记录在 Go 中的工作原理。 你还将学习一些应始终实现的做法。
a.log包
对于初学者,Go 提供了一个用于处理日志的简单标准包。 可以像使用 fmt
包一样使用此包。 该标准包不提供日志级别,且不允许为每个包配置单独的记录器。 如果需要编写更复杂的日志记录配置,可以使用记录框架执行此操作。 稍后我们将介绍记录框架。
下面是使用日志的最简单方法:
import (
"log"
)
func main()
log.Print("Hey, I'm a log!")
默认情况下,log.Print()
函数将日期和时间添加为日志消息的前缀。 你可以通过使用 fmt.Print()
获得相同的行为,但使用 log
包还能执行其他操作,例如将日志发送到文件。 稍后我们将详细介绍 log
包功能。
你可以使用 log.Fatal()
函数记录错误并结束程序,就像使用 os.Exit(1)
一样。 使用以下代码片段试一试:
package main
import (
"fmt"
"log"
)
func main()
log.Fatal("Hey, I'm an error log!")
fmt.Print("Can you see me?")
注意最后一行 fmt.Print("Can you see me?")
未运行。 这是因为 log.Fatal()
函数调用停止了该程序。 在使用 log.Panic()
函数时会出现类似行为,该函数也调用 panic()
函数,如下所示:
另一重要函数是 log.SetPrefix()
。 可使用它向程序的日志消息添加前缀。 例如,可以使用以下代码片段:
package main
import (
"log"
)
func main()
log.SetPrefix("main(): ")
log.Print("Hey, I'm a log!")
log.Fatal("Hey, I'm an error log!")
其他函数可在网站上查看:https://golang.org/pkg/log/
记录到文件
除了将日志打印到控制台之外,你可能还希望将日志发送到文件,以便稍后或实时处理这些日志。
为什么想要将日志发送到文件? 首先,你可能想要对最终用户隐藏特定信息。 他们可能对这些信息不感兴趣,或者你可能公开了敏感信息。 在文件中添加日志后,可以将所有日志集中在一个位置,并将它们与其他事件关联。 此模式为典型模式:具有可能是临时的分布式应用程序,例如容器。
让我们使用以下代码测试将日志发送到文件:
package main
import (
"log"
"os"
)
func main()
file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil
log.Fatal(err)
defer file.Close()
log.SetOutput(file)
log.Print("Hey, I'm a log!")
运行前面的代码时,在控制台中看不到任何内容。 在目录中,你应看到一个名为 info.log 的新文件,其中包含使用 log.Print()
函数发送的日志。 请注意,需要首先创建或打开文件,然后将 log
包配置为将所有输出发送到文件。 然后,可以像通常做法那样继续使用 log.Print()
函数。
记录框架
最后,可能有 log
包中的函数不足以处理问题的情况。 你可能会发现,使用记录框架而不编写自己的库很有用。 Go 的几个记录框架有 Logrus、zerolog、zap 和 Apex。
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main()
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Print("Hey! I'm a log message!")
请注意,你只需包含正确的导入名称,然后便可以像通常做法那样继续使用 log.Print()
函数。 另请注意输出更改为 JSON 格式。 在集中位置运行搜索时,JSON 是一种有用的日志格式。
另一有用功能是你可以快速添加上下文数据,如下所示:
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main()
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Debug().
Int("EmployeeID", 1001).
Msg("Getting employee information")
log.Debug().
Str("Name", "John").
Send()
"level":"debug","EmployeeID":1001,"time":1639749764,"message":"Getting employee information"
"level":"debug","Name":"John","time":1639749764
注意我们如何将员工 ID 添加为上下文。 它作为另一属性成为 logline 的一部分。 另外,务必要强调的是,你包含的字段是强类型的。
你可以使用 zerolog 实现其他功能,例如使用分级的日志记录、使用格式化的堆栈跟踪,以及使用多个记录器实例来管理不同输出。 有关详细信息,请参阅
3.使用方法和接口
面向对象编程 (OOP)[Object Oriented Programming]是一种广受欢迎的编程模式,大部分编程语言都支持(至少部分支持)。 Go 是其中一种语言,但它并不完全支持所有 OOP 原则。
在学习路径的这一阶段,你已掌握足够的基础知识,准备好学习和实现封装和组合等原则。
此模块介绍了接口在 Go 中的工作原理,以及接口在 Go 和其他编程语言中的区别。 Go 中的接口是隐式接口,你将通过此模块了解其工作原理。
此模块还介绍了在 Go 中使用接口的方法和原因。
a.使用方法
Go 中的方法是一种特殊类型的函数,但存在一个简单的区别:你必须在函数名称之前加入一个额外的参数。 此附加参数称为 接收方。
如你希望分组函数并将其绑定到自定义类型,则方法非常有用。 Go 中的这一方法类似于在其他编程语言中创建类,因为它允许你实现面向对象编程 (OOP) 模型中的某些功能,例如嵌入、重载和封装。
如要了解方法在 Go 中的重要性,请先学习如何声明一个方法。
声明方法的语法如下所示:
func (variable type) MethodName(parameters ...)
// method functionality
但是,在声明方法之前,必须先创建结构。 假设你想要创建一个几何包,并决定创建一个名为 triangle
的三角形结构作为此程序包的一个组成部分。 然后,你需要使用一种方法来计算此三角形的周长。 你可以在 Go 中将其表示为:
type triangle struct
size int
func (t triangle) perimeter() int
return t.size * 3
结构看起来像普通结构,但 perimeter()
函数在函数名称之前有一个类型 triangle
的额外参数。 也就是说,在使用结构时,你可以按如下方式调用函数:
func main()
t := triangle3
fmt.Println("Perimeter:", t.perimeter())
如果尝试按平常的方式调用 perimeter()
函数,则此函数将无法正常工作,因为此函数的签名表明它需要接收方。 正因如此,调用此方法的唯一方式是先声明一个结构,获取此方法的访问权限。 这也意味着,只要此方法属于不同的结构,你甚至可以为其指定相同的名称。 例如,你可以使用 perimeter()
函数声明一个 square
结构,具体如下所示:
package main
import "fmt"
type triangle struct
size int
type square struct
size int
func (t triangle) perimeter() int
return t.size * 3
func (s square) perimeter() int
return s.size * 4
func main()
t := triangle3
s := square4
fmt.Println("Perimeter (triangle):", t.perimeter())
fmt.Println("Perimeter (square):", s.perimeter())
通过对 perimeter()
函数的两次调用,编译器将根据接收方类型来确定要调用的函数。 这有助于在各程序包之间保持函数的一致性和名称的简短,并避免将包名称作为前缀。 在下一个单元讲解接口时,我们将介绍这样做的重要性。
方法中的指针
有时,方法需要更新变量,或者,如果参数太大,则可能需要避免复制它。 在遇到此类情况时,你需要使用指针传递变量的地址。 在之前的模块中,当我们在讨论指针时提到,每次在 Go 中调用函数时,Go 都会复制每个参数值以便使用。
如果你需要更新方法中的接收方变量,也会执行相同的行为。 例如,假设你要创建一个新方法以使三角形的大小增加一倍。 你需要在接收方变量中使用指针,具体如下所示:
func (t *triangle) doubleSize()
t.size *= 2
如果方法仅可访问接收方的信息,则不需要在接收方变量中使用指针。 但是,依据 Go 的约定,如果结构的任何方法具有指针接收方,则此结构的所有方法都必须具有指针接收方,即使某个方法不需要也是如此。
声明其他类型的方法
方法的一个关键方面在于,需要为任何类型定义方法,而不只是针对自定义类型(如结构)进行定义。 但是,你不能通过属于其他包的类型来定义结构。 因此,不能在基本类型(如 string
)上创建方法。
尽管如此,你仍然可以利用一点技巧,基于基本类型创建自定义类型,然后将其用作基本类型。 例如,假设你要创建一个方法,以将字符串从小写字母转换为大写字母。 你可以按如下所示写入方法:
package main
import (
"fmt"
"strings"
)
type upperstring string
func (s upperstring) Upper() string
return strings.ToUpper(string(s))
func main()
s := upperstring("Learning Go!")
fmt.Println(s)
fmt.Println(s.Upper())
嵌入方法
在之前的模块中,您已了解到可以在一个结构中使用属性,并将同一属性嵌入另一个结构中。 也就是说,可以重用来自一个结构的属性,以避免出现重复并保持代码库的一致性。 类似的观点也适用于方法。 即使接收方不同,也可以调用已嵌入结构的方法。
例如,假设你想要创建一个带有逻辑的新三角形结构,以加入颜色。 此外,你还希望继续使用之前声明的三角形结构。 因此,彩色三角形结构将如下所示:
type coloredTriangle struct
triangle
color string
如果你熟悉 Java 或等 C++ 等 OOP 语言,则可能会认为 triangle
结构看起来像基类, coloredTriangle
像子类(如继承),但并不完全如此。 实际上,Go 编译器会通过创建如下的包装器方法来推广 perimeter()
方法:
func (t coloredTriangle) perimeter() int
return t.triangle.perimeter()
请注意,接收方是 coloredTriangle
,它从三角形字段调用 perimeter()
方法。 好的一点在于,你不必再创建之前的方法。 你可以选择创建,但 Go 已在内部为你完成了此工作。 我们提供的上述示例仅供学习。
重载方法
让我们回到之前讨论过的 triangle
示例。 如果要在 coloredTriangle
结构中更改 perimeter()
方法的实现,会发生什么情况? 不能存在两个同名的函数。 但是,因为方法需要额外参数(接收方),所以,你可以使用一个同名的方法,只要此方法专门用于要使用的接收方即可。 这就是重载方法的方式。
换而言之,如你想要更改其行为,可以编写我们刚才讨论过的包装器方法。 如果彩色三角形的周长是普通三角形的两倍,则代码将如下所示:
func (t coloredTriangle) perimeter() int
return t.size * 3 * 2
方法中的封装
“封装”表示对象的发送方(客户端)无法访问某个方法。 通常,在其他编程语言中,你会将 private
或 public
关键字放在方法名称之前。 在 Go 中,只需使用大写标识符,即可公开方法,使用非大写的标识符将方法设为私有方法。
Go 中的封装仅在程序包之间有效。 换句话说,你只能隐藏来自其他程序包的实现详细信息,而不能隐藏程序包本身。
如要进行尝试,请创建新程序包 geometry
并按如下方式将三角形结构移入其中:
package geometry
type Triangle struct
size int
func (t *Triangle) doubleSize()
t.size *= 2
func (t *Triangle) SetSize(size int)
t.size = size
func (t *Triangle) Perimeter() int
t.doubleSize()
return t.size * 3
你可以使用上述程序包,具体如下所示:
func main()
t := geometry.Triangle
t.SetSize(3)
fmt.Println("Perimeter", t.Perimeter())
如要尝试从 main()
函数中调用 size
字段或 doubleSize()
方法,程序将死机,如下所示:
func main()
t := geometry.Triangle
t.SetSize(3)
fmt.Println("Size", t.size)
fmt.Println("Perimeter", t.Perimeter())
在运行前面的代码时,你将看到以下错误:
./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)
b.接口
Go 中的接口是一种用于表示其他类型的行为的数据类型。 接口类似于对象应满足的蓝图或协定。 在你使用接口时,你的基本代码将变得更加灵活、适应性更强,因为你编写的代码未绑定到特定的实现。 因此,你可以快速扩展程序的功能。 在本模块,你将了解相关原因。
与其他编程语言中的接口不同,Go 中的接口是满足隐式实现的。 Go 并不提供用于实现接口的关键字,因此,如果你之前使用的是其他编程语言中的接口,但不熟悉 Go,那么此概念可能会造成混淆。
在此模块中,我们将使用多个示例来探讨 Go 中的接口,并演示如何充分利用这些接口。
声明接口
Go 中的接口是一种抽象类型,只包括具体类型必须拥有或实现的方法。 正因如此,我们说接口类似于蓝图。
假设你希望在几何包中创建一个接口来指示形状必须实现的方法。 你可以按如下所示定义接口:
type Square struct
size float64
func (s Square) Area() float64
return s.size * s.size
func (s Square) Perimeter() float64
return s.size * 4
请注意 Square
结构的方法签名与 Shape
接口的签名的匹配方式。 但是,另一个接口可能具有不同的名称,但方法相同。 Go 如何或何时知道某个具体类型正在实现哪个接口? 在运行时,Go 会知道你何时使用了接口。
如要演示如何使用接口,你可以编写以下内容:
func main()
var s Shape = Square3
fmt.Printf("%T\\n", s)
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
此时,无论你是否使用接口,都没有任何区别。 接下来,让我们创建另一种类型,如 Circle
,然后探讨接口有用的原因。 以下是 Circle
结构的代码:
type Circle struct
radius float64
func (c Circle) Area() float64
<以上是关于微软Go学习教程(下半部分)的主要内容,如果未能解决你的问题,请参考以下文章