13、结构体

结构体

定义

使用type定义结构体,可以把结构体看做类型使用。必须指定结构体的字段(属性)名称和类型。

type User struct {
    id int
    name,addr string
    score float32
}
  • 真正的结构体是struct{}这个整体的
  • User只是一个标识符指代结构体

初始化

package main

import "fmt"

type User struct {
	id         int
	name, addr string
	score      float32
}

func main() {
	// 1 声明
	var u1 User             // 这种方式声明结构体变量很方便,所有字段都是零值
	fmt.Println(u1)         //{0   0} 因为name和addr是string 所以只占位
	fmt.Printf("%+v\n", u1) // 加上字段打印{id:0 name: addr: score:0}
	fmt.Printf("%#v\n", u1) // 加上更多信息打印 main.User{id:0, name:"", addr:"", score:0}

	//2 字面量初始化 推荐用这个
	var u2 = User{}         // 字段为零值
	fmt.Printf("%+v\n", u2) //{id:0 name: addr: score:0}

	//3 字面量初始化,field: value为字段赋值
	u3 := User{id: 102}
	fmt.Printf("%+v\n", u3) //{id:102 name: addr: score:0}

	u4 := User{id: 12, score: 99, name: "zfl", addr: "xian"} //如果按照名称对应的话,无所谓顺序
	fmt.Printf("%+v\n", u4)                                  // {id:12 name:zfl addr:xian score:99}

	u5 := User{103, "zfl", "xi'an", 99.9}//如果名称没有对应的话,必须按照结构体定义的顺序来给所有的字段值
	fmt.Printf("%+v\n", u5)	//{id:103 name:zfl addr:xi'an score:99.9}

}

可见性

  • Go包的顶层代码中,首字母大写的标识符,跨package包可见(导出),否则只能本包内可见
  • 导出的结构体,package内外皆可见,同时,导出的结构体中的成员(属性、方法)要在包外也可见,则也需首字母大写
type User struct {
	Id         int
	Name, Addr string
	Score      float32
} //所以结构体的标识符User首字母大写,结构体的成员也需要大写,才可以包外可见

通过字段名打印,修改

package main

import "fmt"

type User struct {
	id         int
	name, addr string
	score      float32
}

func main() {
	u1 := User{id: 12, score: 99, name: "zfl", addr: "xian"}
	fmt.Printf("%+v\n", u1)        //{id:12 name:zfl addr:xian score:99}
	fmt.Println(u1.id, u1.addr)    //12 xian
	fmt.Println(u1.name, u1.score) //zfl 99
	//直接修改id,通过字段名
	u1.id = 104		
	fmt.Println(u1.id) //104
}

成员方法

package main

import "fmt"

type User struct {
	id         int
	name, addr string
	score      float32
}

// u称为receiver
// 等价于 func (User) string
func (u User) getName() string {
	return u.name
}

func main() {
	u1 := User{18, "zfl", "xa", 99.9}
	fmt.Println(u1.getName())
}

指针

package main

import "fmt"

type Point struct {
	x, y int
}

func main() {
	var p1 = Point{100, 200}     //字面量初始化,一个实例
	fmt.Printf("%T %[1]v\n", p1) //main.Point {100 200}

	var p2 = &Point{5, 6}
	fmt.Printf("%T %[1]v\n", p2) //*main.Point &{5 6}

	var p3 = new(Point)
	fmt.Printf("%T %[1]v\n", p3) //*main.Point &{0 0}

	// 通过实例修改属性
	p1.x = 10
	fmt.Printf("%T, %[1]v\n", p1) //main.Point, {10 200}

	// 通过指针修改属性
	p2.x = 200
	p3.x = 300
	fmt.Printf("%T, %[1]v\n", p2) //*main.Point, &{200 6}
	fmt.Printf("%T, %[1]v\n", p3) //*main.Point, &{300 0}

	// p3.x中. 是 -> 的语法糖,更方便使用。等价于(*p3).x
	fmt.Print(*p3, (*p3).x) // {300 0} 300
}


package main

import "fmt"

type Point struct {
	x, y int
}

func test(p Point) Point {
	fmt.Printf("4 %+v %p\n", p, &p) //4 {x:10 y:20} 0xc00001a160
	return p
}
func main() {
	var p1 = Point{10, 20}
	fmt.Printf("1 %+v %p\n", p1, &p1) //1 {x:10 y:20} 0xc00001a0b0

	p2 := p1
	fmt.Printf("2 %+v %p\n", p2, &p2) //2 {x:10 y:20} 0xc00001a100

	p3 := &p1
	fmt.Printf("3 %+v %p\n", p3, p3) //3 &{x:10 y:20} 0xc00001a0b0
	fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")

	p4 := test(p1)
	fmt.Printf("5 %+v %p\n", p4, &p4) //5 {x:10 y:20} 0xc00001a150
}

// 1 {x:10 y:20} 0xc00001a0b0
// 2 {x:10 y:20} 0xc00001a100
// 3 &{x:10 y:20} 0xc00001a0b0
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 4 {x:10 y:20} 0xc00001a160
// 5 {x:10 y:20} 0xc00001a150

​ 可以看出,结构体是非引用类型,使用的是值拷贝。传参或返回值如果使用结构体实例,将产生很多副本。如何避免过多副本,如何保证函数内外使用的是同一个结构体实例呢?使用指针。


package main

import "fmt"

type Point struct {
	x, y int
}

func test(p *Point) *Point {
	p.x += 100
	fmt.Printf("4 %+v %p\n", p, p)
	return p
}
func main() {
	var p1 = Point{10, 20} // 实例
	fmt.Printf("1 %+v %p\n", p1, &p1)
	p2 := p1
	fmt.Printf("2 %+v %p\n", p2, &p2)
	p3 := &p1
	fmt.Printf("3 %+v %p\n", p3, p3)
	fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
	p4 := test(p3)
	fmt.Printf("5 %+v %p\n", p1, &p1)
	fmt.Printf("6 %+v %p\n", p4, p4)
	p4.x += 200
	fmt.Printf("7 %+v %p\n", p1, &p1)
	fmt.Printf("8 %+v %p\n", p4, p4)
}
1 {x:10 y:20} 0xc00001a0b0
2 {x:10 y:20} 0xc00001a100
3 &{x:10 y:20} 0xc00001a0b0
​~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 &{x:110 y:20} 0xc00001a0b0
5 {x:110 y:20} 0xc00001a0b0
6 &{x:110 y:20} 0xc00001a0b0
7 {x:310 y:20} 0xc00001a0b0
8 &{x:310 y:20} 0xc00001a0b0
//说明,使用了同一个内存区域中的结构体实例,减少了拷贝。

匿名结构体

​ 匿名结构体:标识符直接使用struct部分结构体本身来作为类型,而不是使用type定义的有名字的结构体的标识符。

​ 可以使用 var 、 const 、 := 来定义匿名结构体。
​ type定义结构体的标识符,可以反复定义其结构体实例,但是匿名结构体是一次性的。

//定义一个结构体类型,指代其的标识符名称为Point
type Point struct {
	x, y int
}
//使用var定义一个变量point,后面跟类型,这个类型没有名字,只有结构体的本身
var Point struct {
	x, y int
}
package main

import "fmt"

var Point struct {
	x, y int
} // 定义Point是后面匿名结构体类型的,用零值

func main() {
   	fmt.Printf("%#v\n", Point) //struct { x int; y int }{x:0, y:0}
}


package main

import "fmt"

var message = struct {
	id   int
	data string
}{1, "ok"}

func main() {
	fmt.Println(message)         //{1 ok}
	fmt.Printf("%#v\n", message) //struct { id int; data string }{id:1, data:"ok"}
}

匿名结构体,只是为了快速方便地得到一个结构体实例,而不是使用结构体创建N个实例。

匿名成员

有时候属性名可以省略

package main

import "fmt"

type Point struct {
	x    int
	int  // 字段,匿名成员变量
	bool // 匿名,必须类型不一样才能区分
}

var p1 = Point{1, 2, false}
var p2 = Point{x: 20, int: 5, bool: false}

func main() {
	fmt.Println(p1)  //{1 2 false}
	fmt.Println(p2, p2.x, p2.int, p2.bool) //{20 5 false} 20 5 false
}
{1 2 false}
{20 5 false} 20 5 false

构造函数

​ Go语言并没有从语言层面为结构体提供什么构造器,但是有时候可以通过一个函数为结构体初始化提供属性值,从而方便得到一个结构体实例。习惯上,函数命名为 NewXxx 的形式。

package main

import "fmt"

type Animal struct {
	name string
	age  int
}

func NewAnimal(name string, age int) Animal {
	a := Animal{name, age}
	fmt.Printf("%+v,%p\n", a, &a)
	return a
}

func main() {
	a := NewAnimal("zfl", 20)
	fmt.Printf("%+v,%p\n", a, &a)
}
{name:zfl age:20},0xc000008090
{name:zfl age:20},0xc000008078
//NewAnimal的返回值使用了值拷贝,增加了内存开销,习惯上返回值会采用指针类型,避免实例的拷贝。
package main

import "fmt"

type Animal struct {
	name string
	age  int
}

func NewAnimal(name string, age int) *Animal {
	a := Animal{name, age}
	fmt.Printf("%+v, %p\n", a, &a)
	return &a
}

func main() {
	a := NewAnimal("zfl", 20)
	fmt.Printf("%+v,%p\n", a, a)
}
{name:zfl age:20}, 0xc000008078
&{name:zfl age:20},0xc000008078

父子关系构造

package main

import "fmt"

type Animal struct {
	name string
	age  int
}

type Cat struct {
	Animal // 匿名成员,可以使用类型名作为访问的属性名
	color  string
}

func main() {
	var cat = new(Cat) // Cat实例化,Animal同时被实例化
	fmt.Printf("%#v\n", cat)
	cat.color = "black"     // 子结构体属性
	cat.Animal.name = "Tom" // 完整属性访问
	cat.age = 20            // 简化写法,只有匿名成员才有这种效果
	fmt.Printf("%#v\n", cat)

}
&main.Cat{Animal:main.Animal{name:"", age:0}, color:""}
&main.Cat{Animal:main.Animal{name:"Tom", age:20}, color:"black"}
//使用结构体嵌套实现类似面向对象父类子类继承(派生)的效果
//子结构体使用匿名成员能简化调用父结构体成员

指针类型receiver

​ Go语言中,可以为任意类型包括结构体增加方法,形式是 func Receiver 方法名 签名 {函数体} ,这个receiver类似其他语言中的this或self。

​ receiver必须是一个类型T实例或者类型T的指针,T不能是指针或接口。

package main

import "fmt"

type Point struct {
	x, y int
}

func (p Point) getX() int {
	fmt.Println("instance")
	return p.x
}
func (p Point) getY() int {
	fmt.Println("pointer")
	return p.y
}
func main() {
	p := Point{4, 5}
	fmt.Println(p)
	fmt.Println(p.getX(), (&p).getX())
	fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
	fmt.Println(p.getY(), (&p).getY())
}
{4 5}
instance
instance
4 4
​~~~~~~~~~~~~~~~~~~~~~~~~~~~
pointer
pointer
5 5

接收器receiver可以是类型T也可以是指针*T

package main

import (
	"fmt"
)

type Point struct {
	x, y int
}

func (p Point) setX(v int) {
	fmt.Printf("1 %+v,%p\n", p, &p)
	p.x = v
	fmt.Printf("2 %+v,%p\n", p, &p)
}

func (p *Point) setY(v int) {
	fmt.Printf("3 %+v, %p\n", p, p)
	p.y = v
	fmt.Printf("4 %+v, %p\n", p, p)
}

func main() {
	p := Point{4, 5}
	fmt.Printf("5 %+v, %p\n", p, &p)
	p.setX(11) // 实例调用是值拷贝
	p.setY(22) // 看似实例调用,实则是指针,操作同一处内存
	fmt.Printf("6 %+v, %p\n", p, &p)
}
5 {x:4 y:5}, 0xc0000a6070
1 {x:4 y:5},0xc0000a60c0
2 {x:11 y:5},0xc0000a60c0

3 &{x:4 y:5}, 0xc0000a6070
4 &{x:4 y:22}, 0xc0000a6070
6 {x:4 y:22}, 0xc0000a6070

​ 非常明显,如果是非指针接收器方法调用有值拷贝,操作的是副本,而指针接收器方法调用操作的是同一个内存的同一个实例。
​ 如果是操作大内存对象时,且操作同一个实例时,一定要采用指针接收器的方法。

深浅拷贝

  • shadow copy
    影子拷贝,也叫浅拷贝。遇到引用类型数据,仅仅复制一个引用而已
  • deep copy
    深拷贝,往往会递归复制一定深度

​ 注意,深浅拷贝说的是拷贝过程中是否发生递归拷贝,也就是说如果某个值是一个地址,是只复制这个地址 ,还是复制地址指向的内容。

值拷贝是深拷贝,地址拷贝是浅拷贝,这种说法是错误的。因为地址拷贝只是拷贝了地址,因此本质上来讲也是值拷贝。

​ Go语言中,引用类型实际上拷贝的是标头值,这也是值拷贝,并没有通过标头值中对底层数据结构的指针指向的内容进行复制,这就是浅拷贝。非引用类型的复制就是值拷贝,也就是再造一个副本,这也是浅拷贝。因为你不能说对一个整数值在内存中复制出一个副本,就是深的拷贝。像整数类型这样的基本类型就是一个单独的值,没法深入拷贝,根本没法去讲深入的事儿。

​ 简单讲,大家可以用拷贝文件是否对软链接跟进来理解。直接复制软链接就是浅拷贝,钻进软链接里面复制其内容就是深拷贝。

​ 复杂数据结构,往往会有嵌套,有时嵌套很深,如果都采用深拷贝,那代价很高,所以,浅拷贝才是语言普遍采用的方案。

你可能感兴趣的:(go,go)