【Go】Go语言结构体


一、前言

Go语言中没有 “类” 的概念,也不支持 “类” 的继承等面向对象的概念。
Go语言中通过 结构体的内嵌 再配合 接口 比面向对象具有更高的扩展性和灵活性。


二、结构体

Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。

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

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种 自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。(什么是自定义数据类型?请参见【Go】Go语言 类型别名和自定义类型

Go语言中通过结构体来实现面向对象。

实例:
结构体可以表示一项记录,比如保存图书馆的书籍记录,每本书有以下属性:

  • Title :标题
  • Author : 作者
  • Subject:学科
  • ID:书籍ID

结构体是 值类型 的数据结构。


三、定义结构体

结构体定义需要使用 typestruct 关键字。
struct 语句定义一个新的结构体类型,结构体中有一个或多个成员。
type 语句设定了该结构体类型的名称。
结构体的格式如下:

type 结构体类型名 struct {
    
    
	字段名1 字段类型1
    字段名2 字段类型2}

其中:

  1. 类型名:标识自定义结构体的名称,在同一个包内不能重复。

  2. 字段名:表示结构体字段名。结构体中的字段名必须唯一。

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

  3. 字段类型:表示结构体字段的具体类型。

举个例子,我们定义一个Person(人)结构体,代码如下:

type person struct {
    
    
    name string
    city string
    age  int8
}

同样类型的字段也可以写在一行:

type person struct {
    
    
    name, city string
    age        int8
}

这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人的信息了。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型。

Go语言中有两个聚合数据类型,一个是数组,一个是结构体。


四、结构体初始化

结构体只是我们自己定义的一个数据类型,和内置的 int 等数据类型是一样的。
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

1. 结构体默认初始化

结构体本身也是一种类型(我们自定义的类型),我们可以像声明内置类型一样使用var关键字声明结构体类型。

var 结构体实例 结构体类型

实例:

package main

import "fmt"

type person struct {
    
    
	name string
	city string
	age  int8
}

func main() {
    
    
	var p4 person
	fmt.Printf("p4=%#v\n", p4) 
}

输出结果:

p4=main.person{
    
    name:"", city:"", age:0}

2. 使用值或键值对初始化结构体

声明+初始化:

//1. 使用值初始化
结构体实例 := 结构体类型 {
    
    value1, value2,...,valuen}//2. 使用键值对初始化
结构体实例 := 结构体类型 {
    
     member1: value1, member2: value2..., membern: valuen}

//3. 对结构体指针进行键值对初始化
结构体实例的指针 := &结构体类型 {
    
     member1: value1, member2: value2..., membern: valuen}

方法1:

  1. 必须初始化结构体的所有字段。(如果少值会报错:too few values in 结构体类型{…})
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。

方法2、3:
可以不初始化结构体的所有字段。当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。

实例:

package main

import "fmt"

type person struct {
    
    
	name string
	city string
	age  int8
}

func main() {
    
    
	p5 := person{
    
    
		name: "pprof.cn",
		city: "北京",
		age:  18,
	}
	fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"pprof.cn", city:"北京", age:18}
}

输出结果:

p5=main.person{
    
    name:"pprof.cn", city:"北京", age:18}

也可以对结构体指针进行键值对初始化,实例:

package main

import "fmt"

type person struct {
    
    
	name string
	city string
	age  int8
}

func main() {
    
    
	p6 := &person{
    
    
		name: "pprof.cn",
		city: "北京",
		age:  18,
	}
	fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"pprof.cn", city:"北京", age:18}
}

输出结果:

p6=&main.person{
    
    name:"pprof.cn", city:"北京", age:18}

3. 用访问成员的方式初始化结构体

我们通过.来访问结构体的字段(成员变量),例如p1是一个结构体实例,p1.name就可以访问其name字段。

实例:

package main

import (
	"fmt"
)

type person struct {
    
    
	name string
	city string
	age  int8
}

func main() {
    
    
	var p1 person
	fmt.Printf("p1=%v\n", p1)

	p1.name = "pprof.cn"
	p1.city = "北京"
	p1.age = 18
	fmt.Printf("p1=%v\n", p1)
	fmt.Printf("p1=%#v\n", p1)
}

输出结果:

p1={
    
      0}
p1={
    
    pprof.cn 北京 18}
p1=main.person{
    
    name:"pprof.cn", city:"北京", age:18}

五、访问结构体成员

  1. 如果要访问结构体成员,需要使用点号 . 操作符,格式为:
结构体.成员名
  1. Go语言也支持在结构体指针上使用点号 . 操作符来访问结构体成员。
结构体指针.成员名

实例:

package main

import "fmt"

//Books是结构体类型
type Books struct {
    
    
	title   string
	author  string
	subject string
	book_id int
}

func main() {
    
    
	var Book1 Books /* 声明 Book1 为 Books 类型 */
	var Book2 Books /* 声明 Book2 为 Books 类型 */

	/* book 1 描述 */
	Book1.title = "Go 语言"
	Book1.author = "www.runoob.com"
	Book1.subject = "Go 语言教程"
	Book1.book_id = 6495407

	/* book 2 描述 */
	Book2.title = "Python 教程"
	Book2.author = "www.runoob.com"
	Book2.subject = "Python 语言教程"
	Book2.book_id = 6495700

	/* 打印 Book1 信息 */
	fmt.Printf("Book 1 title : %s\n", Book1.title)
	fmt.Printf("Book 1 author : %s\n", Book1.author)
	fmt.Printf("Book 1 subject : %s\n", Book1.subject)
	fmt.Printf("Book 1 book_id : %d\n", Book1.book_id)

	/* 打印 Book2 信息 */
	fmt.Printf("Book 2 title : %s\n", (&Book2).title)
	fmt.Printf("Book 2 author : %s\n", (&Book2).author)
	fmt.Printf("Book 2 subject : %s\n", (&Book2).subject)
	fmt.Printf("Book 2 book_id : %d\n", (&Book2).book_id)
}

输出结果:

Book 1 title : Go 语言
Book 1 author : www.runoob.com
Book 1 subject : Go 语言教程
Book 1 book_id : 6495407
Book 2 title : Python 教程
Book 2 author : www.runoob.com
Book 2 subject : Python 语言教程
Book 2 book_id : 6495700

六、匿名结构体 与 结构体的匿名字段

匿名结构体

在定义一些临时数据结构等场景下还可以使用匿名结构体。

import (
	"fmt"
)

func main() {
    
    
	var user struct {
    
    
		Name string
		Age  int
	}
	user.Name = "pprof.cn"
	user.Age = 18
	fmt.Printf("%#v\n", user)
}

输出结果:

struct {
    
     Name string; Age int }{
    
    Name:"pprof.cn", Age:18}

结构体的匿名字段

结构体允许其成员字段在声明时 没有字段名只有类型 ,这种没有名字的字段就称为匿名字段。

package main

import (
	"fmt"
)

//Person 结构体
type Person struct {
    
    
	string
	int
}

func main() {
    
    
	p1 := Person{
    
    
		"pprof.cn",
		18,
	}
	fmt.Printf("%#v\n", p1)        //main.Person{string:"pprof.cn", int:18}
	fmt.Println(p1.string, p1.int) //pprof.cn 18
}

输出结果:

main.Person{
    
    string:"pprof.cn", int:18}
pprof.cn 18

匿名字段 默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中 同种类型的匿名字段只能有一个


七、结构体指针

1. 使用 new 实例化结构体,返回指向结构体的指针

我们可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

package main

import (
	"fmt"
)

type person struct {
    
    
	name string
	city string
	age  int8
}

func main() {
    
    
	var p2 = new(person)
	fmt.Printf("%T\n", p2)     
	fmt.Printf("p2=%#v\n", p2) 
}

输出结果:

*main.person
p2=&main.person{
    
    name:"", city:"", age:0}

从打印的结果中我们可以看出 p2 是一个结构体指针。

需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。

package main

import (
	"fmt"
)

type person struct {
    
    
	name string
	city string
	age  int8
}

func main() {
    
    
	var p2 = new(person)  //p2 是一个结构体指针
	p2.name = "测试"      //对结构体指针直接使用`.`来访问结构体的成员
	p2.age = 18
	p2.city = "北京"
	fmt.Printf("p2=%#v\n", p2)
}

输出结果:

p2=&main.person{
    
    name:"测试", city:"北京", age:18}

p2.name = "测试"其实在底层是(*p2).name = "测试",这是Go语言帮我们实现的语法糖。

2. 取结构体的地址实例化

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

package main

import (
	"fmt"
)

type person struct {
    
    
	name string
	city string
	age  int8
}

func main() {
    
    
	p3 := &person{
    
    }
	fmt.Printf("%T\n", p3)     //*main.person
	fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
	p3.name = "博客"
	p3.age = 30
	p3.city = "成都"
	fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"博客", city:"成都", age:30}
}

输出结果:

*main.person
p3=&main.person{
    
    name:"", city:"", age:0}
p3=&main.person{
    
    name:"博客", city:"成都", age:30}

我们现在剖析一下以上实例涉及的步骤细节:

  1. 你可以声明 指向结构体的指针 类似于其他指针变量,该指针变量可以存储结构体变量的地址。
    然后通过将 & 符号放置于结构体变量前来查看结构体变量地址,并赋给指针:
//声明
var struct_pointer *结构体名
//初始化
struct_pointer = &结构体实例

或
//声明+初始化
struct_pointer := &结构体名{
    
    这里可以初始化值}
  1. 使用指针访问成员:
    使用结构体指针访问结构体成员,使用 “.” 操作符:
struct_pointer.title

Go语言帮我们在底层实现了(*struct_pointer).title


八、结构体作为函数参数

  1. 实例1:将结构体作为函数参数

你可以像其他数据类型一样将结构体类型作为参数传递给函数。可用以上实例的方式访问结构体变量:

package main

import "fmt"

type Books struct {
    
    
	title   string
	author  string
	subject string
	book_id int
}

func main() {
    
    
	var Book1 Books /* 声明 Book1 为 Books 类型 */
	var Book2 Books /* 声明 Book2 为 Books 类型 */

	/* book 1 描述 */
	Book1.title = "Go 语言"
	Book1.author = "www.runoob.com"
	Book1.subject = "Go 语言教程"
	Book1.book_id = 6495407

	/* book 2 描述 */
	Book2.title = "Python 教程"
	Book2.author = "www.runoob.com"
	Book2.subject = "Python 语言教程"
	Book2.book_id = 6495700

	/* 打印 Book1 信息 */
	printBook(Book1)

	/* 打印 Book2 信息 */
	printBook(Book2)
}

func printBook(book Books) {
    
    
	fmt.Printf("Book title : %s\n", book.title)
	fmt.Printf("Book author : %s\n", book.author)
	fmt.Printf("Book subject : %s\n", book.subject)
	fmt.Printf("Book book_id : %d\n", book.book_id)
}

输出结果:

Book title : Go 语言
Book author : www.runoob.com
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : www.runoob.com
Book subject : Python 语言教程
Book book_id : 6495700
  1. 实例2:将结构体指针作为函数参数
package main

import "fmt"

type Books struct {
    
    
	title   string
	author  string
	subject string
	book_id int
}

func main() {
    
    
	var Book1 Books /* 声明 Book1 为 Books 类型 */
	var Book2 Books /* 声明 Book2 为 Books 类型 */

	/* book 1 描述 */
	Book1.title = "Go 语言"
	Book1.author = "www.runoob.com"
	Book1.subject = "Go 语言教程"
	Book1.book_id = 6495407

	/* book 2 描述 */
	Book2.title = "Python 教程"
	Book2.author = "www.runoob.com"
	Book2.subject = "Python 语言教程"
	Book2.book_id = 6495700

	/* 打印 Book1 信息 */
	//地址就是一个指针,指针的实质就是一个地址
	printBook(&Book1)

	/* 打印 Book2 信息 */
	printBook(&Book2)
}
func printBook(book *Books) {
    
    
	fmt.Printf("Book title : %s\n", book.title)
	fmt.Printf("Book author : %s\n", book.author)
	fmt.Printf("Book subject : %s\n", book.subject)
	fmt.Printf("Book book_id : %d\n", book.book_id)
}

输出结果:

Book title : Go 语言
Book author : www.runoob.com
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : www.runoob.com
Book subject : Python 语言教程
Book book_id : 6495700

九、结构体内存布局

package main

import "fmt"

type test struct {
    
    
	a int8
	b int8
	c int8
	d int8
}

func main() {
    
    
	n := test{
    
    
		1, 2, 3, 4,
	}
	fmt.Printf("n.a %p\n", &n.a)
	fmt.Printf("n.b %p\n", &n.b)
	fmt.Printf("n.c %p\n", &n.c)
	fmt.Printf("n.d %p\n", &n.d)
}

输出结果:

n.a 0xc00000a198
n.b 0xc00000a199
n.c 0xc00000a19a
n.d 0xc00000a19b

十、构造函数

构造函数是一种特殊的函数,用来在对象实例化的时候初始化对象的成员变量。

Go语言的结构体没有构造函数,我们可以自己实现。

例如,下方的代码就实现了一个 person 的构造函数。 因为 struct 是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

func newPerson(name, city string, age int8) *person {
    
    
    return &person{
    
    
        name: name,
        city: city,
        age:  age,
    }
}

调用构造函数

p9 := newPerson("pprof.cn", "测试", 90)
fmt.Printf("%#v\n", p9)

完整代码:

package main

import "fmt"

type person struct {
    
    
	name string
	city string
	age  int8
}

func main() {
    
    
	p9 := newPerson("pprof.cn", "测试", 90)
	fmt.Printf("%#v\n", p9)

}

//构造函数,用于初始化结构体person
func newPerson(name, city string, age int8) *person {
    
    
	return &person{
    
    
		name: name,
		city: city,
		age:  age,
	}
}

输出结果:

&main.person{
    
    name:"pprof.cn", city:"测试", age:90}

十一、方法和接收器

为什么要在结构体这一节,讲 Go语言中的方法呢?

在Go语言中,结构体 就像是 的一种简化形式,那么类的方法在哪里呢?

Go语言中的方法是作用在接收器(receiver)上的一个函数。接收器是某种类型的变量,方法是作用在其上的函数。如果我们能在结构体类型上定义一个方法,那么就回答了我们上面的问题。

接收器类型可以是几乎任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型,但是 接收器不能是一个接口类型 ,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误 invalid receiver type…。

一个类型加上它的方法等价于面向对象中的一个类,一个重要的区别是,在Go语言中,类型的代码和绑定在它上面的方法的代码可以 放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是 同一个包 的。我们不能给别的包的类型定义方法。

类型 T 上的所有方法的集合叫做类型 T 的方法集。

因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法,但是如果基于接收器类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收器类型上存在,比如在同一个包里这么做是允许的。

提示:
在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在Go语言中“方法”的概念与其他语言一致,只是Go语言建立的“接收器”强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。有没有接收器也是Go语言中函数和方法的本质区别。

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

1. 为结构体添加方法

接下来使用背包作为“对象”,将物品放入背包的过程作为“方法”,通过面向过程的方式和Go语言中结构体的方式来理解Go语言中“方法”的概念。

(1)面向过程实现方法

面向过程中没有 “方法” 概念,只能通过结构体和函数,由使用者使用函数参数和调用关系来形成接近 “方法” 的概念,代码如下:

//背包结构体
type Bag struct {
    
    
    items []int
}

// 将一个物品放入背包的函数
func Insert(b *Bag, itemid int) {
    
    
    b.items = append(b.items, itemid)
}

func main() {
    
    
    bag := new(Bag)
    //调用函数
    Insert(bag, 1001)
}

Insert() 函数将 *Bag 参数放在第一位,强调 Insert 会操作 *Bag 结构体,但实际使用中,并不是每个人都会习惯将操作对象放在首位,一定程度上让代码失去一些范式和描述性。同时,Insert() 函数也与 Bag 没有任何归属概念,随着类似 Insert() 的函数越来越多,面向过程的代码描述对象方法概念会越来越麻烦和难以理解。

(2)Go语言的结构体方法

将背包及向背包放入物品使用Go语言的结构体及定义在其上的方法编写,为 *Bag 创建一个方法,代码如下:

type Bag struct {
    
    
    items []int
}

//定义在背包结构体上的名为Insert的方法
func (b *Bag) Insert(itemid int) {
    
    
    b.items = append(b.items, itemid)
}
func main() {
    
    
    b := new(Bag)
    //在结构体b上调用方法
    b.Insert(1001)
}

将 Insert() 函数转换为方法后,我们就可以愉快地像其他语言一样,用面向对象的方法来调用 b 的 Insert。

Go 语言实现面向对象的基础是结构体和接口。

2. 接收器——方法作用的对象

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。

方法(有接收器的函数)的定义格式如下:

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

对各部分的说明:

  • 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。
  • 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:格式与函数定义一致。方法可以没有参数,也可以没有返回值。

接收器根据其数据类型可以分为指针接收器、非指针接收器,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。

(1)理解指针类型的接收器

指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。

由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。

在下面的例子,使用定义一个 Property 结构体,为 Property 添加 SetValue() 方法以封装设置属性的过程,通过属性的 Value() 方法可以重新获得属性的数值,使用属性时,通过 SetValue() 方法的调用,可以达成修改属性值的效果。

package main
import "fmt"
// 定义结构体 Property 
type Property struct {
    
    
    value int  // 属性值
}
// 设置 Property 值
func (p *Property) SetValue(v int) {
    
    
    // 修改p的成员变量
    p.value = v
}
// 取 Property 值
func (p *Property) Value() int {
    
    
    return p.value
}
func main() {
    
    
    // 实例化 Property 
    p := new(Property)
    // 设置值
    p.SetValue(100)
    // 打印值
    fmt.Println(p.Value())
}

输出结果:

100

(2)理解非指针类型的接收器

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。需要辅以 return+赋值 实现真正的修改。

点(Point)使用结构体描述时,为点添加 Add() 方法,这个方法不能修改 Point 的成员 X、Y 变量,而是在计算后返回新的 Point 对象,Point 属于小内存对象,在函数返回值的复制过程中可以极大地提高代码运行效率,详细过程:

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}

由于例子中使用了非指针接收器,Add() 方法变得类似于只读的方法,Add() 方法内部不会对成员进行任何修改。

(3)指针和非指针接收器的使用

在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器。
大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。

什么时候应该使用指针类型接收者?

  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

十二、嵌套结构体

1. 嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针。

package main

import (
	"fmt"
)

//Address 地址结构体
type Address struct {
    
    
	Province string
	City     string
}

//User 用户结构体
type User struct {
    
    
	Name    string
	Gender  string
	Address Address
}

func main() {
    
    
	user1 := User{
    
    
		Name:   "pprof",
		Gender: "女",
		Address: Address{
    
    
			Province: "黑龙江",
			City:     "哈尔滨",
		},
	}
	fmt.Printf("user1=%#v\n", user1) //user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}

输出结果:

user1=main.User{
    
    Name:"pprof", Gender:"女", Address:main.Address{
    
    Province:"黑龙江", City:"哈尔滨"}}

2. 嵌套匿名结构体

package main

import (
	"fmt"
)

//Address 地址结构体
type Address struct {
    
    
	Province string
	City     string
}

//User 用户结构体
type User struct {
    
    
	Name    string
	Gender  string
	Address //匿名结构体
}

func main() {
    
    
	var user2 User
	user2.Name = "pprof"
	user2.Gender = "女"
	user2.Address.Province = "黑龙江"   //通过匿名结构体.字段名访问
	user2.City = "哈尔滨"               //可以直接访问匿名结构体的字段名
	fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}

输出结果:

user2=main.User{
    
    Name:"pprof", Gender:"女", Address:main.Address{
    
    Province:"黑龙江", City:"哈尔滨"}}

当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。

3. 嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。

package main

import (
	"fmt"
)

//Address 地址结构体
type Address struct {
    
    
	Province   string
	City       string
	CreateTime string
}

//Email 邮箱结构体
type Email struct {
    
    
	Account    string
	CreateTime string
}

//User 用户结构体
type User struct {
    
    
	Name   string
	Gender string
	Address
	Email
}

func main() {
    
    
	var user3 User
	user3.Name = "pprof"
	user3.Gender = "女"
	// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
	user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
	user3.Email.CreateTime = "2000"   //指定Email结构体中的CreateTime
	fmt.Printf("user3=%#v\n", user3)
}

输出结果:

user3=main.User{
    
    Name:"pprof", Gender:"女", Address:main.Address{
    
    Province:"", City:"", CreateTime:"2000"}, Email:main.Email{
    
    Account:"", CreateTime:"2000"}}

如果直接使用user3.CreateTime = "2000"会报错:ambiguous selector user3.CreateTime

4. 结构体的“继承”

Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。

package main

import (
	"fmt"
)

//动物结构体
type Animal struct {
    
    
	name string
}


//动物结构体的move方法
func (a *Animal) move() {
    
    
	fmt.Printf("%s会动!\n", a.name)
}

//狗结构体
type Dog struct {
    
    
	Feet    int8
	*Animal //通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
    
    
	fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
    
    
	d1 := &Dog{
    
    
		Feet: 4,
		Animal: &Animal{
    
     //注意嵌套的是结构体指针
			name: "乐乐",
		},
	}
	d1.wang() 
	d1.move() 
}

输出结果:

乐乐会汪汪汪~
乐乐会动!

十三、结构体与JSON序列化

现代网络应用 Web APP 或大型网站的后台一般只有一个,然后客户端却是各种各样的(iOS, android, 浏览器), 而且客户端的开发语言很可能与后台的开发语言不一样。这时我们需要后台能够提供可以跨平台跨语言的一种标准的数据交换格式供前后端沟通(这就是Web API的作用)。如今大家最常用的跨平台跨语言数据交换格式就是JSON(JavaScript Object Notation)了。JSON是一种文本序列化格式(它输出的是unicode文件,大多数时候会被编码为utf-8),人们很容易进行阅读和编写。

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法

Json语法规则:

  • 数据在键值对中
  • 数据由逗号分隔
  • 花括号保存对象
  • 方括号保存数组

像这样:

{
    
     "firstName":"John" , "lastName":"Doe" }

这样:

{
    
    
"employees": [
{
    
     "firstName":"John" , "lastName":"Doe" },
{
    
     "firstName":"Anna" , "lastName":"Smith" },
{
    
     "firstName":"Peter" , "lastName":"Jones" }
]
}

或者这样:

var employees = [
{
    
     "firstName":"Bill" , "lastName":"Gates" },
{
    
     "firstName":"George" , "lastName":"Bush" },
{
    
     "firstName":"Thomas" , "lastName": "Carter" }
];

那什么是对象序列化呢?

序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以轻松地存储和传输数据。
通俗讲呢:对象序列化就是指将对象的状态转换为字符串。

Go语言的结构体与JSON序列化:

package main

import (
	"encoding/json"
	"fmt"
)

//Student 学生
type Student struct {
    
    
	ID     int
	Gender string
	Name   string
}

//Class 班级
type Class struct {
    
    
	Title    string
	Students []*Student
}

func main() {
    
    
	c := &Class{
    
    
		Title:    "101",
		Students: make([]*Student, 0, 200),
	}
	for i := 0; i < 10; i++ {
    
    
		stu := &Student{
    
    
			Name:   fmt.Sprintf("stu%02d", i),
			Gender: "男",
			ID:     i,
		}
		c.Students = append(c.Students, stu)
	}
	//JSON序列化:结构体-->JSON格式的字符串
	data, err := json.Marshal(c)
	if err != nil {
    
    
		fmt.Println("json marshal failed")
		return
	}
	fmt.Printf("json:%s\n", data)
	//JSON反序列化:JSON格式的字符串-->结构体
	str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
	c1 := &Class{
    
    }
	err = json.Unmarshal([]byte(str), c1)
	if err != nil {
    
    
		fmt.Println("json unmarshal failed!")
		return
	}
	fmt.Printf("%#v\n", c1)
}

输出结果:

json:{
    
    "Title":"101","Students":[{
    
    "ID":0,"Gender":"男","Name":"stu00"},{
    
    "ID":1,"Gender":"男","Name":"stu01"},{
    
    "ID":2,"Gender":"男","Name":"stu02"},{
    
    "ID":3,"Gender":"男","Name":"stu03"},{
    
    "ID":4,"Gender":"男","Name":"stu04"},{
    
    "ID":5,"Gender":"男","Name":"stu05"},{
    
    "ID":6,"Gender":"男","Name":"stu06"},{
    
    "ID":7,"Gender":"男","Name":"stu07"},{
    
    "ID":8,"Gender":"男","Name":"stu08"},{
    
    "ID":9,"Gender":"男","Name":"stu09"}]}
&main.Class{
    
    Title:"101", Students:[]*main.Student{
    
    (*main.Student)(0xc0000749c0), (*main.Student)(0xc0000749f0), (*main.Student)(0xc000074a20), (*main.Student)(0xc000074a50), (*main.Student)(0xc000074ab0), (*main.Student)(0xc000074ae0), (*main.Student)(0xc000074b10), (*main.Student)(0xc000074b40), (*main.Student)(0xc000074b70), (*main.Student)(0xc000074ba0)}}

十四、结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。

Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

`key1:"value1" key2:"value2"`

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

例如我们为Student结构体的每个字段定义json序列化时使用的Tag:

package main

import (
	"encoding/json"
	"fmt"
)

//Student 学生
type Student struct {
    
    
	ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
	Gender string //json序列化是默认使用字段名作为key
	name   string //私有不能被json包访问
}

func main() {
    
    
	s1 := Student{
    
    
		ID:     1,
		Gender: "女",
		name:   "pprof",
	}
	data, err := json.Marshal(s1)
	if err != nil {
    
    
		fmt.Println("json marshal failed!")
		return
	}
	fmt.Printf("json str:%s\n", data) 
}

输出结果:

json str:{
    
    "id":1,"Gender":"女"}

参考链接

  1. Go 语言结构体
  2. Go语言方法和接收器
  3. 浅谈Json解析与序列化

猜你喜欢

转载自blog.csdn.net/weixin_44211968/article/details/121349632