探索Go语言中结构体和方法的使用

一、前言

结构体是 Go 语言中重要的数据类型之一,用于组织和表示复杂的数据结构。结构体方法则允许我们为结构体类型定义特定的行为,使代码更加模块化和可维护。在本文中,我们将探讨结构体的各个方面,以及如何使用结构体方法来操作结构体数据。

二、内容

2.1 结构体初体验

Go中,结构体(struct)是由一系列字段(fields)组成的数据类型,每个字段可以具有不同的数据类型,但每个字段的数据类型在结构体内必须明确定义,并且这些字段的类型可以是相同的,也可以是不同的。

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

举个例子:

go 复制代码
package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
    IsMarried bool
}

func main() {
    person1 := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
        IsMarried: false,
    }

    person2 := Person{
        FirstName: "Alice",
        LastName:  "Smith",
        Age:       25,
        IsMarried: true,
    }

    fmt.Println(person1)
    fmt.Println(person2)
}

运行结果:

bash 复制代码
{John Doe 30 false}
{Alice Smith 25 true}

在上述示例中,Person 结构体包含了不同的字段,包括了字符串,整数和布尔值类型的字段。每个字段都有自己的数据类型,但它们都组成了一个结构体类型,即 Person

数组(array)在Go中也是一种数据类型,它存储相同类型的数据元素,并且数组的长度是固定的。不同于结构体,数组的元素类型必须相同,无法在同一个数组中存储不同类型的数据。

2.2 结构体的定义与使用

结构体的定义格式如下:

go 复制代码
type struct_name struct {
   field_name data_type
   field_name data_type
   ...
   field_name data_type
}

在上述格式中,struct_name 是结构体的类型名称,而大括号内包含了一个或多个成员(字段)定义,每个成员定义包括成员的名称和数据类型。

一旦定义了结构体类型,您可以使用它来声明结构体变量,语法如下:

go 复制代码
val_name := struct_name{value1, value2...valuen}

或者,您可以使用键值对的方式来初始化结构体变量,语法如下:

go 复制代码
val_name := struct_name{key1: value1, key2: value2..., keyn: valuen}

这两种方式都允许您为结构体的字段赋值。

举个例子:

go 复制代码
package main

import "fmt"

// 定义结构体 Person
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // 方式一
    person1 := Person{"John", "Doe", 30}

    // 方式二
    person2 := Person{
        FirstName: "Alice",
        LastName:  "Smith",
        Age:       25,
    }

    fmt.Println("person1", person1) // person1 {John Doe 30}
    fmt.Println("person2", person2) // person2 {Alice Smith 25}
}

2.3 访问结构体对象

如果要访问结构体的成员字段,则需要使用点号.操作符,将结构体变量与成员字段名称相结合。

举个例子:

go 复制代码
package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    person := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }

    // 访问结构体成员字段并打印它们的值
    fmt.Println("First Name:", person.FirstName)
    fmt.Println("Last Name:", person.LastName)
    fmt.Println("Age:", person.Age)
}

运行结果:

shell 复制代码
First Name: John
Last Name: Doe
Age: 30

这就是访问结构体成员字段的标准方式。

点号操作符使我们能够获取和修改结构体中的各个字段的值。

2.4 结构体作为函数参数

像其他数据类型一样,结构体类型同样可以作为参数传递给函数。这使得我们可以将负责的数据结构传递给函数,并在函数内部对其进行操作或分析。

举个例子:

go 复制代码
package main

import "fmt"

// 定义结构体
type Person struct {
	FirstName string
	LastName  string
	Age       int
}

func main() {
    // 创建结构体
    person := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }

    // 将结构体作为参数传递给函数
    PrintPersonDetails(person)
}

// PrintPersonDetails ,打印参数中 Person 结构体的对象
func PrintPersonDetails(p Person) {
    fmt.Println("First Name:", p.FirstName)
    fmt.Println("Last Name:", p.LastName)
    fmt.Println("Age:", p.Age)
}

2.5 结构体指针

之前的文章讲过,Go中的数据传递严格来说都是值传递。

举个例子:

go 复制代码
package main

import "fmt"

// 定义结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func main() {
    // 创建一个结构体
    person := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }

    // 将结构体指针作为参数传递给函数
    ModifyPersonDetails(person)

    // 查看 person 结构体的数据是否已被修改
    fmt.Println("First Name:", person.FirstName)
    fmt.Println("Last Name:", person.LastName)
    fmt.Println("Age:", person.Age)
}

// 函数接受一个指向 Person 结构体的指针作为参数
func ModifyPersonDetails(p Person) {
    p.FirstName = "Alice"
    p.LastName = "Smith"
    p.Age = 28
}

运行结果为:

shell 复制代码
First Name: John
Last Name: Doe
Age: 30

可以看到,person 结构体的数据并没有发生变化。这就意味着,结构体在作为函数参数进行传递时,是按照值传递的规则,此时会将结构体的副本传递给函数,而不是原始结构体本身。

如果我们希望在函数内部修改原始结构体的数据,那么应该将结构体的指针传递给函数。

当我们将结构体的指针传递给函数时,函数将会操作原始结构体的引用,而不是其副本。

go 复制代码
package main

import "fmt"

// 定义结构体
type Person struct {
	FirstName string
	LastName  string
	Age       int
}

func main() {
    // 创建一个结构体
    person := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }

    // 将结构体指针作为参数传递给函数
    ModifyPersonDetails(&person)

    // 查看 person 结构体的数据是否已被修改
    fmt.Println("First Name:", person.FirstName)
    fmt.Println("Last Name:", person.LastName)
    fmt.Println("Age:", person.Age)
}

// 函数接受一个指向 Person 结构体的指针作为参数
func ModifyPersonDetails(p *Person) {
    p.FirstName = "Alice"
    p.LastName = "Smith"
    p.Age = 28
}

运行结果如下:

shell 复制代码
First Name: Alice
Last Name: Smith
Age: 28

小结:

当我们将函数修改为接收指向某个结构体的指针作为参数,那么通过传递该结构体的内存地址,函数就可以直接修改原始结构体的数据。注意,我们可以使用 & 操作符来获取某结构体变量的地址。

2.6 字段可见性

在Go中,结构体字段(fields)的首字母大小写规则会影响字段的可见性和访问权限。

特点如下:

  1. 如果一个结构体字段的首字母是大写字母(例如FirstName),那么这个字段是可导出的,可以被其他包中的代码访问。这种字段具有公共的访问权限。
  2. 如果一个结构体字段的首字母是小写字母(例如age),那么这个字段是不可导出的,只能被定义该结构体的包内代码访问。这种字段具有私有的访问权限。

举个例子:

project 项目根目录下的otherpackage包中有 Person 结构体定义:

go 复制代码
package otherpackage

type Person struct {
    FirstName string // 可导出字段
    age       int    // 不可导出字段
}

那么,在其他包中是无法访问不可导出字段的。

比如:

go 复制代码
package main

import (
    "project/otherpackage"
    "fmt"
)

func main() {
    person := otherpackage.Person{
        FirstName: "John",
        // age:       30,	// error: unknown field age in struct literal of type otherpackage.Person
    }

    fmt.Println(person.FirstName) // 可以访问可导出字段
    // fmt.Println(person.age)       // 无法访问不可导出字段
}

字段可见性规则有助于数据的封装和隐藏内部实现细节,是Go语言中的一种重要特性,有助于编写清晰、可维护和安全的代码。

2.7 结构体方法

Go 支持在结构体类型中定义方法

在Go中,结构体方法是一种用于关联特定类型(结构体)的函数。这些方法可以访问结构体的字段,并对其执行操作,或者执行其他与结构体相关的任务。

结构体方法是通过将函数与结构体类型关联来定义的。这样的函数被称为方法,它们在结构体类型的定义上定义。

结构体方法的语法如下:

go 复制代码
func (receiver ReceiverType) MethodName(parameters) ReturnType {
    // 方法的实现
}

在上述语法中,receiver 是一个参数,指定了方法将作用于哪个结构体类型。它可以是值接收者(receiver)或指针接收者(receiver)。

什么叫值接收者或指针接收者?

事实上,值接收者指的是结构体方法的接受者是结构体的一个副本,此时对结构体字段的修改是不会影响到原始结构体的。那么对于指针接受者来说,方法接收的是结构体的一个指针,那么此时对结构体字段的修改将会影响到原始结构体。

我们在方法内部可以通过接受者来访问结构体的字段,或者调用其他方法,那方法本身呢,是可以通过结构体变量或者结构体指针来调用的,具体看方法的接受者类型。

我们举个例子,看以下示例:

go 复制代码
package main

import "fmt"

func main() {
	// 创建一个 RectangleA 结构体实例
	rectA := RectangleA{Width: 5, Height: 3}
	fmt.Printf("initial value: Width-[%v]  Height-[%v]\n", rectA.Width, rectA.Height)
	fmt.Println("rectA.Area(): ", rectA.Area())
	rectA.Change(2) // 调用值接收者方法
	fmt.Printf("changed value: Width-[%v]  Height-[%v]\n", rectA.Width, rectA.Height)
	fmt.Println("----------------------------------------")
	// 创建一个 RectangleB 结构体实例
	rectB := RectangleB{Width: 5, Height: 3}
	areaPB := &rectB //  创建指向 RectangleB 实例的指针
	fmt.Printf("initial value: Width-[%v]  Height-[%v]\n", areaPB.Width, areaPB.Height)
	fmt.Println("areaPB.Area(): ", areaPB.Area())
	areaPB.Change(2) // 调用指针接收者方法
	fmt.Printf("changed value: Width-[%v]  Height-[%v]\n", areaPB.Width, areaPB.Height)
}

// 结构体 A
type RectangleA struct {
	Width  float64 // 宽度
	Height float64 // 长度
}

// 值接收者方法
func (ra RectangleA) Area() float64 {
	return ra.Width * ra.Height
}

// 值接收者方法
func (ra RectangleA) Change(factor float64) {
	ra.Width *= factor  // ineffective assignment to field RectangleA.Width
	ra.Height *= factor // ineffective assignment to field RectangleA.Height
}

// 结构体 B
type RectangleB struct {
	Width  float64 // 宽度
	Height float64 // 长度
}

// 指针接收者方法
func (rb *RectangleB) Area() float64 {
	return rb.Width * rb.Height
}

// 指针接收者方法
func (rb *RectangleB) Change(factor float64) {
	rb.Width *= factor
	rb.Height *= factor
}

运行结果:

bash 复制代码
initial value: Width-[5]  Height-[3]
rectA.Area():  15
changed value: Width-[5]  Height-[3]
----------------------------------------
initial value: Width-[5]  Height-[3]
areaPB.Area():  15
changed value: Width-[10]  Height-[6]

可以看到,对于 Change 方法来说,如果接收的是结构体的指针,就可以直接修改原始数据,否则操作的就是结构体的副本。

关于结构体的方法命名来说,通常采用驼峰式命名风格,并且首字母大写,以表示它是公开的方法(可导出的)。


三、总结

结构体和结构体方法是Go语言中强大的工具,可用于创建复杂的数据结构并为其定义行为。本文详细介绍了结构体的定义、访问、可见性以及结构体方法的使用方法,希望读者通过本文的学习能够更好地利用这些特性来编写高效、可维护的Go代码。

相关推荐
研究司马懿1 天前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大1 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰2 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘2 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤2 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto4 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto6 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室7 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题7 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo