Golang数据类型之结构体-下篇

仙人技术

共 12684字,需浏览 26分钟

 ·

2021-09-01 20:56

目录

  • 1、结构体指针

    • 1.1 声明

    • 1.2 声明并初始化

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

    • 1.4 传递结构体指针

    • 1.5 结构体值与结构体指针

    • 1.6 传值还是传递指针

  • 2、匿名结构体

  • 3、结构体方法

  • 4、结构体嵌套

    • 4.1 匿名嵌套

    • 4.2 命名嵌套

    • 4.3 指针类型结构体嵌套

    • 4.4 结构体嵌套的实际意义

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

  • 6、结构体的可见性


本文是 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 ~

浏览 32
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报