本文是Golang数据类型之结构体-上篇的续篇内容

1、结构体指针

1.1 声明

和其他基础数据类型一样,也可声明结构体指针变量,此时变量被初始化为nil

func TestMain4(t *testing.T)  {
	var person *Person
	fmt.Println(person)  // <nil>
}

1.2 声明并初始化

声明并初始化指针对象

// 先声明再初始化
//var person *Person
//person = &Person{}
// 简短声明
person := new(Person)
//person := &Person{}  // *Person
fmt.Printf("%p", person)  // 0xc00013a080

声明并初始化赋值

var person *Person = &Person{
    Name:          "andy",
    Age:           66,
    Gender:        "male",
    Weight:        120,
    FavoriteColor: []string{"red", "blue"},
}
fmt.Printf("%p", person)  // 0xc0000ce080

1.3 通过new函数创建指针对象

Go中常定义N(n)ew+结构体名命名的函数用于创建对应的结构体值对象或指针对象

person := new(Person)
fmt.Printf("%p", person)  // 0xc00013a080
fmt.Printf("%T", person)  // *test.Person

// 定义工厂函数用于创建Author对象
func NewAuthor(id int, name, birthday, addr, tel, desc string) *User {
	return &User{id, name, birthday,addr, tel, desc}
}
// 调用
	me8 := NewAuthor(1004, "geek", "2021-06-08", "北京市", "15588888888", "备注")
	fmt.Printf("%T: %#v\n", me8, me8)

1.4 传递结构体指针

将一个结构体的指针传递给函数,能否修改到该结构体

结果是可以修改该实例对象

func ChangeColor(car *Car) {
	car.Color = "blue"
	fmt.Println(car.Color)
}

func main() {
	car := Car{
		Color: "yellow",    // 黄色
		Brand: "ford",      // 福特
		Model: "Mustang",   // 野马
	}
	ChangeToW(car)
	fmt.Println(car.Color) // blue
}

1.5 结构体值与结构体指针

什么是值? 什么是指针?

下面三种方式都可以构造Car struct的实例

c1 := Car{}
c2 := &Car{}
c3 := new(Car)
fmt.Println(c1, c2, c3) // {  } &{  } &{  }

c1c2c3都是car struct的实例,c2, c3是指向实例的指针,指针中保存的是实例的地址,所以指针再指向实例,c1则是直接指向实例。这三个变量与Car struct实例的指向关系如下

变量名      指针     数据对象(实例)
-------------------------------
c1 -------------------> { }
c2 -----> ptr(addr) --> { }
c3 -----> ptr(addr) --> { }

访问实例和访问实例指针是否有区别

fmt.Println("c1, ", c1.Color)    // 访问实例的属性
fmt.Println("c2, ", (*c2).Color) // 先通过*求出 指针的值,就是实例的内存地址, 然后通过实例的内存地址访问该实例对象的属性

如果我们需要访问指针对象的属性, 上面的(*c2).Color是理论上的正确写法, 可以看出过于繁琐, 而我们方法指针,往往也是想访问这个指针的实例, 所以编译帮我们做了优化, 比如访问指针实例也可以这样写

fmt.Println("c2, ", c2.Color) // 编译器自动补充上(*c2).Color, 这样写法上就简洁了

简单总结:尽管一个是数据对象值,一个是指针,它们都是数据对象的实例。也就是说,p1.namep2.name都能访问对应实例的属性,只是指针的访问写法是一种简写(正确写法由编译器补充)

1.6 传值还是传递指针

前面文章Golang函数参数的值传递和引用传递说的也是这个话题

即什么时候传值,什么时候传递指针?

  • 传递值: 不希望实例被外部修改的时候,传值就相当于copy了一份副本给函数
  • 传递指针: 希望外部能修改到这个实例本身的时候,就需要传递该实例的指针,就是把该实例的内存地址告诉对方,可以通过地址直接找到本体

但是经常看到函数接收的结构体参数都是指针是为什么

因为复制传值时,如果函数的参数是一个struct对象,将直接复制整个数据结构的副本传递给函数,这有两个问题

  • 函数内部无法修改传递给函数的原始数据结构,它修改的只是原始数据结构拷贝后的副本
  • 如果传递的原始数据结构很大,完整地复制出一个副本开销并不小

