通过示例学习-Go-语言-2023-十九-
通过示例学习 Go 语言 2023(十九)
Go 中的接口(Golang)。
这是 Go 语言综合教程系列的第二十一章。请参考此链接获取系列的其他章节 – Golang 综合教程系列。
下一个教程 – Iota。
上一个教程 – 方法。
现在让我们看看当前的教程。下面是当前教程的目录。
目录。
概述。
-
实现接口。
-
接口是隐式实现的。
-
接口类型作为函数参数。
-
为什么使用接口。
-
指针接收者在实现接口时。
-
非结构体自定义类型实现接口。
-
类型实现多个接口。
-
接口的零值。
-
接口的内部工作原理。
-
嵌入接口。
-
在其他接口中嵌入接口。
-
在结构体中嵌入接口。
-
-
访问接口的底层变量。
-
类型断言。
-
类型切换。
-
-
空接口。
-
结论
概述
接口是 Go 中的一种类型,它是方法签名的集合。这些方法签名的集合旨在表示某种行为。接口只声明方法集,任何实现了接口所有方法的类型都是该接口类型。
接口让你在 Golang 中使用鸭子类型。现在,什么是鸭子类型?
鸭子类型是一种计算机编程方式,让你进行鸭子测试,我们不检查类型,而是只检查某些属性或方法的存在。因此,真正重要的是对象是否具有某些属性和方法,而不是它的类型。
鸭子类型源于以下短语。
If it walks like a duck and quack like a duck then it must be duck
再次回到接口。那么,什么是接口?正如之前提到的,它是方法签名的集合。它定义了一个类型可能拥有的确切方法集。下面是一个接口的签名,它仅包含方法签名。
type name_of_interface interface{
//Method signature 1
//Method signature 2
}
让我们通过一个例子来理解这个概念。这样会更加清晰。我们定义一个名为动物的接口。动物接口有两个方法呼吸和行走。它仅定义方法签名,而没有其他内容。
type animal interface {
breathe()
walk()
}
方法签名包括
-
方法的名称
-
参数的数量和每个参数的类型
-
返回值的数量和每个返回值的类型
根据上述声明,我们创建了一个新的接口类型,即动物。定义一个动物类型的变量是可以的。
让我们创建一个动物接口类型的变量。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
func main() {
var a animal
fmt.Println(a)
}
输出
nil
如上程序所示,创建一个接口类型的变量是可以的。它打印 nil,因为接口的默认零值是 nil。
实现接口
任何实现了呼吸和行走方法的类型都被称为实现了动物接口。所以如果我们定义一个狮子结构体并实现呼吸和行走方法,那么它就会实现动物接口。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
func main() {
var a animal
a = lion{age: 10}
a.breathe()
a.walk()
}
输出
Lion breathes
Lion walk
我们声明一个动物接口类型的变量。
var a animal
然后我们将一个狮子结构体的实例赋值给它。
a = lion{}
将狮子结构体的一个实例分配给动物接口类型的变量是可行的,因为狮子结构体实现了呼吸和行走这两个方法。在这个赋值过程中并不会检查类型,而只需检查被分配的类型是否实现了呼吸和行走这两个方法。这个概念类似于鸭子类型,狮子能够像动物一样呼吸和行走,因此它就是一种动物。
如果你注意到,并没有明确声明狮子类型实现了动物接口。这带来了与接口相关的一个非常重要的属性——“接口是隐式实现的”。
接口是隐式实现的
并没有明确声明一个类型实现了一个接口。实际上,在 Go 中并不存在类似于 Java 的“implements”关键字。如果一个类型实现了接口的所有方法,它就实现了该接口。
如上所见,定义一个接口类型的变量是正确的,并且如果具体类型实现了接口的所有方法,我们可以将任何具体类型的值分配给这个变量。
并没有明确声明狮子结构体实现了动物接口。在编译期间,Go 会注意到狮子结构体实现了动物接口的所有方法,因此这是允许的。任何实现了动物接口所有方法的其他类型都成为该接口类型。
让我们看看另一个类型实现动物接口的更复杂的例子。
如果我们定义一个狗结构并且它实现了呼吸和行走方法,那么它也将是动物。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
type dog struct {
age int
}
func (l dog) breathe() {
fmt.Println("Dog breathes")
}
func (l dog) walk() {
fmt.Println("Dog walk")
}
func main() {
var a animal
a = lion{age: 10}
a.breathe()
a.walk()
a = dog{age: 5}
a.breathe()
a.walk()
}
输出
Lion breathes
Lion walk
Dog breathes
Dog walk
狮子和狗都实现了呼吸和行走方法,因此它们属于动物类型,并且可以正确地分配给接口类型的变量。
接口变量 a 最初被分配为狮子实例,然后同一变量被分配为狗实例。因此,接口变量所引用的类型是动态的。它动态地持有对底层类型的引用。
需要注意的两个重要点:
- 接口静态检查是在编译时进行的——这意味着如果一个类型没有实现接口的所有方法,则将该类型实例分配给该接口类型的变量将在编译时引发错误。例如,在删除定义在狮子结构上的行走方法时,下面的错误将在赋值过程中被引发。
cannot use lion literal (type lion) as type animal in assignment:
- 根据实例的类型,在运行时调用正确的方法——这意味着根据接口变量引用的是狮子实例还是狗实例来调用相应的方法。如果它引用的是狮子实例,则调用狮子的方法;如果它引用的是狗实例,则调用狗的方法。这也从输出中得到了验证。这是在 Go 中实现运行时多态性的一种方式。
还需注意,类型定义的方法应与接口中方法的整个签名匹配,即应匹配。
-
方法的名称
-
参数的数量和每个参数的类型
-
返回值的数量和每个返回值的类型。
想象一下,动物接口还有另一个方法速度,返回动物速度的 int 值。
type animal interface {
breathe()
walk()
speed() int
}
如果狮子结构具有如下的速度方法,但不返回 int 值,则狮子结构将不实现动物接口。
func (l lion) speed()
将狮子实例分配给动物类型的变量时将引发下面的编译错误。
cannot use lion literal (type lion) as type animal in assignment:
lion does not implement animal (wrong type for speed method)
have speed()
want speed() int
因此,从本质上讲,方法签名在实现接口时是重要的。
作为函数参数的接口类型
函数可以接受接口类型的参数。任何实现该接口的类型都可以作为该参数传递给该函数。例如,在下面的代码中,我们有callBreathe和callWalk函数,它们接受动物接口类型的参数。狮子和狗实例都可以传递给这个函数。我们创建狮子和狗类型的实例,并将其传递给函数。
它的工作方式类似于我们上面讨论的赋值。在编译过程中,调用函数时不会检查类型,而是只需检查传递给函数的类型是否实现了呼吸和行走方法。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
type dog struct {
age int
}
func (l dog) breathe() {
fmt.Println("Dog breathes")
}
func (l dog) walk() {
fmt.Println("Dog walk")
}
func main() {
l := lion{age: 10}
callBreathe(l)
callWalk(l)
d := dog{age: 5}
callBreathe(d)
callWalk(d)
}
func callBreathe(a animal) {
a.breathe()
}
func callWalk(a animal) {
a.breathe()
}
输出
Lion breathes
Lion walk
Dog breathes
Dog walk
在上面的代码中,我们有callBreathe和callWalk函数,它们接受一个animal接口类型的参数。lion和dog实例都可以传递给这个函数。我们创建了lion和dog类型的实例并将其传递给函数。在编译期间,调用函数时不检查类型,而只需检查传递给函数的类型是否实现了breathe和walk方法。
为什么使用接口
下面是使用接口的一些好处。
- 有助于在代码库的不同部分之间编写更模块化和解耦的代码——它可以帮助减少代码库不同部分之间的依赖,并提供松耦合。
比如想象一个与数据库层交互的应用程序。如果该应用程序通过接口与数据库交互,那么它永远不会知道后台使用的是哪种数据库。你可以在后台更改数据库的类型,比如从 arango db 更改为 mongo db,而应用层无需任何更改,因为它通过实现该接口的 arango db 和 mongo db 与数据库层交互。
- 接口可以用于实现 golang 中的运行时多态性。运行时多态性意味着调用在运行时被解析。让我们通过一个示例了解如何使用接口实现运行时多态性。
不同国家有不同的税收计算方式。这可以通过接口来表示。
type taxCalculator interface{
calculateTax()
}
现在不同国家可以有自己的结构体并实现calculateTax()方法。同样的calculateTax方法在不同上下文中用于计算税。编译器在看到这个调用时,会延迟确定在运行时调用哪个确切的方法。
package main
import "fmt"
type taxSystem interface {
calculateTax() int
}
type indianTax struct {
taxPercentage int
income int
}
func (i *indianTax) calculateTax() int {
tax := i.income * i.taxPercentage / 100
return tax
}
type singaporeTax struct {
taxPercentage int
income int
}
func (i *singaporeTax) calculateTax() int {
tax := i.income * i.taxPercentage / 100
return tax
}
type usaTax struct {
taxPercentage int
income int
}
func (i *usaTax) calculateTax() int {
tax := i.income * i.taxPercentage / 100
return tax
}
func main() {
indianTax := &indianTax{
taxPercentage: 30,
income: 1000,
}
singaporeTax := &singaporeTax{
taxPercentage: 10,
income: 2000,
}
taxSystems := []taxSystem{indianTax, singaporeTax}
totalTax := calculateTotalTax(taxSystems)
fmt.Printf("Total Tax is %d\n", totalTax)
}
func calculateTotalTax(taxSystems []taxSystem) int {
totalTax := 0
for _, t := range taxSystems {
totalTax += t.calculateTax() //This is where runtime polymorphism happens
}
return totalTax
}
输出:
Total Tax is 300
现在下面是运行时多态性发生的地方。
totalTax += t.calculateTax() //This is where runtime polymorphism happens
正确的calculateTax()方法根据实例是否为singaporeTax结构体税或indianTax结构体税被调用。
使用指针接收器实现接口
类型的方法可以具有指针接收器或值接收器。在上述示例中,我们只使用了值接收器。需要注意的是,指针接收器也可以用来实现接口。但这里有一个警告。
-
如果一个类型使用值接收器实现接口的所有方法,那么在将该类型的变量或该类型变量的指针赋值给接口或传递给接受该接口参数的函数时,两者都可以使用。
-
如果一个类型使用指针接收器实现接口的所有方法,那么在将该类型的变量赋值给接口或传递给接受该接口参数的函数时,只有该变量的指针可以使用。
示例以演示上述第一点
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
func main() {
var a animal
a = lion{age: 10}
a.breathe()
a.walk()
a = &lion{age: 5}
a.breathe()
a.walk()
}
输出
Lion breathes
Lion walk
Lion breathes
Lion walk
狮子结构体通过值接收器实现了动物接口。因此,它适用于狮子类型的变量和指向狮子类型变量的指针。
这可以工作。
a = lion{age: 10}
还有这一点。
a = &lion{age: 5}
示例以演示上述第二点。狮子结构体通过指针接收器实现了动物接口。因此,它仅适用于指向狮子类型变量的指针。
所以这可以工作。
a = &lion{age: 5}
但这会引发编译错误。
a = lion{age: 10}
cannot use lion literal (type lion) as type animal in assignment:
lion does not implement animal (breathe method has pointer receiver)
查看完整的工作代码。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l *lion) breathe() {
fmt.Println("Lion breathes")
}
func (l *lion) walk() {
fmt.Println("Lion walk")
}
func main() {
var a animal
//a = lion{age: 10}
a.breathe()
a.walk()
a = &lion{age: 5}
a.breathe()
a.walk()
}
取消注释该行。
a = lion{age: 10}
这也会引发编译错误。
cannot use lion literal (type lion) as type animal in assignment:
lion does not implement animal (breathe method has pointer receiver)
非结构自定义类型实现接口
到目前为止,我们只看到了结构类型实现接口的例子。任何非结构自定义类型实现接口也是完全可以的。让我们看一个例子。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type cat string
func (c cat) breathe() {
fmt.Println("Cat breathes")
}
func (c cat) walk() {
fmt.Println("Cat walk")
}
func main() {
var a animal
a = cat("smokey")
a.breathe()
a.walk()
}
输出
Cat breathes
Cat walk
上面的程序说明了任何自定义类型也可以实现接口的概念。猫是字符串类型,它实现了呼吸和行走方法,因此将猫类型的实例赋值给动物类型的变量是正确的。
类型实现多个接口
如果一个类型定义了接口的所有方法,则该类型实现了该接口。如果它定义了另一个接口的所有方法,那么它也实现了那个接口。从本质上讲,一个类型可以实现多个接口。
在下面的程序中,我们有一个哺乳动物接口,具有一个进食方法。狮子结构体也定义了这个方法,因此它实现了哺乳动物接口。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type mammal interface {
feed()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
func (l lion) feed() {
fmt.Println("Lion feeds young")
}
func main() {
var a animal
l := lion{}
a = l
a.breathe()
a.walk()
var m mammal
m = l
m.feed()
}
输出
Lion breathes
Lion walk
Lion feeds young
接口的零值
接口的默认或零值是 nil。下面的程序演示了这一点。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
func main() {
var a animal
fmt.Println(a)
}
输出
nil
接口的内部工作原理
像其他变量一样,接口变量由类型和值表示。接口值在底层由两个元组组成。
-
底层类型
-
底层值
请看下面的图示,说明了我们上面提到的内容。
例如,狮子结构体实现动物接口如下。
Golang 提供了格式标识符,以打印由接口值表示的底层类型和底层值。
-
%T 可以用来打印接口值的具体类型。
-
%v 可以用来打印接口值的具体值。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
func main() {
var a animal
a = lion{age: 10}
fmt.Printf("Underlying Type: %T\n", a)
fmt.Printf("Underlying Value: %v\n", a)
}
输出
Concrete Type: main.lion
Concrete Value: {10}
接口可以嵌入其他接口,也可以嵌入结构体。让我们逐一看看。
嵌入接口
接口可以嵌入其他接口,也可以嵌入结构体。让我们逐一看看。
在其他接口中嵌入接口
一个接口可以嵌入任意数量的接口,也可以嵌入任何接口。嵌入接口的所有方法都成为嵌入接口的一部分。这是通过合并一些小接口来创建新接口的一种方式。让我们通过一个例子来理解。
假设我们有一个接口动物如下。
type animal interface {
breathe()
walk()
}
假设还有一个名为 human 的接口,它嵌入了 animal 接口。
type human interface {
animal
speak()
}
因此,如果任何类型需要实现 human 接口,则必须定义
-
breathe() 和 walk() 方法的动物接口嵌入在 human 中
-
speak() 方法的人类接口
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type human interface {
animal
speak()
}
type employee struct {
name string
}
func (e employee) breathe() {
fmt.Println("Employee breathes")
}
func (e employee) walk() {
fmt.Println("Employee walk")
}
func (e employee) speak() {
fmt.Println("Employee speaks")
}
func main() {
var h human
h = employee{name: "John"}
h.breathe()
h.walk()
h.speak()
}
输出
Employee breathes
Employee walk
Employee speaks
作为另一个示例,golang 的 io 包的 ReaderWriter 接口 (golang.org/pkg/io/#ReadWriter
) 嵌入了两个其他接口。
-
reader 接口 –
golang.org/pkg/io/#Reader
-
writer 接口 –
golang.org/pkg/io/#Writer
type ReadWriter interface {
Reader
Writer
}
在结构中嵌入接口
接口也可以嵌入到结构中。所有嵌入接口的方法都可以通过该结构调用。这些方法的调用方式取决于嵌入接口是命名字段还是未命名/匿名字段。
-
如果嵌入接口是命名字段,则接口方法必须通过命名接口名称调用
-
如果嵌入接口是未命名/匿名字段,则可以直接或通过接口名称引用接口方法
让我们来看一个程序,说明上述要点
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type dog struct {
age int
}
func (d dog) breathe() {
fmt.Println("Dog breathes")
}
func (d dog) walk() {
fmt.Println("Dog walk")
}
type pet1 struct {
a animal
name string
}
type pet2 struct {
animal
name string
}
func main() {
d := dog{age: 5}
p1 := pet1{name: "Milo", a: d}
fmt.Println(p1.name)
// p1.breathe()
// p1.walk()
p1.a.breathe()
p1.a.walk()
p2 := pet2{name: "Oscar", animal: d}
fmt.Println(p1.name)
p2.breathe()
p2.walk()
p1.a.breathe()
p1.a.walk()
}
输出
Milo
Dog breathes
Dod walk
Oscar
Dog breathes
Dog walk
Dog breathes
Dog walk
我们声明了两个结构 pet1 和 pet2。 pet1 结构中有命名的 animal 接口
type pet1 struct {
a animal
name string
}
pet2 嵌入了未命名/匿名 animal 接口
type pet2 struct {
animal
name string
}
对于 pet1 结构的实例,我们可以这样调用 breathe() 和 walk() 方法。
p1.a.breathe()
p1.a.walk()
直接调用这些方法将引发编译错误
p1.breathe()
p1.walk()
p1.breathe undefined (type pet1 has no field or method breathe)
p1.walk undefined (type pet1 has no field or method walk)
对于 pet2 结构的实例,我们可以直接调用 breathe() 和 walk() 方法
p2.breathe()
p2.walk()
如果嵌入接口是匿名或未命名的,我们可以直接访问嵌入接口的方法。
下面也是有效的,另一种调用未命名/匿名嵌入接口方法的方式
p2.animal.breathe()
p2.animal.walk()
还请注意,在创建 pet1 或 pet2 结构的实例时,嵌入的接口 animal 是用实现该接口的类型 dog 初始化的。
p1 := pet1{name: "Milo", a: d}
p2 := pet2{name: "Oscar", animal: d}
如果我们不初始化嵌入接口 animal,则它将被初始化为接口的零值,即 nil。在这样的 pet1 或 pet2 结构的实例上调用 breathe() 和 walk() 方法将导致恐慌。
访问接口的底层变量
可以通过两种方式访问底层变量
-
类型断言
-
类型切换
类型断言
类型断言提供了一种通过断言底层值的正确类型来访问接口值内部的底层变量的方法。下面是其语法,其中 i 是一个接口。
val := i.({type})
上述声明断言接口中的底层值的类型为 {type}。如果这个断言成立,则将底层值分配给 val。如果不成立,则上述声明将导致恐慌。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
type dog struct {
age int
}
func (d dog) breathe() {
fmt.Println("Dog breathes")
}
func (d dog) walk() {
fmt.Println("Dog walk")
}
func main() {
var a animal
a = lion{age: 10}
print(a)
}
func print(a animal) {
l := a.(lion)
fmt.Printf("Age: %d\n", l.age)
//d := a.(dog)
//fmt.Printf("Age: %d\n", d.age)
}
输出
Age: 10
这就是我们如何断言变量 a 的类型 animal 为底层类型 lion。
l := a.(lion)
下面的行将引发程序崩溃,因为底层类型是狮子而不是狗。取消注释该行以查看效果。
//d := a.(dog)
类型断言提供了获取底层值的另一种方法,同时也能防止程序崩溃。其语法为:
val, ok := i.(<type>)</type>
在这种情况下,类型断言返回两个值,第一个值与上面讨论的相同,另一个值是布尔值,指示类型断言是否正确。这个值是:
-
如果类型断言正确,则返回 true,意味着断言的类型与底层类型相同。
-
如果类型断言失败,则返回 false。
所以第二种方法是一种良好的类型断言方式,因为它可以防止程序崩溃。让我们来看一个例子。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
type dog struct {
age int
}
func (d dog) breathe() {
fmt.Println("Dog breathes")
}
func (d dog) walk() {
fmt.Println("Dog walk")
}
func main() {
var a animal
a = lion{age: 10}
print(a)
}
func print(a animal) {
l, ok := a.(lion)
if ok {
fmt.Println(l)
} else {
fmt.Println("a is not of type lion")
}
d, ok := a.(dog)
if ok {
fmt.Println(d)
} else {
fmt.Println("a is not of type lion")
}
}
输出:
{10}
a is not of type lion
现在让我们继续讨论类型开关。
类型开关
类型开关使我们能够连续进行上述类型断言。请参见下面的代码示例。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
type dog struct {
age int
}
func (d dog) breathe() {
fmt.Println("Dog breathes")
}
func (d dog) walk() {
fmt.Println("Dog walk")
}
func main() {
var a animal
x = lion{age: 10}
print(x)
}
func print(a animal) {
switch v := a.(type) {
case lion:
fmt.Println("Type: lion")
case dog:
fmt.Println("Type: dog")
default:
fmt.Printf("Unknown Type %T", v)
}
}
输出:
Type: lion
在上面的代码中,使用类型开关我们可以确定接口变量 x 中包含的值的类型是狮子、狗或其他某种类型。也可以在 case 语句中添加更多不同的类型。
空接口
空接口没有方法,因此默认情况下,所有具体类型都实现空接口。如果你编写一个接受空接口的函数,那么你可以将任何类型传递给该函数。请参见下面的工作代码。
package main
import "fmt"
func main() {
test("thisisstring")
test("10")
test(true)
}
func test(a interface{}) {
fmt.Printf("(%v, %T)\n", a, a)
}
输出
(thisisstring, string)
(10, string)
(true, bool)
结论
这就是 Go 语言中的接口。希望你喜欢这篇文章。请在评论中分享反馈/改进建议/错误。
下一教程 – Iota
上一教程 – 方法
Go(Golang)中的结构体接口
在编程中,我们有时会遇到一个空接口内部可能包含一个结构体的情况,我们需要从中提取出具体的结构体。对于不清楚空接口是什么的人,应该阅读这篇优秀的文章:research.swtch.com/interfaces
。
为了将 interface{}转换为结构体,我们将使用这个库 – github.com/mitchellh/mapstructure
。让我们通过一个示例来理解如何将接口转换为结构体:
package main
import (
"fmt"
"github.com/mitchellh/mapstructure"
)
type NewCustomerEvent struct {
Name string
Phone string
Email string
}
func main() {
newCustomer := NewCustomerEvent{Name: "x", Phone: "082213909101", Email: "xyz@gmail.com"}
convert(newCustomer)
}
func convert(event interface{}) {
c := NewCustomerEvent{}
mapstructure.Decode(event, &c)
fmt.Printf("Event is: %v", c)
}
输出:
Event is: {x 082213909101 xyz@gmail.com}
在 Go(Golang)中,接口是隐式实现的。
来源:
golangbyexample.com/interface-implit-implementation-golanng/
并没有明确声明一个类型实现了一个接口。实际上,在 Go 中并不存在类似 Java 的 “implements” 关键字。如果一个类型实现了接口的所有方法,那么它就实现了这个接口。
让我们来看一个示例。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
func main() {
var a animal
a = lion{age: 10}
a.breathe()
a.walk()
}
输出
Lion breathes
Lion walk
没有明确声明 狮子 结构体实现了 动物 接口。在编译时,Go 会注意到 狮子 结构体实现了 动物 接口的所有方法,因此是被允许的。任何其他实现了 动物 接口所有方法的类型都成为该接口类型。
这在接口和定义接口所有方法的类型位于不同包时也同样成立。
让我们看看另一个类型实现动物接口的更复杂示例。如果我们定义一个 狗 结构体,并且它实现了 呼吸 和 行走 方法,那么它也将成为一种动物。让我们来看一个示例。
package main
import "fmt"
type animal interface {
breathe()
walk()
}
type lion struct {
age int
}
func (l lion) breathe() {
fmt.Println("Lion breathes")
}
func (l lion) walk() {
fmt.Println("Lion walk")
}
type dog struct {
age int
}
func (l dog) breathe() {
fmt.Println("Dog breathes")
}
func (l dog) walk() {
fmt.Println("Dog walk")
}
func main() {
var a animal
a = lion{age: 10}
a.breathe()
a.walk()
a = dog{age: 5}
a.breathe()
a.walk()
}
输出
Lion breathes
Lion walk
Dog breathes
Dog walk
狮子 和 狗 都实现了 呼吸 和 行走 方法,因此它们属于 动物 类型,可以正确地赋值给接口类型的变量。
Go 语言中的交错字符串程序
目录
-
概述
-
递归解决方案
-
动态规划解决方案
概述
给定三个字符串s1、s2、s3。判断字符串s3是否为字符串的交错。
如果满足以下条件,s3将是字符串s1和s2的交错。
- s3 包含s1和s2的所有字符,并且每个字符串中的所有字符顺序保持不变。
示例
s1: aabcc
s2: dbbca
s3: aadbbcbcac
Output: true
递归解决方案
以下是相同的递归解决方案
package main
import "fmt"
func main() {
output := isInterleave("aabcc", "dbbca", "aadbbcbcac")
fmt.Println(output)
output = isInterleave("", "", "")
fmt.Println(output)
}
func isInterleave(s1 string, s2 string, s3 string) bool {
s1Rune := []rune(s1)
s2Rune := []rune(s2)
s3Rune := []rune(s3)
lenS1 := len(s1Rune)
lenS2 := len(s2Rune)
lenS3 := len(s3Rune)
if lenS1+lenS2 != lenS3 {
return false
}
return isInterleaveUtil(s1Rune, s2Rune, s3Rune, 0, 0, 0, lenS1, lenS2, lenS3)
}
func isInterleaveUtil(s1, s2, s3 []rune, x, y, z, lenS1, lenS2, lenS3 int) bool {
if x == lenS1 && y == lenS2 && z == lenS3 {
return true
}
if x < lenS1 && z < lenS3 && s1[x] == s3[z] {
match := isInterleaveUtil(s1, s2, s3, x+1, y, z+1, lenS1, lenS2, lenS3)
if match {
return true
}
}
if y < lenS2 && z < lenS3 && s2[y] == s3[z] {
return isInterleaveUtil(s1, s2, s3, x, y+1, z+1, lenS1, lenS2, lenS3)
}
return false
}
输出
true
true
如果你注意到上述程序,许多子问题被反复计算,因此上述解决方案的复杂性是指数级的。因此,我们也可以在这里使用动态规划来减少整体时间复杂度。
这是相同程序的代码
动态规划解决方案
package main
import "fmt"
func main() {
output := isInterleave("aabcc", "dbbca", "aadbbcbcac")
fmt.Println(output)
output = isInterleave("", "", "")
fmt.Println(output)
}
func isInterleave(s1 string, s2 string, s3 string) bool {
s1Rune := []rune(s1)
s2Rune := []rune(s2)
s3Rune := []rune(s3)
lenS1 := len(s1Rune)
lenS2 := len(s2Rune)
lenS3 := len(s3Rune)
if lenS1+lenS2 != lenS3 {
return false
}
interleavingMatrix := make([][]bool, lenS1+1)
for i := range interleavingMatrix {
interleavingMatrix[i] = make([]bool, lenS2+1)
}
i := 1
k := 1
interleavingMatrix[0][0] = true
for i <= lenS1 && k <= lenS3 {
if s1Rune[i-1] == s3Rune[k-1] {
interleavingMatrix[i][0] = true
i++
k++
} else {
break
}
}
i = 1
k = 1
for i <= lenS2 && k <= lenS3 {
if s2Rune[i-1] == s3Rune[k-1] {
interleavingMatrix[0][i] = true
i++
k++
} else {
break
}
}
for i := 1; i <= lenS1; i++ {
for j := 1; j <= lenS2; j++ {
if s1Rune[i-1] == s3Rune[i+j-1] {
interleavingMatrix[i][j] = interleavingMatrix[i-1][j]
}
if s2Rune[j-1] == s3Rune[i+j-1] && !interleavingMatrix[i][j] {
interleavingMatrix[i][j] = interleavingMatrix[i][j-1]
}
}
}
return interleavingMatrix[lenS1][lenS2]
}
输出
true
true
注意:查看我们的 Golang 高级教程。本系列的教程详细且我们尽力涵盖所有概念及示例。本教程适合希望获得 Golang 专业知识和扎实理解的人 - Golang 高级教程
如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是,那么这篇文章适合你 - 所有设计模式 Golang
Go 中的 IOTA(Golang)
这是 Golang 综合教程系列的第二十二章。请参考此链接以获取该系列的其他章节– Golang 综合教程系列
下一个教程– Goroutines
上一个教程– 接口
现在让我们看看当前的教程。下面是当前教程的目录。
目录
概述
-
更多关于 IOTA 的信息
-
Golang 中的枚举
-
结论
概述
Iota 是一个标识符,用于常量,能够简化使用自动递增数字的常量定义。IOTA关键字表示从零开始的整数常量。因此,它可以用于在 Go 中创建有效的常量。它们还可以用于在 Go 中创建枚举,正如我们在本教程后面将看到的那样。
没有 IOTA 的自动递增常量。
const (
a = 0
b = 1
c = 2
)
带 IOTA 的自动递增常量。
const (
a = iota
b
c
)
两者将设置。
a=0
b=1
c=2
所以 IOTA 是。
-
一个从零开始的计数器。
-
每行增加 1。
-
仅用于常量。
IOTA 从零开始,每行增加 1,但也有一些注意事项。首先,让我们看一个简单的例子,其中 iota 从零开始并在每一行后递增 1。
package main
import "fmt"
const (
a = iota
b
c
)
func main() {
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
输出
0
1
2
Iota 将a的值设置为零。然后在每一行上递增该值 1。因此,输出为 0,接着是 1,再接着是 2。
更多关于 IOTA 的信息
让我们看看与 iota 相关的其他几点。
- iota 关键字也可以在每一行上使用。在这种情况下,iota 将从零开始并在每一行递增。这将与上述情况相同。
const (
a = iota
b = iota
c = iota
)
将输出。
0
1
2
- iota 关键字也可以跳过。在这种情况下,iota 将从零开始并在每一行递增。这与上述两种情况相同。
const (
a = iota
b
c = iota
)
将输出。
0
1
2
- 如果存在空行或注释行,则不会递增。
const (
a = iota
b
//comment
c
)
将输出。
0
1
2
- 如果再次使用 const 关键字,iota 值将重置并重新从零开始。
const (
a = iota
b
)
const (
c = iota
)
将输出。
0
1
0
- 可以使用空标识符跳过 iota 递增。
const (
a = iota
_
b
c
)
将输出。
0
2
3
- iota 表达式–iota 允许表达式,可以用于为常量设置任何值。
package main
import "fmt"
const (
a = iota
b = iota + 4
c = iota * 4
)
func main() {
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
将输出。
0
5
8
第一次的 iota 值是零,因此输出为零。
下一行的 iota 值为 1,因此输出为 1+4=5。
下一行,iota 值为 2,因此输出为 2*4=8。
- iota 也可以从非零数字开始,iota 表达式也可以用于从任何数字开始 iota。
const (
a = iota + 10
b
c
)
将输出
10
11
12
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 类型常量的字符串值。
结论
这就是关于 IOTA 和枚举在 Golang 中的所有内容。希望你喜欢这篇文章。请在评论中分享反馈/改进/错误。
下一篇教程 – 协程
上一篇教程 – 接口
Go(Golang)中的图是否为二分图程序
目录
-
概述
-
程序
概述
给定一个无向图。如果图的节点可以被分成两个子集,使得每条边连接第一个子集中的一个节点与第二个子集中的某个节点,则该图称为二分图。
图包含 n 个节点,编号从0到n-1。输入是一个名为graph的矩阵,它是一个二维矩阵,其中 graph[i]包含第 i个节点连接的节点。例如,如果
graph[0] = [1,3]
这意味着节点 0连接到节点 1和节点 3。
示例 1
Input: [[1,3],[0,2],[1,3],[0,2]]
Output: true
示例 2
Input: [[1,4],[0,2],[1,3],[2,4],[0,3]
Output: false
思路是使用 DFS。我们将尝试为每个节点分配红色或黑色。如果一个节点被涂成红色,则其邻居必须涂成黑色。
-
如果我们能够以这种方式上色,那么图就是二分图。
-
如果在上色时发现由边连接的两个节点具有相同的颜色,则图不是二分图。
让我们看看相应的程序
程序
下面是相应的程序
package main
import "fmt"
func isBipartite(graph [][]int) bool {
nodeMap := make(map[int][]int)
numNodes := len(graph)
if numNodes == 1 {
return true
}
for i := 0; i < numNodes; i++ {
nodes := graph[i]
for j := 0; j < len(nodes); j++ {
nodeMap[i] = append(nodeMap[i], nodes[j])
}
}
color := make(map[int]int)
for i := 0; i < numNodes; i++ {
if color[i] == 0 {
color[i] = 1
isBiPartite := visit(i, nodeMap, &color)
if !isBiPartite {
return false
}
}
}
return true
}
func visit(source int, nodeMap map[int][]int, color *map[int]int) bool {
for _, neighbour := range nodeMap[source] {
if (*color)[neighbour] == 0 {
if (*color)[source] == 1 {
(*color)[neighbour] = 2
} else {
(*color)[neighbour] = 1
}
isBipartite := visit(neighbour, nodeMap, color)
if !isBipartite {
return false
}
} else {
if (*color)[source] == (*color)[neighbour] {
return false
}
}
}
return true
}
func main() {
output := isBipartite([][]int{{1, 3}, {0, 2}, {1, 3}, {0, 2}})
fmt.Println(output)
output = isBipartite([][]int{{1, 4}, {0, 2}, {1, 3}, {2, 4}, {0, 3}})
fmt.Println(output)
}
true
false
注意:请查看我们的 Golang 高级教程。本系列的教程内容详尽,我们努力涵盖所有概念并提供示例。本教程适合那些希望获得 Golang 专业知识和扎实理解的人 - Golang 高级教程
如果您有兴趣了解所有设计模式如何在 Golang 中实现。如果是,那么这篇文章适合您 - 所有设计模式 Golang
另外,请查看我们的系统设计教程系列 - 系统设计教程系列
在 Go(Golang)中遍历字符串
在 Golang 中,字符串是字节的序列。字符串字面量实际上表示一个 UTF-8 字节序列。在 UTF-8 中,ASCII 字符是单字节,对应于前 128 个 Unicode 字符。所有其他字符占用 1 到 4 个字节。为了更好地理解,考虑以下字符串:
sameple := "a£c"
在上述字符串中
-
根据 UTF-8,‘a’占用一个字节。
-
根据 UTF-8,‘£’占用两个字节。
-
根据 UTF-8,‘b’占用一个字节。
上述字符串总共有 1+2+1 = 4 个字节。因此,当我们尝试使用标准的len() 函数打印字符串的长度时,它将输出 4,而不是 3,因为 len() 函数返回的是字符串中的字节数。
fmt.Printf("Length is %d\n", len(sample))
因此,独立的 for 循环不能用于遍历字符串中的所有字符,因为它会遍历字节而不是字符。因此,下面的 for 循环将循环四次,并打印对应于该索引的字节值。
for i := 0; i < len(sample); i++ {
fmt.Printf("%c\n", sample[i])
}
它将输出以下字符串,与 sample 字符串不同。
a£b
现在我们提到了使用 len()
函数和 for
循环的上述限制,让我们来看两种计算字符串长度的方法。
-
使用 for-range 循环
-
通过将字符串转换为 rune 数组。
使用 for-range 循环
for-range 遍历字符串中的 Unicode 点(在 golang 中也称为 rune),并将正确输出 a, £, b。因此,它也可以用于计算字符串的长度。下面是使用 for-range 和字符串时的格式。
for index, character := range string {
//Do something with index and character
}
示例代码
package main
import "fmt"
func main() {
sample := "a£b"
for i, letter := range sample {
fmt.Printf("Start Index: %d Value:%s\n", i, string(letter))
}
}
输出
Start Index: 0 Value:a
Start Index: 1 Value:£
Start Index: 3 Value:b
通过将字符串转换为 rune 数组
一个 rune 表示一个 Unicode 点。通过将字符串转换为 rune 数组,基本上就是创建该字符串的 Unicode 点数组。因此,一旦字符串转换为 rune 数组,就可以用于遍历字符串中的所有字符。
package main
import "fmt"
func main() {
sample := "a£b"
runeSample := []rune(sample)
fmt.Printf("Length of given string is %d\n", len(runeSample))
//Iterate
for i := 0; i < len(runeSample); i++ {
fmt.Println(string(runeSample[i]))
}
}
输出
Length of given string is 3
a
£
b
在 Go (Golang) 中遍历路径下的所有文件和文件夹。
来源:
golangbyexample.com/iterate-over-all-files-and-folders-go/
‘Walk’ 函数来自 ‘filepath’ 包,可用于递归遍历目录树中的所有文件/文件夹。
https://golang.org/pkg/path/filepath/
‘Walk’ 函数将遍历以根路径为根的整个树,包括所有子目录。以下是该函数的签名。
type WalkFunc func(path string, info os.FileInfo, err error) error
WalkFunc 将被调用,并传入文件/文件夹的 path 和 fileInfo 或 error(如果在遍历该文件/文件夹时发生了错误)。
关于 Walk 函数的一些注意事项
-
所有错误都被过滤。打开/访问文件时可能会出现错误。
-
该函数不遵循符号链接。
-
文件以字典顺序遍历。
让我们看一个例子:
package main
import (
"fmt"
"log"
"os"
"path/filepath"
)
func main() {
currentDirectory, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
iterate(currentDirectory)
}
func iterate(path string) {
filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Fatalf(err.Error())
}
fmt.Printf("File Name: %s\n", info.Name())
return nil
})
}
Go(Golang)中的迭代二叉搜索树
目录
** 介绍
- 完整工作代码
介绍
二叉搜索树(BST)是二叉树的缩写。每个节点在二叉搜索树中
-
左子树中每个节点的值都小于当前节点的值。
-
右子树中每个节点的值都大于当前节点的值。
-
左子树和右子树本身都是二叉搜索树。
完整工作代码
insertRec() 函数以迭代方式插入到二叉搜索树中。
package main
import "fmt"
type bstnode struct {
value int
left *bstnode
right *bstnode
}
type bst struct {
root *bstnode
}
func initList() *bst {
return &bst{}
}
func (b *bst) reset() {
b.root = nil
}
func (b *bst) insert(value int) {
b.insertRec(b.root, value)
}
func (b *bst) insertRec(node *bstnode, value int) {
if b.root == nil {
b.root = &bstnode{
value: value,
}
}
if node == nil {
return
}
//Find the terminalNode where to insert the new node
var terminalNode *bstnode
for node != nil {
terminalNode = node
if value <= node.value {
node = node.left
} else {
node = node.right
}
}
if value <= terminalNode.value {
terminalNode.left = &bstnode{value: value}
} else {
terminalNode.right = &bstnode{value: value}
}
return
}
func (b *bst) find(value int) error {
node := b.findRec(b.root, value)
if node == nil {
return fmt.Errorf("Value: %d not found in tree", value)
}
return nil
}
func (b *bst) findRec(node *bstnode, value int) *bstnode {
if node == nil {
return nil
}
if node.value == value {
return b.root
}
if value < node.value {
return b.findRec(node.left, value)
}
return b.findRec(node.right, value)
}
func (b *bst) inorder() {
b.inorderRec(b.root)
}
func (b *bst) inorderRec(node *bstnode) {
if node != nil {
b.inorderRec(node.left)
fmt.Println(node.value)
b.inorderRec(node.right)
}
}
func main() {
bst := &bst{}
eg := []int{2, 5, 7, -1, -1, 5, 5}
for _, val := range eg {
bst.insert(val)
}
fmt.Println("Printing Inorder")
bst.inorder()
bst.reset()
eg = []int{4, 5, 7, 6, -1, 99, 5}
for _, val := range eg {
bst.insert(val)
}
fmt.Println("Printing Inorder")
bst.inorder()
err := bst.find(2)
if err != nil {
fmt.Printf("Value %d Not Found\n", 2)
} else {
fmt.Printf("Value %d Found\n", 2)
}
err = bst.find(6)
if err != nil {
fmt.Printf("Value %d Not Found\n", 6)
} else {
fmt.Printf("Value %d Found\n", 6)
}
err = bst.find(5)
if err != nil {
fmt.Printf("Value %d Not Found\n", 5)
} else {
fmt.Printf("Value %d Found\n", 5)
}
err = bst.find(1)
if err != nil {
fmt.Printf("Value %d Not Found\n", 1)
} else {
fmt.Printf("Value %d Found\n", 1)
}
}
输出:
Printing Inorder
-1
-1
2
5
5
5
7
Printing Inorder
-1
4
5
5
6
7
99
Value 2 Not Found
Value 6 Found
Value 5 Found
Value 1 Not Found