18

Go 结构体

 3 years ago
source link: http://www.cnblogs.com/Yunya-Cnblogs/p/13777898.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

类型别名&定制

类型别名

类型别名是 Go 的1.9版本中新添加的功能。

大概意思就是给一个类型取一个别名,小名等,但是这个别名还是指向的相同类型。

uint32 的别名 rune ,其底层还是 uint32

uint8 的别名 byte ,使用 byte 实际上还是 uint8

别名的作用在于在编程中更方便的进行使用类型,如下示例,我们为 int64 取一个别名 long

package main

import (
	"fmt"
)

func main() {
	type long = int64
	var num long
	num = 100000000
	fmt.Printf("%T %v", num, num)
	// int64 100000000
}

自定类型

自定类型类似于继承某个内置的类型,它会以一种全新的类型出现,并且我们可以为该自定类型做一些定制方法。

如下定义的 small 类型,是基于 uint8 的一个类型。它是一种全新的类型,但是具有 uint8 的特性。

package main

import (
	"fmt"
)

func main() {
	type small uint8
	var num small = 32
	fmt.Printf("%T %v", num, num)
	// main.small 32
}

区别差异

可以看到上面示例中的打印结果

// int64 100000000
// main.small 32

结果显示自定义类型是 main.small ,其实自定义类型只在代码中存在,编译时会将其转换为 uint8

结构体

结构体类似于其他语言中的面向对象,值得一提的是 Go 语言是一种面向接口的语言,所以弱化了对面向对象方面的处理。

在结构体中,我们可以清晰的表示一个现实中的事物,注意:结构体其实就是一种自定义的类型。

Go 中使用 struct 来定义结构体。

以下是语法介绍, typestruct 这两个关键字用于定义结构体。

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}
 类型名:标识自定义结构体的名称,在同一个包内不能重复。如果不是对外开放的接口,则首字母小写,否则大写。
 字段名:表示结构体字段名。结构体中的字段名必须唯一。
 字段类型:表示结构体字段的具体类型。

下面我们来定义一个 dog 的结构体。

// 命名小写,表示dog结构体不对外开放
type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

.实例化

当结构体定义完成后,必须对其进行实例化后才可使用。

单纯的定义结构体是不会分配内存的。

以下将介绍通过 . 进行实例化。

基本实例化

下面是基本实例化的示例。

首先定义一个变量,声明它是 dog 类型,再通过 . 对其中的字段进行赋值。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	var d1 dog
	d1.dogName = "大黄"
	d1.dogAge = 12
	d1.dogGender = true
	fmt.Println(d1)
	// {大黄 12 true}
}

匿名结构体

有的结构体只使用一次,那么就可以使用匿名结构体在定义之初对其进行实例化。

这个时候只使用 stuct 即可,不必使用 type 进行类型的自定义。

package main

import (
	"fmt"
)


func main() {
	var dog struct{
		dogName string 
		dogAge int8
		dogGender bool
	}
	dog.dogName = "大黄"
	dog.dogAge = 12
	dog.dogGender = true
	fmt.Println(dog)
	// {大黄 12 true}
}

指针实例化

通过 new 可以对结构体进行实例化,具体步骤是拿到其结构体指针后通过 . 对其字段填充,进而达到实例化的目的。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	var d1 = new(dog) // 拿到结构体指针
	d1.dogName = "大黄"
	d1.dogAge = 12
	d1.dogGender = true
	fmt.Println(d1)
	// &{大黄 12 true}
}

地址实例化

使用 & 对结构体进行取地址操作相当于对该结构体类型进行了一次 new 实例化操作。

与上面的方式本质都是相同的。

d1.dogName= "大黄" 其实在底层是 (*d1).dogName= "大黄" ,这是 Go 语言帮我们实现的语法糖。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	d1 := &dog{}
	d1.dogName = "大黄"
	d1.dogAge = 12
	d1.dogGender = true
	fmt.Println(d1)
	// &{大黄 12 true}
}

{}实例化

实例化时,除开可以使用 . 也可以使用 {}

在实际开发中使用 {} 实例化的普遍更多。

基本实例化

以下是使用 {} 进行基本实例化的示例。

key 对应字段名, value 对应实例化的填充值。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	d1 := dog{
		dogName:"大黄",
		dogAge:12,
		dogGender:true,
	}
	fmt.Print(d1)
}

顺序实例化

可以不填入 key 对其进行实例化,但是要与定义结构体时的字段位置一一对应。

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。
package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	d1 := dog{
		"大黄",
		12,
		true,
	}
	fmt.Print(d1)
}

地址实例化

下面是使用 {} 进行地址实例化。

package main

import (
	"fmt"
)

type dog struct{
	dogName string 
	dogAge int8
	dogGender bool
}

func main() {
	d1 := &dog{
		"大黄",
		12,
		true,
	}
	fmt.Print(d1)
}

内存布局

连续内存

一个结构体中的字段,都是占据一整块连续内存。

但有的字段可能看起来不会与前一个字段进行相邻,这是受到类型的影响,具体可查看: 在 Go 中恰到好处的内存对齐

