介绍
- 常规面向对象语言(java,c#)使用类来实现属性和方法的聚合以及继承的概念。
- Go语言不是一种 “传统” 的面向对象编程语言,它没有类和继承的概念。Go语言通过结构体来实现上述功能。
- 结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。
声明
对各个部分的说明:
- 类型名: 标识自定义结构体的名称,在同一个包内不能重复。
- 字段名: 字段名必须唯一。
- 字段类型:可以是基础类型或结构体,甚至是字段所在结构体的类型。
例子:type 类型名 struct {
字段名1 字段1类型
字段名2 字段2类型
…
}
type Point struct {
X int
Y int
}
实例化
- 结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。
- Go语言可以通过多种方式实例化结构体,根据实际需要可以选用不同的写法。
1.基本实例化形式
// T 为结构体类型
// ins 为结构体的实例。
var ins T
var p Point
p.X = 10
2.创建指针类型的结构体
// T 为类型,可以是结构体、整型、字符串等。
// ins 为结构体的实例,类型为 *T,是指针类型。
ins := new(T)
p := new(Point)
p.Y = 20
经过 new 实例化的结构体实例在成员赋值上与基本实例化的写法一致。
3. 取结构体的地址实例化
- 在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作。
- 取地址实例化是最广泛的一种结构体实例化方式。 ```go // T 表示结构体类型。 // ins 为结构体的实例,类型为 *T,是指针类型。
ins := &T{}
---
<a name="of2JH"></a>
### 初始化成员变量
<a name="iXozV"></a>
####
<a name="yKoRQ"></a>
#### 一. 普通结构体
1. 普通结构体在实例化时可以直接对成员变量进行初始化,初始化有两种形式且不能混用:
1. 字段“键值对”形式:适合选择性填充字段较多的结构体。
1. 多个值的列表形式:适合填充字段较少的结构体。
<a name="yqedv"></a>
##### 1. 字段“键值对”形式
1. 键值对的填充是可选的,不需要初始化的字段可以不填入到初始化列表中。这些字段的默认值是字段类型的默认值,例如 ,数值为 0、字符串为 ""(空字符串)、布尔为 false、指针为 nil 等。
1. 键值之间以`:`分隔,键值对之间以`,`分隔。
```go
type People struct {
name string
child *People
}
relation := &People{
name: "爷爷",
child: &People{
name: "爸爸",
child: &People{
name: "我",
},
},
}
2. 多个值的列表形式
- 必须初始化结构体的所有字段。
- 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}
addr := Address{
"四川",
"成都",
610000,
"0",
}
fmt.Println(addr) // {四川 成都 610000 0}
二. 匿名结构体
- 匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。
- 匿名结构体在使用时需要重新定义,造成大量重复的代码,因此开发中较少使用。
- 匿名结构体的初始化写法由两部分组成:
- 结构体的定义:没有结构体名,只有字段和类型定义。
- 键值对初始化:由可选的多个键值对组成。该部分是可选的。
1.匿名结构体的定义和初始化:
ins := struct {
// 匿名结构体字段定义
字段1 字段类型1
字段2 字段类型2
…
}{
// 字段值初始化
初始化字段1: 字段1的值,
初始化字段2: 字段2的值,
…
}
// demo
msg := struct { // 定义部分
id int
data string
}{ // 值初始化部分
1024,
"hello",
}
2.匿名结构体不初始化成员时:
ins := struct {
字段1 字段类型1
字段2 字段类型2
…
}
// demo
func printMsgType(msg struct {
id int
data string
}) {
// 使用动词%T打印msg的类型
fmt.Printf("%T\n", msg)
}
构造函数
其他编程语言构造函数的一些常见功能及特性如下:
- 每个类可以添加构造函数,多个构造函数使用函数重载实现。
- 构造函数一般与类名同名,且没有返回值。
- 构造函数有一个静态构造函数,一般用这个特性来调用父类的构造函数。
- 对于 C++ 来说,还有默认构造函数、拷贝构造函数等。
- Go语言的结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程将参数使用函数传递到结构体构造参数中即可完成构造函数的任务。
- 我们通常通过
New?
或New?By?
来模式构造方法,实现重载。
1. 多种方式创建和初始化结构体 (模拟构造函数重载)
type Cat struct {
Color string
Name string
}
func NewCatByName(name string) *Cat {
return &Cat{
Name: name,
}
}
func NewCatByColor(color string) *Cat {
return &Cat{
Color: color,
}
}
2. 带有父子关系的结构体的构造和初始化(模拟父级构造调用)
type Cat struct {
Color string
Name string
}
// “构造基类”
func NewCat(name string) *Cat {
return &Cat{
Name: name,
}
}
type BlackCat struct {
Cat // 嵌入Cat, 类似于派生
}
// “构造子类”
func NewBlackCat(color string) *BlackCat {
cat := &BlackCat{}
cat.Color = color
return cat
}
接收器
介绍
- 在Go语言中,结构体就像是类的一种简化形式,那么类的方法在哪里呢?Go语言中类方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。一个类型加上它的方法等价于面向对象中的一个类。
- 在Go语言中,绑定在接收器上的方法可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的。
- 一个接收器类型上的所有方法的集合叫做该接收器的方法集。
- 接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。
- 接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误
invalid receiver type…
。 - 接收器不能是一个指针类型,但是它可以是任何其他允许类型的指针。
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
函数体
}
- 接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误
- 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 等命名。例如,Socket 类型接收器变量应该命名为 s,Connector 类型接收器变量应该命名为 c 等。
- 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中:
- 指针类型 :大对象因为复制性能较低,在接收器和参数间传递时不进行复制,只是传递指针。
- 非指针类型:小对象由于值复制时的速度较快,所以适合使用非指针接收器。
- 方法名、参数列表、返回参数:格式与函数定义一致。
指针类型 接收器
- 指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。
- 由于指针的特性,调用方法时,修改接收器指针的任意成员变量,都是有效的。 ```go package main import “fmt”
// 定义属性结构 type Property struct { value int // 属性值 } // 设置属性值 func (p Property) SetValue(v int) { // 修改p的成员变量 p.value = v } // 取属性值 func (p Property) Value() int { return p.value } func main() { // 实例化属性 p := new(Property) // 设置值 p.SetValue(100) // 打印值 fmt.Println(p.Value()) // 100 }
<a name="3ynXO"></a>
#### 非指针类型 接收器
1. 会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效。
```go
package main
import (
"fmt"
)
// 定义点结构
type Point struct {
X int
Y int
}
// 非指针接收器的加方法
func (p Point) Add(other Point) Point {
// 成员值与参数相加后返回新的结构
return Point{p.X + other.X, p.Y + other.Y}
}
func main() {
// 初始化点
p1 := Point{1, 1}
p2 := Point{2, 2}
// 与另外一个点相加
result := p1.Add(p2)
// 输出结果
fmt.Println(result) // {3 3}
}
为基本类型添加方法
package main
import (
"fmt"
)
// 将int定义为MyInt类型
type MyInt int
// 为MyInt添加IsZero()方法
func (m MyInt) IsZero() bool {
return m == 0
}
// 为MyInt添加Add()方法
func (m MyInt) Add(other int) int {
return other + int(m)
}
func main() {
var b MyInt
fmt.Println(b.IsZero())
b = 1
fmt.Println(b.Add(2))
}
http.Header
http.Header,就是典型的自定义类型,并且拥有自己的方法:
type Header map[string][]string
func (h Header) Add(key, value string) {
textproto.MIMEHeader(h).Add(key, value)
}
func (h Header) Set(key, value string) {
textproto.MIMEHeader(h).Set(key, value)
}
func (h Header) Get(key string) string {
return textproto.MIMEHeader(h).Get(key)
}
类型内嵌
介绍
- 结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时内嵌结构体的字段名为类型名。匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。
- 可以粗略地将这个和面向对象语言中的继承概念相比较,Go语言的结构体内嵌特性就是一种组合特性,使用组合特性可以快速构建对象的不同特性,从而实现类似继承功能。
- 一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。
- 嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如,ins.a.b.c的访问可以简化为ins.c。内嵌结构体字段仍然可以使用详细的字段进行一层层访问,
package main
import "fmt"
type innerS struct {
in1 int
in2 int
}
type outerS struct {
b int
c float32
int // anonymous field
innerS //anonymous field
}
func main() {
outer := new(outerS)
outer.b = 6
outer.c = 7.5
outer.int = 60
outer.in1 = 5
outer.in2 = 10
fmt.Printf("outer.b is: %d\n", outer.b)
fmt.Printf("outer.c is: %f\n", outer.c)
fmt.Printf("outer.int is: %d\n", outer.int)
fmt.Printf("outer.in1 is: %d\n", outer.in1)
fmt.Printf("outer.in2 is: %d\n", outer.in2)
// 使用结构体字面量
outer2 := outerS{6, 7.5, 60, innerS{5, 10}}
fmt.Printf("outer2 is:", outer2)
}
// 结果如下:
outer.b is: 6
outer.c is: 7.500000
outer.int is: 60
outer.in1 is: 5
outer.in2 is: 10
outer2 is:{6 7.5 60 {5 10}}
内嵌成员名字冲突
在使用内嵌结构体时,Go语言的编译器会非常智能地提醒我们可能发生的歧义和错误。
type A struct {
a int
}
type B struct {
a int
}
type C struct {
A
B
}
func main() {
c := &C{}
c.a = 1 // 报错
c.A.a = 1 //正常
fmt.Println(c)
}
当使用如上错误方式时,编译阶段会报错:.\main.go:13:3: ambiguous selector c.a
类型内建
类型内建
- 属于定义一个新的类型,内建后的类型本身依然具备内建类型的特性,但没有原类型的任何方法。
type newType oldType
//demo
type byte uint8
type rune int32
类型别名
- 类型别名是 Go 1.9 版本添加的新功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。
- TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型,就像一个孩子小时候有小名。
- 类型别名 不属于定义新类型,所以拥有原类型所有方法。
- 非本地类型不能定义方法。
type TypeAlias = Type
//demo
type byte = uint8
type rune = int32
对比
类型内建 与 类型别名 表面上看只有一个等号的差异,那么它们之间实际的区别有哪些呢?
类型不一致
// 将NewInt定义为int类型
type NewInt int
// 将int取一个别名叫IntAlias
type IntAlias = int
func main() {
// 将a声明为NewInt类型
var a1 NewInt
// 查看a的类型名
fmt.Printf("a1 type: %T\n", a1) // a1 type: main.NewInt
// 将a2声明为IntAlias类型
var a2 IntAlias
// 查看a2的类型名
fmt.Printf("a2 type: %T\n", a2) // a2 type: int
}
- a1 类型是 main.NewInt,表示 main 包下定义的 NewInt 类型,本身依然具备 int 类型的特性。
- a2 类型是 int,IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。
方法不一致
类型内建
类型内建 属于定义一个新的类型,虽然拥有原类型的特性,但内建后,原类型的所有方法将无法访问。
type MyDuration time.Duration
func main() {
var d MyDuration // d 中没有任何方法。
}
类型别名
- 类型别名 不属于定义新类型,所以拥有原类型所有方法。
- 非本地类型不能定义方法。
编译代码,panic:cannot define new methods on non-local type time.Duration// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration
// 为MyDuration添加一个函数
func (m MyDuration) EasySet(a string) {
}
func main(){
var d MyDuration
fmt.Printf(d.Seconds())
}
编译器提示:不能在一个非本地的类型 time.Duration 上定义新方法,非本地类型指的就是 time.Duration 不是在 main 包中定义的,而是在 time 包中定义的,与 main 包不在同一个包中,因此不能为不在一个包中的类型定义方法。
内嵌时不一致
package main
import (
"fmt"
"reflect"
)
// 定义商标结构
type Brand struct {
}
// 为商标结构添加Show()方法
func (t Brand) Show() {
}
// 为Brand定义一个别名FakeBrand
type FakeBrand = Brand
// 定义车辆结构
type Vehicle struct {
// 嵌入两个结构
FakeBrand
Brand
}
func main() {
// 声明变量a为车辆类型
var a Vehicle
// 指定调用FakeBrand的Show
a.FakeBrand.Show()
// 取a的类型反射对象
ta := reflect.TypeOf(a)
// 遍历a的所有成员
for i := 0; i < ta.NumField(); i++ {
// a的成员信息
f := ta.Field(i)
// 打印成员的字段名和类型
fmt.Printf("FieldName: %v, FieldType: %v\n", f.Name, f.Type.
Name())
}
}
// log
FieldName: FakeBrand, FieldType: Brand
FieldName: Brand, FieldType: Brand
- FakeBrand 是 Brand 的一个别名,在 Vehicle 中嵌入 FakeBrand 和 Brand 并不意味着嵌入两个 Brand,FakeBrand 的类型会以名字的方式保留在 Vehicle 的成员中。
- 如果将 25 行 改为 :
a.Show()
,编译报错panic:ambiguous selector a.Show。在调用 Show() 方法时,因为两个类型都有 Show() 方法,会发生歧义,证明 FakeBrand 的本质确实是 Brand 类型。