所以为了节省开销一般都会选择传递指针

2、匿名结构体

在定义变量时将类型指定为结构体的结构,此时叫匿名结构体。匿名结构体常用于初始化一次结构体变量的场景,例如项目配置

package main

import "fmt"

func main() {
	var me struct {
		ID   int
		Name string
	}

	fmt.Printf("%T\n", me)  // struct { ID int; Name string }
	fmt.Printf("%#v\n", me)  // struct { ID int; Name string }{ID:0, Name:""}
	fmt.Println(me.ID)  // 0
	me.Name = "geek"
	fmt.Printf("%#v\n", me)  // struct { ID int; Name string }{ID:0, Name:"geek"}

	me2 := struct {
		ID   int
		Name string
	}{1, "geek"}

	fmt.Printf("%#v\n", me2)  // struct { ID int; Name string }{ID:1, Name:"geek"}
}

3、结构体方法

可以为结构体定义属于自己的函数

在声明函数时,声明属于结构体的函数,方法与结构体绑定,只能通过结构体person的实例访问,不能在外部直接访问,这就是结构体方法和函数的区别,例如

// p 是person的别名
func (p Person) add() int {
	return p.Age * 2
}

调用结构体方法

func TestMain6(t *testing.T) {
	m := new(Person)
	m.Age = 18
	fmt.Println(m.add()) // 36
}

4、结构体嵌套

4.1 匿名嵌套

简单来说,就是将数据结构直接放进去,放进去的时候不进行命名

在定义变量时将类型指定为结构体的结构,此时叫匿名结构体。匿名结构体常用于初始化一次结构体变量的场景,例如项目配置

匿名结构体可以组合不同类型的数据,使得处理数据变得更为灵活。尤其是在一些需要将多个变量、类型数据组合应用的场景,匿名结构体是一个不错的选择

// 访问方式 结构体.成员名
type Person2 struct {
	Name          string
	Age           int
	Gender        string
	Weight        uint
	FavoriteColor []string
	NewAttr       string
	Addr          Home
	NewHome
}

type NewHome struct {
	City string
}

func TestPerson2(t *testing.T) {
	m := new(Person2)
	m.Age = 18
	m.City = "beijing"
	fmt.Println(m.City)  // beijing
}

嵌套过后带来的好处就是能够像访问原生属性一样访问嵌套的属性

示例

package main
 
import (
	"encoding/json"
	"fmt"
)
//定义手机屏幕
type Screen01 struct {
	Size       float64 //屏幕尺寸
	ResX, ResY int //屏幕分辨率 水平 垂直
}
//定义电池容量
type Battery struct {
	Capacity string
}
 
//返回json数据
func getJsonData() []byte {
	//tempData 接收匿名结构体(匿名结构体使得数据的结构更加灵活)
	tempData := struct {
		Screen01
		Battery
		HashTouchId bool  // 是否有指纹识别
	}{
		Screen01:    Screen01{Size: 12, ResX: 36, ResY: 36},
		Battery:     Battery{"6000毫安"},
		HashTouchId: true,
	}
	jsonData, _ := json.Marshal(tempData)  //将数据转换为json
	return jsonData
}

4.2 命名嵌套

结构体命名嵌入是指结构体中的属性对应的类型也是结构体

给嵌入的结构体一个名字,让其成为另一个结构体的属性

适用于复合数据结构<嵌入匿名>

嵌套定义

type Book struct {
    Author  struct{
        Name string
        Aage int
    }
    Title struct{
        Main string 
        Sub  string
    }
}

声明和初始化

b := &Book{
    Author: struct {
        Name string
        Aage int
    }{
        Name: "xxxx",
        Aage: 11,
    },
    Title: struct {
        Main string
        Sub  string
    }{
        Main: "xxx",
        Sub:  "yyy",
    },
}

// 
b := new(Book)
b.Author.Aage = 11
b.Author.Name = "xxx"

嵌入命名,在外面定义

type Author struct {
    Name string
    Aage int
}

type Title struct {
    Main string
    Sub  string    
}

type Book struct {
    Author Author
    Title Title
}

示例

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

type TeacherNew struct {
	Pn        Person
	TeacherId int
}

func main() {
	t2 := TeacherNew{
		Pn: Person{
			Name: "geek",
			Age:  18,
		},
		TeacherId: 123,
	}
	fmt.Printf("[TeacherId: %v][Name: %v][Age: %v]", t2.TeacherId, t2.Pn.Name, t2.Pn.Age)
  // [TeacherId: 123][Name: geek][Age: 18]
}

4.3 指针类型结构体嵌套

结构体嵌套(命名&匿名)类型也可以为结构体指针

声明&初始化&操作

type Book2 struct {
	Author *Author
	Title *Title
}

func (b *Book2) GetName() string {
	return b.Author.GetName() + "book"
}

func TestMain8(t *testing.T) {
	b1 := Book2{
		Author: &Author{
			Name: "ssgeek",
		},
		Title: &Title{},
	}

	b2 := &Book2{
		Author: &Author{},
		Title: &Title{},
	}
}

使用属性为指针类型底层共享数据结构,当底层数据发生变化,所有引用都会发生影响
使用属性为值类型,则在复制时发生拷贝,两者不相互影响

4.4 结构体嵌套的实际意义

  • 例如大项目对应复杂的配置文件,将公共的字段抽取出来,放到一个公共common的结构体
  • cmdb、资产系统等类型设计

示例

package main

import "time"

// 云有云资源公共字段
type Common struct {
	ChargingMod string    // 付费模式:预付费和后付费
	Region      string    // 区域
	Az          string    // 可用区
	CreateTime  time.Time // 购买时间
}

type Ecs struct {
	Common
	guide string // 4C 16G
}

type Rds struct {
	Common
	dbType string // 代表数据库是哪一种
}

5、通过函数创建结构体对象

除了通过直接赋值创建结构体对象,还可以通过函数来创建,也就是把创建结构体对象的过程进行封装

即“工厂函数”

package main

import "fmt"

type Address struct {
	Region string
	Street string
	No     string
}

type User struct {
	ID   int
	Name string
	Addr *Address
}

func NewUser(id int, name string, region, street, no string) *User {
	return &User{
		ID:   id,
		Name: name,
		Addr: &Address{region, street, no},
	}
}

func main() {
	me := User{
		ID:   1,
		Name: "geek",
		Addr: &Address{"上海市", "南京路", "0001"},
	}

	me2 := me
	me2.Name = "ss"
	me2.Addr.Street = "黄河路"

	fmt.Printf("%#v\n", me.Addr)
	fmt.Printf("%#v\n", me2.Addr)

	hh := NewUser(2, "hh", "北京市", "海淀路", "0001")
	fmt.Printf("%#v\n", hh)
}

6、结构体的可见性

结构体对外是否可见,在go中受其首字母是否大写控制,结论是

结构体首字母大写则包外可见(公开的),否者仅包内可访问(内部的)
结构体属性名首字母大写包外可见(公开的),否者仅包内可访问(内部的)

组合起来的可能情况:

  • 结构体名首字母大写,属性名大写:结构体可在包外使用,且访问其大写的属性名
  • 结构体名首字母大写,属性名小写:结构体可在包外使用,且不能访问其小写的属性名
  • 结构体名首字母小写,属性名大写:结构体只能在包内使用,属性访问在结构体嵌入时由被嵌入结构体(外层)决定,被嵌入结构体名首字母大写时属性名包外可见,否者只能
    在包内使用
  • 结构体名首字母小写,属性名小写:结构体只能在包内使用
  • 结构体成员变量在同包内小写也是可以访问到的

总结:

  • 跨包访问:全局变量、结构体本身、结构体成员变量、必须要首字母大写才可以暴露出来被访问到(在go中常见的是会给结构体绑定一个方法,返回小写的成员变量让外面访问到)
  • 同包访问:上述变量首字母小写也可以被访问到

示例:

首先在tt包下定义一个person结构体,person大写的时候外部的包可以访问到,person小写的时候外部的包不可以访问到

package main

import (
	"fmt"
	"go-learning/chapter06/tt"
)

func main() {
	p1 := tt.Person{
		Name: "geek",
		Age:  18,
	}
	fmt.Println(p1)
	/*
	# command-line-arguments
	./last.go:9:8: cannot refer to unexported name tt.person
	 */
}

See you ~