package main

import (
	"fmt"
)

type dog struct {
	dogName   string
	dogAge    int8
	dogGender bool
}

func main() {
	d1 := &dog{
		"大黄",
		12,
		true,
	}
	fmt.Printf("%p \n", &d1.dogName)
	fmt.Printf("%p \n", &d1.dogAge)
	fmt.Printf("%p \n", &d1.dogGender)

	// 0xc0000044a0
	// 0xc0000044b0
	// 0xc0000044b1

}

空结构体

一个空的结构体是不占据任何内存的。

package main

import (
	"fmt"
	"unsafe"
)


func main() {
	var dog struct{}
	fmt.Print(unsafe.Sizeof(dog)) // 0 查看占据的内存
}

构造函数

当一个函数返回一个结构体实例时,该函数将被称为构造函数。

Go 语言中没有构造函数,但是我们可以自己进行定义。

注意构造函数的命名方式要用 new 进行开头。

普通构造

由于函数的参数传递都是值传递,所以每次需要将结构体拷贝到构造函数中,这是非常消耗内存的。

所以在真实开发中不应该使用这种方式

package main

import (
	"fmt"
)

type dog struct {
	dogName string
	dogAge int8
	dogGender bool
}

// dog每次都会进行拷贝,消耗内存
func newDog(dogName string, dogAge int8, dogGender bool) dog {
	return dog{
		dogName: dogName,
		dogAge: dogAge,
		dogGender: dogGender,
	}
}

func main(){
	d1 := newDog("大黄",12,true)
	fmt.Printf("%p \n",&d1)
}

指针构造

如果让其使用指针构造,就不用每次都会进行拷贝了。推荐使用该方式。

只需要加上 *& 即可。

package main

import (
	"fmt"
)

type dog struct {
	dogName string
	dogAge int8
	dogGender bool
}

// 每次的传递都是dog的指针,所以不会进行值拷贝
func newDog(dogName string, dogAge int8, dogGender bool) *dog {
	return &dog{
		dogName: dogName,
		dogAge: dogAge,
		dogGender: dogGender,
	}
}

func main(){
	d1 := newDog("大黄",12,true)
	fmt.Printf("%p \n",&d1)
}

方法&接收者

为任意类型定制一个自定义方法,必须要为该类型进行接收者限制。

接收者类似于其他语言中的 selfthis

定义方法格式如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}

接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是 selfthis 之类的命名。例如, Person 类型的接收者变量应该命名为 pConnector 类型的接收者变量应该命名为 c 等。

接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。

方法名、参数列表、返回参数:具体格式与函数定义相同。

普通接收者

以下示例是定义普通接收者方法。

注意,这是值拷贝,意味着你的 d 会拷贝 d1 的数据。

package main

import (
	"fmt"
)

type dog struct {
	dogName string
	dogAge int8
	dogGender bool
}

func newDog(dogName string, dogAge int8, dogGender bool) *dog {
	return &dog{
		dogName: dogName,
		dogAge: dogAge,
		dogGender: dogGender,
	}
}

func (d dog)getAge() int8 {
	return d.dogAge  // 返回狗狗的年龄
}

func main(){
	d1 := newDog("大黄",12,true)
	age := d1.getAge()
	fmt.Print(age) // 12
}

指针接收者

由于普通接收者方法无法做到修改原本实例化对象数据的需求,所以我们可以定义指针接收者方法进行引用传递。

如下,调用 addAge() 方法会将原本的年龄加上十岁。

package main

import (
	"fmt"
)

type dog struct {
	dogName   string
	dogAge    int8
	dogGender bool
}

func newDog(dogName string, dogAge int8, dogGender bool) *dog {
	return &dog{
		dogName:   dogName,
		dogAge:    dogAge,
		dogGender: dogGender,
	}
}

func (d *dog) addAge() {
	d.dogAge += 10
}

func main() {
	d1 := newDog("大黄", 12, true)
	fmt.Printf("旧年龄:%v", d1.dogAge) // 12
	d1.addAge()
	fmt.Printf("新年龄:%v", d1.dogAge) // 22
}

关于使用 d1 直接调用,这是一种语法糖形式。完整的形式应该是使用 & 取到地址后再进行传递,但是这样会出现一些问题。

所以直接使用实例化对象调用即可。

下面是关于指针方法的一些使用注意事项:

  1. 修改原本实例化对象中的值时,应该使用指针接收者方法
  2. 实例化对象的内容较多,拷贝代价较大时,应该使用指针接收者方法
  3. 如果该对象下某个方法是指针接收者方法,那么为了保持一致性,其他方法也应该使用指针方法。

自定类型方法

下面将对自定义类型 small 做一个方法, getBool 获取其布尔值。

package main

import (
	"fmt"
)

type small uint8


func (s small) getBool() bool {
	if s != 0 {
		return true
	}
	return false
}

func main() {
	var num small
	num = 18
	result := num.getBool()
	fmt.Print(result) // true
}

匿名字段

基本使用

