发布于 2026-01-06 1 阅读
0

探索 Go 语言中的结构体和接口

探索 Go 语言中的结构体和接口

作者:Alexander Nnakwue ✏️

介绍

Go 是一种类型安全、静态类型、编译型编程语言。其类型系统通过类型名称和类型声明来表示类型,旨在防止出现未经检查的运行时类型错误。

在 Go 语言中,有几种内置的标识符类型,也称为预声明类型。它们包括布尔值、字符串、数值(浮点数、整数、复数)以及其他类型。此外,还有复合类型,它们由多个预声明类型组合而成。

复合类型主要使用类型字面量来指定。它们包括数组、接口、结构体、函数、映射类型等等。在本文中,我们将重点介绍 Go 语言中的结构体和接口类型。

先决条件

为了方便学习本教程,建议您对 Go 语言有一定的了解。建议您在机器上安装 Go 二进制文件。安装说明请参见此处。不过,为了简化操作并方便本文讲解,我们将使用Go Playground来演示示例。

LogRocket 免费试用横幅

围棋入门

Go 是一种现代、快速的编译型语言(即,它能从源代码生成机器代码),拥有许多强大的特性。它开箱即用地支持并发,因此也适用于底层计算机网络和系统编程等领域。

为了探索 Go 的各项功能,我们需要先进行开发环境配置。为此,我们可以先安装 Go 二进制文件。然后,我们进入 Go 工作区文件夹,其中包含bin` <go_source_file> pkg`、`<go_source_file>` 和src`<go_source_file>` 目录。在早期的 Go 版本(1.13 版本之前)中,源代码必须写在 `<go_source_file>`src目录中,该目录包含 Go 源文件。

这是因为 Go 需要一种方法来查找、安装和构建源文件。

这需要我们在开发机器上设置$GOPATH环境变量,Go 会使用该变量来识别工作区根文件夹的路径。因此,为了在工作区内创建一个新目录,我们必须指定完整路径,如下所示:

$ mkdir -p $GOPATH/src/github.com/firebase007
Enter fullscreen mode Exit fullscreen mode

$GOPATH可以是机器上的任何路径,通常是 `/usr/local/bin` $HOME/go,但不包括 Go 安装目录的路径。在上述指定的路径下,我们可以创建包目录,并.go在该目录中创建文件。

bin目录包含可执行的 Go 二进制文件。go工具链及其一系列命令会将这些二进制文件构建并安装到该目录中。该工具提供了一种获取、构建和安装 Go 包的标准方法。工具链的文档可在此处go找到

注意:该pkg目录是 Go 存储预编译文件缓存以供后续编译的地方。有关如何使用该目录编写 Go 代码的更多详细信息,$GOPATH请参见此处

包裹

为了封装、管理依赖关系和提高代码复用性,程序被分组为包。是存储在同一目录下并一起编译的源文件。它们存储在模块中,模块是一组执行特定操作的相关 Go 包。

注意:一个 Go 代码仓库通常只包含一个模块,该模块位于仓库根目录。但是,一个仓库也可以包含多个模块。

如今,随着 Go 1.13 及更高版本引入模块功能,我们可以像这样运行和编译一个简单的 Go 模块或程序:

retina@alex Desktop % mkdir examplePackage // create a directory on our machine outside $GOPATH/src
retina@alex Desktop % cd examplePackage  // navigate into that directory
retina@alex examplePackage % go mod init github.com/firebase007/test  // choose a module path and create a go.mod file that declares that path
go: creating new go.mod: module github.com/firebase007/test
retina@Terra-011 examplePackage % ls
go.mod
Enter fullscreen mode Exit fullscreen mode

假设test这是我们上面提到的模块的名称,我们可以创建一个包目录,然后在该目录下创建新文件。让我们来看一个简单的例子。

retina@alex examplePackage % mkdir test
retina@alex examplePackage % ls
go.mod  test
retina@alex examplePackage % cd test 
retina@alex test % ls
retina@alex test % touch test.go
retina@alex test % ls
test.go
retina@alex test % go run test.go 
Hello, Go
retina@alex test % %
Enter fullscreen mode Exit fullscreen mode

文件内的示例代码test.go如下所示:

package main  // specifies the package name

import "fmt"

func main() {
  fmt.Println("Hello, Go")
}
Enter fullscreen mode Exit fullscreen mode

注意:该go.mod文件声明了模块的路径,其中也包括模块内所有包的导入路径前缀。这对应于模块在工作区或远程仓库中的位置。有关使用模块组织 Go 代码的更多详细信息,请参阅此处

Go语言中的类型系统

