Go语言结构体详解
引言
在Go语言中,结构体是一种非常重要的数据类型,它允许程序员将不同类型的数据组合在一起,形成一个完整的数据结构。这种特性使得Go语言在处理复杂数据时更加灵活和高效。本文将深入探讨Go语言中结构体的定义、实例化、方法定义以及相关操作,通过丰富的示例代码帮助读者全面理解结构体的使用方法。
自定义类型
在Go语言中,当标准库提供的数据类型无法满足开发需求时,开发人员可以通过type
关键字自定义新类型。自定义类型可以基于现有类型,也可以是结构体、接口、函数等复杂类型。
type name string
上述代码基于string
类型定义了新的类型name
。尽管name
类型与string
类型的用法相似,但它们是两种独立的类型,不能直接进行比较运算。
var a string = "abcde"
var b name = "abcde"
fmt.Printf("变量a的类型:%T\n", a)
fmt.Printf("变量b的类型:%T\n", b)
输出结果如下:
变量a的类型:string
变量b的类型:main.name
如果尝试直接比较a
和b
,会引发编译错误,因为它们属于不同的类型。
a == b // 错误:类型不同,无法比较
当基于现有类型定义的新类型仍无法满足需求时,可以定义更复杂的类型,如结构体、接口和函数类型。
// 结构体
type car struct {id uintcolor uint32
}// 接口
type sender interface {writeTo(d string, len int, msg string)
}// 函数
type otherFunc func(x float32) float32
在定义类型时,如果使用了赋值运算符=
,则新类型仅仅是现有类型的别名,而不是全新的类型。例如,char
是int32
类型的别名,因此char
类型的变量与int32
类型的变量可以进行比较运算。
var x char = 'H'
var y int32 = 72
fmt.Printf("x和y变量相等?%t", x == y)
结构体的定义
结构体是一种用户自定义的复合数据类型,它可以将多个不同类型的数据组合在一起,形成一个整体。结构体的定义格式如下:
type <结构体名称> struct {<字段列表>
}
字段列表是可选的,可以定义没有字段成员的结构体,但即使字段列表为空,一对大括号也不能省略。
type atbWorker struct {}
字段成员的定义与变量相似,可以逐个定义,也可以将类型相同的字段写在一起。
type photo struct {pID uint16width float32height float32dpi float32
}// 等价于
type photo struct {pID uint16width, height, dpi float32
}
若希望结构体的字段成员能被其他包的代码访问,除了结构体自身的名称需要首字母大写外,其字段成员的名称也要首字母大写。
type Order struct {ID uint64Product stringDate time.TimeQty float32Remarks string
}
命名中首字母为小写的字段只能在当前包中访问。
type Student struct {StdID uintName stringAge uint8email string // email字段只能在当前包中使用
}
结构体的实例化
结构体的实例化过程有多种代码格式,总体可归纳为两大类——默认初始化和手动初始化。
默认初始化
当使用var
关键字声明结构体类型的变量后,程序会为该结构体的各个字段分配默认值。
type fileInfo struct {name stringsize uint64isSysFile boolcreateTime int64
}var x fileInfo
fmt.Printf("文件名:%s\n", x.name)
fmt.Printf("文件大小:%d\n", x.size)
fmt.Printf("是否为系统文件:%t\n", x.isSysFile)
fmt.Printf("创建时间:%s\n", time.Unix(x.createTime, 0))
也可以这样写:
var x = fileInfo{}
// 或者
x := fileInfo{}
上面代码虽然为变量赋了值,但没有设置fileInfo
实例各字段的值,因此各字段的值仍然为默认值。
手动初始化
结构体实例化后通常需要为字段赋值,可以在定义变量后逐个字段进行赋值,也可以在定义变量时直接初始化字段的值。
var y fileInfo
y.name = "dmd.txt"
y.isSysFile = false
y.size = 6955263
y.createTime = time.Date(2020, 3, 7, 15, 48, 16, 0, time.Local).Unix()
或者在定义变量时直接初始化:
var g = fileInfo{name: "abc.dat",size: 128880,isSysFile: true,createTime: time.Date(2019, 10, 20, 14, 36, 21, 0, time.Local).Unix(),
}
在多行初始化语句中,最后一个字段末尾的逗号不能省略。
var g = fileInfo{name: "abc.dat",size: 128880,isSysFile: true,createTime: time.Date(2019, 10, 20, 14, 36, 21, 0, time.Local).Unix(),
}
许多时候,某些字段的默认值正是所需要的值,这种情况下可以忽略部分字段的赋值。
k := fileInfo{name: "dxy.ts",size: 3006265,createTime: time.Date(2020, 1, 1, 23, 15, 4, 0, time.Local).Unix(),
}
还有一种最简洁的初始化方法:省略字段名称。
var z = fileInfo{"test.dat", 1172363, true, time.Now().Unix()}
赋值的顺序必须与字段在结构体中定义的顺序一致,而且赋值的数量也要与字段的数量相等。
如果变量的类型声明为指针类型,那么可以先创建结构体实例并完成初始化,然后使用取地址运算符&
获取其内存地址,再赋值给指针变量。
var c = fileInfo{"sys.dll", 23312, true, time.Now().Unix()}
var pc *fileInfo = &c
也可以合并为一步完成。
var pc *fileInfo = &fileInfo{"sys.dll", 23312, true, time.Now().Unix()}
结构体的方法
结构体的方法对象并不是在结构体的内部定义的,而是在结构体外部以函数的形式定义。方法与一般函数有一点不同,在方法名称前有一个接收参数。该参数传递的是方法所属结构体的实例。
type test struct {
}func (o test) doSomething() string {return "do nothing"
}
方法调用语法如下:
var n test
s := n.doSomething()
在定义方法时,接收的结构体实例也可以是指针类型。
func (o *test) doSomething2() string {return "do nothing - 2"
}
接收结构体实例的参数何时使用指针类型,这取决于应用场景。下面示例可通过对比看出两种传递参数方式的区别。
示例:setIntV1和setIntV2方法
步骤1:定义demo
结构体,它包含data
字段成员。
type demo struct {data int
}
步骤2:为demo
结构体定义两个方法。其中setIntV1
方法在接收对象参数时只复制demo
实例,而setIntV2
方法接收的是demo
类型的指针,传递的是实例的内存地址。
func (x demo) setIntV1(n int) {x.data = n // 修改data字段的值
}func (x *demo) setIntV2(n int) {x.data = n // 修改data字段的值
}
步骤3:初始化demo
实例。
var a = demo{data: 100}
步骤4:分别调用setIntV1
方法和setIntV2
方法,并输出调用前后data
字段的值。
// 情况一:非指针类型接收demo实例
fmt.Println("---------- 传递demo实例的副本 ----------")
fmt.Printf("调用setIntV1方法前,data字段的值:%d\n", a.data)
// 调用setIntV1方法
a.setIntV1(200)
fmt.Printf("调用setIntV1方法后,data字段的值:%d\n\n", a.data)// 情况二:以指针类型接收demo实例
fmt.Println("---------- 传递demo实例的内存地址 ----------")
fmt.Printf("调用setIntV2方法前,data字段的值:%d\n", a.data)
// 调用setIntV2方法
a.setIntV2(200)
fmt.Printf("调用setIntV2方法后,data字段的值:%d\n", a.data)
步骤5:尝试运行示例,会得到以下输出内容。
---------- 传递demo实例的副本 ----------
调用setIntV1方法前,data字段的值:100
调用setIntV1方法后,data字段的值:100---------- 传递demo实例的内存地址 ----------
调用setIntV2方法前,data字段的值:100
调用setIntV2方法后,data字段的值:200
调用setIntV1
方法时,demo
实例会将自身复制一份再传递给方法,所以在setIntV1
方法内部所修改的是demo
实例副本的data
字段,而不是原来demo
实例(变量a
所引用的对象)的data
字段。这使得setIntV1
方法被调用后,a.data
保持原值(100)不变。
调用setIntV2
方法时,demo
实例将自身的内存地址传递给方法,在方法内部所修改的data
字段属于原来的demo
实例(变量a
所引用的对象)。这期间操作的都是同一个实例,因此在调用完setIntV2
方法后,a.data
的值会被更新为200。
通过这个示例,可以得出结论:当需要在方法内部修改结构体对象的字段时,应该传递该结构体实例的指针。如果在方法内部只是读取结构体字段的值,那么传递给方法的结构体实例可以是值类型。
实战示例
为了更好地理解结构体的使用,下面通过一个完整的示例代码,展示结构体的定义、实例化和方法的使用。
package mainimport ("fmt""time"
)// 定义一个Person结构体
type Person struct {name stringage uint8weight float32height float32gender uint8
}// 定义Person结构体的方法:介绍自己
func (p Person) introduce() string {return fmt.Sprintf("大家好,我叫%s,今年%d岁,体重%.1fkg,身高%.1fm,性别%d。", p.name, p.age, p.weight, p.height, p.gender)
}// 定义Person结构体的方法:更新年龄(指针接收)
func (p *Person) updateAge(newAge uint8) {p.age = newAge
}func main() {// 实例化Person结构体person1 := Person{name: "张三",age: 25,weight: 70.5,height: 1.75,gender: 1,}// 调用introduce方法fmt.Println(person1.introduce())// 更新年龄person1.updateAge(26)fmt.Println("更新年龄后:", person1.introduce())// 使用指针接收器修改字段person2 := &Person{name: "李四",age: 30,weight: 65.3,height: 1.68,gender: 0,}person2.updateAge(31)fmt.Println("直接通过指针修改年龄后:", person2.introduce())
}
运行结果:
大家好,我叫张三,今年25岁,体重70.5kg,身高1.75m,性别1。
更新年龄后: 大家好,我叫张三,今年26岁,体重70.5kg,身高1.75m,性别1。
直接通过指针修改年龄后: 大家好,我叫李四,今年31岁,体重65.3kg,身高1.68m,性别0。
在这个示例中,我们定义了一个Person
结构体,并为其定义了两个方法:introduce
和updateAge
。introduce
方法是值接收器,用于返回人物的自我介绍信息;updateAge
方法是指针接收器,用于更新人物的年龄。通过这个示例,我们可以看到如何定义结构体、实例化结构体以及调用结构体的方法。
总结
Go语言的结构体是一种强大的数据结构,允许我们将不同类型的数据组合在一起,形成一个完整的数据模型。通过结构体,我们可以更好地组织和管理数据,使代码更加模块化和易于维护。本文详细介绍了Go语言中结构体的定义、实例化、方法定义以及相关操作,并通过丰富的示例代码帮助读者深入理解结构体的使用方法。希望本文能为Go语言学习者提供有价值的参考,帮助大家在实际开发中更高效地使用结构体。