Go 结构体

类型别名&定制

类型别名

   类型别名是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 北京市海淀区}}
}

小题目

   使用“面向对象”的思维方式编写一个学生信息管理系统。

  1. 学生有id、姓名等信息
  2. 程序提供展示学生列表、添加学生、编辑学生信息、删除学生等功能

   main.go

package main

import (
	"fmt"
	"os"
)

type student struct {
	id   int64
	name string
}


func showMenu() {
	fmt.Println("welcome sms!")
	fmt.Println(`
		1.查看所有学生
		2.添加学生
		3.修改学生
		4.删除学生
		5.退出
	`)
}

func main() {
	smr := studentMgr{
		allStudent: make(map[int64]student, 50), // 实例化出一个map,最多容纳50组键值对
	}

	for {

		showMenu()
		// 获取输入
		fmt.Print("输入您的选项>>>")
		var choice int
		fmt.Scanln(&choice)
		switch choice {
		case 1:
			smr.showStudent()
		case 2:
			smr.addStudent()
		case 3:
			smr.editStudent()
		case 4:
			smr.deleteStudent()
		case 5:
			os.Exit(1) // 退出
		default:
			fmt.Println("输入有误...")
		}
	}
}

   studentMgr.go

package main

import "fmt"

type studentMgr struct {
	allStudent map[int64]student
	// 管理者 增删改查
}

// 查
func (s studentMgr) showStudent() {
	for _, stu := range s.allStudent {
		fmt.Printf("学号:%d 姓名:%s \n", stu.id, stu.name)
	}
}

// 增
func (s studentMgr) addStudent() {
	var (
		stuID   int64
		stuName string
	)
	fmt.Print("请输入学号>>>")
	fmt.Scanln(&stuID)
	fmt.Print("请输入姓名>>>")
	fmt.Scanln(&stuName)
	newStu := student{
		id:   stuID,
		name: stuName,
	}
	s.allStudent[newStu.id] = newStu
}

// 改
func (s studentMgr) editStudent() {
	var stuID int64
	fmt.Print("请输入学号>>>")
	fmt.Scanln(&stuID)
	stuObj, ok := s.allStudent[stuID] // 值拷贝
	if !ok {
		fmt.Println("没有该学生")
		return
	}
	fmt.Printf("学生信息如下:\n  学号:%d\n  姓名:%s\n", stuObj.id, stuObj.name)
	fmt.Print("请输入学生的新名字>>>")
	var newName string
	fmt.Scanln(&newName)
	stuObj.name = newName
	s.allStudent[stuID] = stuObj
}

// 删
func (s studentMgr) deleteStudent() {
	var stuID int64
	fmt.Print("请输入要删除的学生学号>>>")
	fmt.Scanln(&stuID)
	_, ok := s.allStudent[stuID]
	if !ok {
		fmt.Println("没有该学生")
		return
	}
	delete(s.allStudent, stuID)
	fmt.Println("删除成功")
}

   测试使用命令:

go run main.go studentMgr.go
posted @ 2020-10-07 16:42  云崖先生  阅读(237)  评论(0编辑  收藏  举报