就像其他语言中的类型系统一样,Go 的类型系统规定了一组规则,用于为变量、函数和标识符分配类型属性。

Go 语言中的类型可以分为以下几类:

  • 字符串类型:表示一组字符串值,在 Go 语言中,字符串值对应于一个字节切片。字符串一旦创建,就是不可变的或只读的。字符串是已定义类型,因为它们具有附加的方法。
  • 布尔类型:由预先声明的常量true和表示。false
  • 数值类型表示整数或浮点数值的集合。它们包括整数uint8浮点数byte整数浮点数复数整数...uint16uint32uint64int8int16int32runeint64float32float64complex64complex128
  • 数组类型:相同类型元素的编号集合。它们本质上是切片的基本构建模块。在 Go 语言中,数组是值,这意味着当它们被赋值给变量或作为参数传递给函数时,复制的是它们的原始值,而不是它们的内存地址。
  • 切片类型:切片只是底层数组的一部分,或者说,基本上是对底层数组的引用。[]T是一个包含类型为 的元素的切片T
  • 指针类型:一种引用类型,表示指向给定类型变量的所有指针的集合。通常,指针类型保存另一个变量的内存地址。指针的零值为 0。nil

关于其他类型(例如映射、函数、通道等)的更多详细信息,请参阅语言规范的类型部分。如前所述,本文将重点介绍接口和结构体类型。

接口和结构体简介

结构

Go 语言的结构体类型包含相同或不同类型的字段。结构体本质上是一组具有逻辑含义或构造的命名字段的集合,其中每个字段都有一个特定的类型。

通常,结构体类型是用户自定义类型的组合。它们是特殊类型,因为它们允许我们在内置类型不足以满足需求的情况下定义自定义数据类型。让我们通过一个例子来更好地理解这一点。

假设我们有一篇博客文章要发布。使用结构体类型来表示数据字段,代码如下所示:

type blogPost struct {
  author  string    // field
  title   string    // field  
  postId  int       // field
}
// Note that we can create instances of a struct types
Enter fullscreen mode Exit fullscreen mode

在上述结构体定义中,我们添加了不同的字段值。现在,要使用字面量实例化或初始化该结构体,我们可以这样做:

package main

import "fmt"

type blogPost struct {
  author  string
  title   string
  postId  int  
}

func main() {
        var b blogPost // initialize the struct type

        fmt.Println(b) // print the zero value       

        b = blogPost{ //
        author: "Alex",
        title: "Understand struct and interface types",
        postId: 12345,
        }

        fmt.Println(b)        

}

//output
{  0}  // zero values of the struct type is shown
{Alex Understand struct and interface types 12345}
Enter fullscreen mode Exit fullscreen mode

这里是运行上述代码的测试环境链接。

我们还可以使用点.运算符在初始化结构体类型后访问其中的各个字段。下面通过一个例子来说明如何操作:

package main

import "fmt"

type blogPost struct {
  author  string
  title   string
  postId  int  
}

func main() {
        var b blogPost // b is a type Alias for the BlogPost
        b.author= "Alex"
        b.title="understand structs and interface types"
        b.postId=12345

        fmt.Println(b)  

        b.author = "Chinedu"  // since everything is pass by value by default in Go, we can update this field after initializing - see pointer types later

        fmt.Println("Updated Author's name is: ", b.author)           
}
Enter fullscreen mode Exit fullscreen mode

同样,这里提供一个链接,可以在 Playground 中运行上面的代码片段。此外,我们可以使用简短的字面量表示法来实例化结构体类型,而无需使用字段名,如下所示:

package main

import "fmt"

type blogPost struct {
  author  string
  title   string
  postId  int  
}

func main() {
        b := blogPost{"Alex", "understand struct and interface type", 12345}
        fmt.Println(b)        

}
Enter fullscreen mode Exit fullscreen mode

请注意,使用上述方法时,必须始终按照结构体类型中声明的顺序传递字段值。此外,所有字段都必须初始化。

最后,如果某个结构体类型只在函数内部使用一次,我们可以将其内联定义,如下所示:

package main

import "fmt"

type blogPost struct {
  author  string
  title   string
  postId  int  
}

func main() {

        // inline struct init
        b := struct {
          author  string
          title   string
          postId  int  
         }{
          author: "Alex",
          title:"understand struct and interface type",
          postId: 12345,
        }

        fmt.Println(b)           
}
Enter fullscreen mode Exit fullscreen mode

注意:我们也可以使用new关键字初始化结构体类型。在这种情况下,我们可以这样做:

b := new(blogPost)

然后,我们可以使用点.运算符来设置和获取字段的值,就像我们之前看到的那样。让我们来看一个例子:

package main

import "fmt"

type blogPost struct {
  author  string
  title   string
  postId  int  
}

func main() {
        b := new(blogPost)

        fmt.Println(b) // zero value

        b.author= "Alex"
        b.title= "understand interface and struct type in Go"
        b.postId= 12345

        fmt.Println(*b)   // dereference the pointer     

}

//output
&{  0}
{Alex understand interface and struct type in Go 12345}
Enter fullscreen mode Exit fullscreen mode

注意:从输出结果可以看出,使用该new关键字会为变量分配存储空间b,然后初始化结构体字段(在本例中为 `struct`)的值,使其均为零(author="", title="", postId=0)。之后,它会返回一个指针类型*b,其中包含上述变量在内存中的地址。

这里是运行代码的 Playground链接。关于关键字行为的更多详细信息,请点击此处new查看

指向结构体的指针

在之前的示例中,我们使用了 Go 的默认行为,即所有参数都按值传递。但对于指针来说,情况并非如此。让我们通过一个例子来说明:

package main

import "fmt"

type blogPost struct {
  author  string
  title   string
  postId  int  
}

func main() {
        b := &blogPost{
                author:"Alex",
                title: "understand structs and interface types",
                postId: 12345,
                }

        fmt.Println(*b)   // dereference the pointer value 

       fmt.Println("Author's name", b.author) // in this case Go would handle the dereferencing on our behalf
}
Enter fullscreen mode Exit fullscreen mode

这是运行代码的 playground链接。

随着我们对方法和接口部分的深入学习,我们将开始了解这种方法的优点。

嵌套或嵌入的结构体字段

我们之前提到过,结构体类型是复合类型。因此,我们也可以创建嵌套在其他结构体中的结构体。例如,假设我们有一个结构体 AblogPost和一个Author结构体 B,定义如下:

type blogPost struct {
  title      string
  postId     int
  published  bool 
}


type Author struct {
  firstName, lastName, Biography string
  photoId    int
}
Enter fullscreen mode Exit fullscreen mode

然后,我们可以像这样将Author结构体嵌套在另一个结构体中:blogPost

package main

import "fmt"

type blogPost struct {
  author  Author // nested struct field
  title   string
  postId  int 
  published  bool  
}

type Author struct {
  firstName, lastName, Biography string
  photoId    int
}

func main() {
        b := new(blogPost)

        fmt.Println(b)

        b.author.firstName= "Alex"
        b.author.lastName= "Nnakwue"
        b.author.Biography = "I am a lazy engineer"
        b.author.photoId = 234333
        b.published=true
        b.title= "understand interface and struct type in Go"
        b.postId= 12345

        fmt.Println(*b)        

}

// output

&{{   0}  0 false}  // again default values
{{Alex Nnakwue I am a lazy engineer 234333} understand interface and struct type in Go 12345 true}
Enter fullscreen mode Exit fullscreen mode

这是在 Playground 中运行代码的链接。

在 Go 语言中,嵌套结构体类型有一个提升字段的概念。在这种情况下,我们可以直接访问嵌入结构体中定义的结构体类型,而无需深入到很多层级,例如,使用 ` b.author.firstName.`。让我们看看如何实现这一点:

package main

import "fmt"

type Author struct {
  firstName, lastName, Biography string
  photoId    int
}

type BlogPost struct {
  Author  // directly passing the Author struct as a field - also called an anonymous field orembedded type 
  title   string
  postId  int 
  published  bool  
}

func main() {
        b := BlogPost{
        Author: Author{"Alex", "Nnakwue", "I am a lazy engineer", 234333},
        title:"understand interface and struct type in Go",
        published:true,
        postId: 12345,
        }

        fmt.Println(b.firstName) // remember the firstName field is present on the Author struct??

        fmt.Println(b)        

}

//output
Alex
{{Alex Nnakwue I am a lazy engineer 234333} understand interface and struct type in Go 12345 true}
Enter fullscreen mode Exit fullscreen mode

这是运行代码的 playground链接。

注意:Go 语言不支持继承,而是支持组合。在接下来的章节中,我们将学习如何将这些概念应用于结构体和接口类型,以及如何使用方法为它们添加行为。

Go语言中的方法和函数

方法集

类型的方法集T包含所有使用接收器类型声明的方法T。请注意,接收器是通过方法名称前面的额外参数指定的。有关接收器类型的更多详细信息,请参阅此处

Go 语言中的方法是带有接收者的特殊函数。

在 Go 语言中,我们可以通过为类型定义方法来创建具有特定行为的类型。本质上,方法集是一个类型必须具备的方法列表,以便实现某个接口。让我们来看一个例子:

// BlogPost struct with fields defined
type BlogPost struct {
  author  string
  title   string
  postId  int  
}

// Create a BlogPost type called (under) Technology
type Technology BlogPost
Enter fullscreen mode Exit fullscreen mode

注意:这里我们使用结构体类型,因为本文重点讨论结构体。方法也可以定义在其他命名类型上。

// write a method that publishes a blogPost - accepts the Technology type as a pointer receiver
func (t *Technology) Publish() {
    fmt.Printf("The title on %s has been published by %s, with postId %d\n" , t.title, t.author, t.postId)
}


// Create an instance of the type
t := Technology{"Alex","understand structs and interface types",12345}


// Publish the BlogPost -- This method can only be called on the Technology type
t.Publish()

// output
The title on understand structs and interface types has been published by Alex, with postId 12345
Enter fullscreen mode Exit fullscreen mode

这是运行代码的 playground链接。

注意:带有指针接收器的方法既可以操作指针也可以操作值。但是,反过来则不行。有关方法集的更多详细信息,请参阅语言规范中的相关文档。

接口

在 Go 语言中,接口的主要作用是封装,它使我们能够编写更简洁、更健壮的代码。通过这种方式,我们只需在程序中暴露方法和行为即可。正如我们在上一节中提到的,方法集可以为一个或多个类型添加行为。

接口类型定义了一个或多个方法集。因此,一个类型通过实现接口的方法而被称为实现了该接口。从这个意义上讲,接口使我们能够组合具有共同行为的自定义类型。

在 Go 语言中,接口是隐式的。这意味着,如果一个类型实现了接口类型方法集中的所有方法,那么该类型就被称为实现了该接口。要声明一个接口:

type Publisher interface {
    publish()  error
}
Enter fullscreen mode Exit fullscreen mode

在我们上面定义的接口方法中,如果一个类型(例如结构体)实现了该方法,那么我们可以说该类型实现了该接口。下面publish()我们定义一个接受结构体类型的方法:blogpost

func (b blogPost) publish() error {
   fmt.Println("The title has been published by ", b.author)
   return nil
}
Enter fullscreen mode Exit fullscreen mode

现在开始实现接口:

package main

import "fmt"

// interface definition
type Publisher interface {
     Publish()  error
}

type blogPost struct {
  author  string
  title   string
  postId  int  
}

// method with a value receiver
func (b blogPost) Publish() error {
   fmt. Printf("The title on %s has been published by %s, with postId %d\n" , b.title, b.author, b.postId)
   return nil
}

 func test(){

  b := blogPost{"Alex","understanding structs and interface types",12345}

  fmt.Println(b.Publish())

   d := &b   // pointer receiver for the struct type

   b.author = "Chinedu"


   fmt.Println(d.Publish())

}


func main() {

        var p Publisher

        fmt.Println(p)

        p = blogPost{"Alex","understanding structs and interface types",12345}

        fmt.Println(p.Publish())

        test()  // call the test function 

}

//output
<nil>
The title on understanding structs and interface types has been published by Alex, with postId 12345
<nil>
The title on understanding structs and interface types has been published by Alex, with postId 12345
<nil>
The title on understanding structs and interface types has been published by Chinedu, with postId 12345
<nil>
Enter fullscreen mode Exit fullscreen mode

这是运行代码的 playground链接。

我们还可以像这样为接口类型设置别名:

type publishPost Publisher // alias to the interface defined above - only suited for third-party interfaces
Enter fullscreen mode Exit fullscreen mode

注意:如果多个类型实现了同一个方法,则该方法集可以构成一个接口类型。这样,我们就可以将这个接口类型作为参数传递给一个需要实现该接口行为的函数。通过这种方式,就可以实现多态性。

与函数不同,方法只能从定义它的类型的实例中调用。这样做的好处是,我们无需指定函数参数的具体数据类型,而是可以指定传递给函数的参数对象的行为。

我们来看看如何将接口类型用作函数的参数。首先,我们给结构体类型添加一个方法:

package main

import "fmt"


type Publisher interface {
     Publish()  error
}

type blogPost struct {
  author  string
  title   string
  postId  int  
}


func (b blogPost) Publish() error {
   fmt.Printf("The title on %s has been published by %s\n" , b.title, b.author)
   return nil
}

// Receives any type that satisfies the Publisher interface
func PublishPost(publish Publisher) error {
    return publish.Publish()
}

func main() {

        var p Publisher

        fmt.Println(p)

        b := blogPost{"Alex","understand structs and interface types",12345}

        fmt.Println(b)

        PublishPost(b)

}

//output
<nil>
{Alex understand structs and interface types 12345}
The title on understand structs and interface types has been published by Alex
Enter fullscreen mode Exit fullscreen mode

这是在 Playground 上运行代码的链接。

如前所述,我们可以按值或按指针类型传递方法接收器。按值传递时,我们会存储传递值的副本。这意味着调用方法时,我们不会更改底层值。但是,按指针传递时,我们会直接共享底层内存地址,也就是底层类型中声明的变量的位置。

注意:提醒一下,当一个类型定义了接口类型上可用的方法集时,就称该类型实现了接口。

再次强调,类型不需要声明自己实现了某个接口;相反,任何类型只要具有签名与接口声明匹配的方法,就都实现了某个接口。

最后,我们将研究 Go 中嵌入接口类型的签名。让我们使用一个简单的例子:

//embedding interfaces
type interface1 interface {
    Method1()
}

type interface2 interface {
    Method2()
}

type embeddedinterface interface {
    interface1
    interface2
}

func (s structName)  method1 (){

}

func (s structName)  method2 (){

}


type structName struct {
  field1  type1
  field2  type2

}

// initialize struct type inside main func
var e embeddedinterface = structName // struct initialized
e.method1() // call method defined on struct type
Enter fullscreen mode Exit fullscreen mode

注意:一般来说,当包中多个类型实现相同的方法签名时,我们就可以开始重构代码并使用接口类型。这样做可以避免过早进行抽象。

关于结构体类型需要注意的事项

  • 字段名可以隐式地通过变量指定,也可以作为不带字段名的嵌入类型指定。在这种情况下,字段必须指定为类型名称T或指向非接口类型名称的指针。*T
  • 结构体类型中的字段名必须是唯一的。
  • 嵌入式类型的字段或方法可以被提升
  • 提升后的字段不能用作结构体中的字段名。
  • 字段声明后可以跟一个可选的字符串字面量标签
  • 导出的结构体字段必须以大写字母开头。
  • 除了基本类型之外,我们还可以将函数类型和接口类型作为结构体字段。

有关结构体类型的更多详细信息,请参阅语言规范。

关于接口类型需要注意的事项

  • 接口的零值是nil
  • 空接口不包含任何方法。请注意,所有类型都实现了空接口。这意味着,如果您编写一个接受空interface{}值作为参数的函数,则可以为该函数提供任何值。
  • 接口通常应该放在使用接口类型值的包中,而不是放在实现这些值的包中。

有关接口类型的更多详细信息,请参阅此处的语言规范。

结论

正如我们所了解的,接口类型可以存储值的副本,也可以通过存储指向该值地址的指针来与接口共享该值。关于接口类型,需要注意的一点是,不建议过早地进行优化,因为我们不希望在接口被使用之前就对其进行定义。

判断接口是否符合规范或是否被使用,取决于方法接收者以及接口调用的方式。更多相关信息,请参阅此处的 Go 代码审查和评论部分

关于方法接收器的指针和值,有一条相当令人困惑的规则:虽然值方法可以对指针和值调用,但指针方法只能对指针调用。对于接收器类型,如果方法需要修改接收器,则接收器必须是指针。

关于接口类型的更多细节,请参阅《Effective Go》。具体来说,您可以查看接口和方法接口检查以及接口转换和类型断言部分。类型断言更像是对接口类型的底层值进行的操作。本质上,它是提取接口类型值的过程。它们表示为 `<interface type="type">` x.(T),其中值x是一个接口类型。

再次感谢您的阅读,欢迎在下方评论区留言或评论,也可以通过Twitter联系我。祝您学习愉快!🙂


插件:LogRocket,一款用于 Web 应用的 DVR

 
LogRocket 控制面板免费试用横幅
 
LogRocket是一款前端日志工具,可让您重现问题,如同在您自己的浏览器中发生一样。无需猜测错误原因,也无需用户提供屏幕截图和日志转储,LogRocket 即可让您重现会话,快速了解问题所在。它与任何框架的应用程序完美兼容,并提供插件来记录来自 Redux、Vuex 和 @ngrx/store 的额外上下文信息。
 
除了记录 Redux 操作和状态之外,LogRocket 还会记录控制台日志、JavaScript 错误、堆栈跟踪、包含标头和正文的网络请求/响应、浏览器元数据以及自定义日志。它还会对 DOM 进行插桩,记录页面上的 HTML 和 CSS,即使是最复杂的单页应用程序,也能生成像素级精确的视频。
 
免费试用


文章《探索 Go 中的结构体和接口》最初发表于LogRocket 博客

文章来源:https://dev.to/bnevilleoneill/exploring-structs-and-interfaces-in-go-31ho