匿名字段即只使用字段类型,不使用字段名。

使用较少

注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

package main

import (
	"fmt"
)

type dog struct {
	string // 只能出现一次同类型的字段。
	int8
	bool
}

func main() {
	d1 := dog{
		string: "大黄",
		int8:   12,
		bool:   true,
	}
	fmt.Print(d1)
}

结构体嵌套

基本使用

一个结构体中可以嵌套另一个结构体。

通过这种方式,可以达到继承的效果。

package main

import (
	"fmt"
)

type details struct {
	phone string // 电话
	addr  string // 地址
}

type person struct {
	name    string
	gender  bool
	age     int8
	details // 匿名字段,详细信息
}

func main() {
	p1 := person{
		name:   "云崖",
		gender: true,
		age:    18,
		details: details{  // 对匿名字段的嵌套结构体进行实例化
			phone: "1008611",
			addr:  "北京市海淀区",
		},
	}
	fmt.Print(p1)
	// {云崖 true 18 {1008611 北京市海淀区}}
}

匿名简写

如果要访问上例中的电话,可以使用简写形式。也可以使用全写形式。

查找顺序是先查找具名字段,再查找匿名字段。

要注意多个结构体嵌套产生的字段名冲突问题。

package main

import (
	"fmt"
)

type details struct {
	phone string // 电话
	addr  string // 地址
}

type person struct {
	name    string
	gender  bool
	age     int8
	details // 匿名字段,详细信息
}

func main() {
	p1 := person{
		name:   "云崖",
		gender: true,
		age:    18,
		details: details{  // 对匿名字段的嵌套结构体进行实例化
			phone: "1008611",
			addr:  "北京市海淀区",
		},
	}
	fmt.Println(p1.phone)  // 简写
	fmt.Println(p1.details.phone) // 全写

}

JSON

使用 JSON 包可对结构体进行序列化操作。

常用于前后端数据交互。

字段可见性

由于 json 包是再 encoding/json 中,所以我们要想让 main 包的结构体能被 json 包访问,需要将结构体名字,字段名字等进行首字母大写。

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

基本使用

以下是关于 JSON 序列化与反序列化的基本使用。

package main

import (
	"encoding/json" // 导包
	"fmt"
)

// Details 详情  对于大写的结构体,应该具有注释。注意空格
type Details struct {
	Phone string // 电话
	Addr  string // 地址
}

// Person 人
type Person struct {
	Name    string
	Gender  bool
	Age     int8
	Details // 匿名字段,详细信息
}

func main() {
	p1 := Person{
		Name:   "云崖",
		Gender: true,
		Age:    18,
		Details: Details{ // 对匿名字段的嵌套结构体进行实例化
			Phone: "1008611",
			Addr:  "北京市海淀区",
		},
	}
	// 序列化 得到一个[]bytes类型
	data, err := json.Marshal(p1)
	if err != nil {
		fmt.Println("json error")
		return
	}
	fmt.Println(string(data)) // 查看结果
	// {"Name":"云崖","Gender":true,"Age":18,"Phone":"1008611","Addr":"北京市海淀区"}

	// 反序列化
	p2 := Person{}
	json.Unmarshal(data, &p2) // 反序列化时需要实例化出该结构体。通过地址对其进行赋值
	fmt.Println(p2)           // {云崖 true 18 {1008611 北京市海淀区}}
}

标签使用

我们可以看到上面的序列化后的结果字段名都是大写名字开头的。

{"Name":"云崖","Gender":true,"Age":18,"Phone":"1008611","Addr":"北京市海淀区"}

怎么样把它转换为小写?这个需要使用到结构体标签。

示例如下:

package main

import (
	"encoding/json" // 导包
	"fmt"
)

// Details 详情  对于大写的结构体,应该具有注释。注意空格
type Details struct {
	// 代表在json转换中,Phone更名为phone  orm中更名为phone  配置文件ini中更名为phone
	Phone string `json:"phone" db:"phone" ini:"phone"`
	Addr  string `json:"addr"`
}

// Person 人
type Person struct {
	Name    string `json:"name"`
	Gender  bool   `json:"gender"`
	Age     int8   `json:"age"`
	Details        // 匿名字段,详细信息
}

func main() {
	p1 := Person{
		Name:   "云崖",
		Gender: true,
		Age:    18,
		Details: Details{ // 对匿名字段的嵌套结构体进行实例化
			Phone: "1008611",
			Addr:  "北京市海淀区",
		},
	}
	// 序列化 得到一个[]bytes类型
	data, err := json.Marshal(p1)
	if err != nil {
		fmt.Println("json error")
		return
	}
	fmt.Println(string(data)) // 查看结果
	// {"name":"云崖","gender":true,"age":18,"phone":"1008611","addr":"北京市海淀区"}

	// 反序列化
	p2 := Person{}
	json.Unmarshal(data, &p2) // 反序列化时需要实例化出该结构体。通过地址对其进行赋值
	fmt.Println(p2)           // {云崖 true 18 {1008611 北京市海淀区}}
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK