Go语言基础之结构体struct

Go语言基础之结构体struct

一、结构体介绍

struct是go语言为我们提供的可以自定义的一种类型,该类型可以封装多个基本数据类型,可以用来存放一个事物的不同属性

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

Go语言中通过struct来实现面向对象。Go语言中没有“类”的概念(或者说结构体struct充当的就是类的作用),也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口以达到比面向对象更高的扩展性和灵活性。

二、 结构体的定义

使用typestruct关键字来定义结构体,具体代码格式如下:

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}

其中:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:表示结构体字段的具体类型。
  • 结构体名字or结构体中的字段名字首字母大写表示公开的、可以被其他包导入,小写表示私有、仅在定义当前结构体的包中可访问

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

type person struct {
    name string
    city string
    age  int8
}

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

type person1 struct {
    name, city string
    age        int8
}

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

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

三、 结构体实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

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

var 结构体实例 结构体类型

3.1.通过对象.点方式

没有初始化的结构体,其成员变量都是对应其类型的零值。

//大写表示外部包可以使用
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}
    
    // 赋值
      // 实例化方式一 键值方式
    var p1 person
    p1.name = "randy"
    p1.city = "ah"
    p1.age = 18
    fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"randy", city:"ah", age:18}
}

我们通过.来访问结构体的字段(成员变量)例如p1.namep1.age等,也可以赋值。

3.2 使用键值对初始化

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

p5 := person{
    name: "randy",
    city: "ah",
    age:  18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"randy", city:"ah", age:18}

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

p6 := &person{
    name: "ransy",
    city: "ah",
    age:  18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"ransy", city:"ah", age:18}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。

p7 := &person{
    city: "ah",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"ah", age:0}
  1. 按关键字,可以忽略位置,可以传一部分,没有指定初始值的字段的值就是该字段类型的零值

3.3 使用值的列表初始化

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:

p8 := &person{
    "randy",
    "ah",
    28,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"randy", city:"ah", age:28}

使用这种格式初始化时,需要注意:

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。

四、匿名结构体

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

package main
     
import (
    "fmt"
)
     
func main() {
    //一堆属性的集合  (其实就是面向对象的封装)
    a := struct {
		Name string
		age  int
	}{Name:"randy",age:19}
    fmt.Printf("%#v\n", a)
}

五、创建指针类型结构体

5.1 结构体指针

var p Person=Person{name:"randy",age:18,sex:1}
fmt.Println(p)
stest6(&p)
//var p1 *Person=&Person{name:"randy",age:18,sex:1}
p1:=&Person{name:"randy",age:18,sex:1}
(*p1).name="xxx"  //正统用法
p1.name="yyy"   //推荐用法(别人的代码,这种多)
fmt.Printf("%T",p2)

func stest6(p *Person) () {
	//	//p是个指针
	//	//(*p).Name="randysun"  //正统的用法,需要解引用
	p.name = "randysun" //这种用法也可以,推荐这种用法,没有解引用,go自动处理
	fmt.Println(p)

}

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

var p2 = new(person)
fmt.Printf("%T\n", p2)     //*main.person
fmt.Printf("p2=%v\n", p2) //p2=&main.person{name:"", city:"", age:0}

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

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

var p2 = new(person)
p2.name = "randy"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"randy", city:"上海", age:28}

5.2 取结构体的地址实例化

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

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

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

六、结构体内存布局

结构体占用一块连续的内存。

type test struct {
    a int8
    b int8
    c int8
    d int8
}
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 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063

【进阶知识点】关于Go语言中的内存对齐推荐阅读:在 Go 中恰到好处的内存对齐

下面代码的执行结果是什么?

type student struct {
    name string
    age  int
}

func main() {
    m := make(map[string]*student)
    stus := []student{
        {name: "randy", age: 18},
        {name: "bary", age: 23},
        {name: "jack", age: 9000},
    }

    for _, stu := range stus {
        m[stu.name] = &stu
    }
    for k, v := range m {
        fmt.Printf("地址:%p, k:%v => v: %v \n", v, k, v.name)
    }
    
    // 方案一:在循环体内每次都声明一个新的变量来接收值
    for _, p := range persons {
        x:=p
        m[p.name] = &x
    }

    // 方案二:不使用p,而是直接去引用老祖宗即persons中的元素
    for i := range persons {
        m[persons[i].name] = &persons[i]
    }
}

把注意力放在第一个for循环
针对语句:_, stu := range stus,只会在第一次声明变量stu,之后都是在为同一个变量stu做赋值操作!!!
range会将遍历得到的值赋值给变量名stu,循环往复,变量stu的值每次都被赋予一个新的值,但始终都是同一个变量stu,所以&stu得到的地址永远相同。当迭代完毕,把迭代出的最后一个值放入变量stu中后(指向同一个地址),之后也就输出了相同的值

地址:0xc0000040a8, k:randy => v: jack 
地址:0xc0000040a8, k:bary => v: jack 
地址:0xc0000040a8, k:jack => v: jack 

七、结构体相等性

结构体内部所有字段都可以比较,结构体才可以比较,如果内部有不可比较的字段,结构体就不能比较

结构体内部所有字段都可以比较,结构体才可以比较

type Name struct {
    firstName string
    lastName string
}
type AA struct {
    XX map[int]string
}

a:=Name{"randy","sun"}
b:=Name{"ra","s"}
if a==b{
    fmt.Printf("xxx")
}


//如果内部有不可比较的字段,结构体就不能比较
//c:=AA{}
//d:=AA{}
//if c==d {
//
//}


//int 和int比较  string和string   数组和数组比      
//引用类型不能比较:切片不能和切片比  map不能和map比
//	//int和string能比么?不是同一个类型就不能比较
//	//Myint 和int
  • int 和int比较 string和string 数组和数组比
  • 引用类型不能比较:切片不能和切片比 map不能和map比

八、结构体的匿名字段

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

//Person 结构体Person类型
type Person struct {
    string
    int
    //	 int  报错
}

func main() {
    p1 := Person{
        "ransy",
        18,
    } //按位置
   var p1 Person1=Person1{string:"randy",int:18} //按关键字,有点奇特(这就是为什么一种类型只能写一次)
    fmt.Printf("%#v\n", p1)        //main.Person{string:"ah", int:18}
    fmt.Println(p1.string, p1.int) //ah 18
}

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个匿名字段做变量提升,面向对象中的继承,类型既可以是内置的也可以是自定义的,比如自定义的结构体,如果一个结构体内的一个字段是另外一个结构体,那就是结构体嵌套了

注意:

不能匿名嵌入切片或者map,如下

type Person struct {
	name string
	// []string // 错误,修正如下
	hobbies []string
}

九、嵌套结构体

9.2 嵌套结构体

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

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

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

func main() {
    user1 := User{
        Name:   "ransy",
        Gender: "男",
        Address: Address{
            Province: "山东",
            City:     "威海",
        },
    } //按关键字
    
    p:=User{"randy","男",Address{Province:"ah", City:"hf"}}   //按位置
    
    fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"ransy", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
    
    var p User  //注意区分引用类型和值类型(数组,字符串,数字,只需要定义,不需要初始化就可以使用)
	////修改Address的Province
	p.Province.id="sh"
	fmt.Println(p)
}

值类型不需要初始化就可以使用

9.2 嵌套匿名结构体

go的结构体嵌套就是面向对象的继承

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

嵌套的匿名字段是一个结构体or结构体指针类型,结构体的字段属于提升字段

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

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

func main() {
    var user2 User
    user2.Name = "ransy"
    user2.Gender = "男"
    user2.Address.Province = "山东"    //通过匿名结构体.字段名访问
    //在外层可以调用内层的属性,提升了,匿名的时候提升
    user2.City = "威海"                //直接访问匿名结构体的字段名
    //以后这两种方式都可以了
    
    // 重名怎么办
    // 通过结构体名字来查询
    
    fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"ransy", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}

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

十、总结

  1. 定义 type关键字 结构体名字 struct{一系列属性的集合},类似于面向对象的类,但是只有属性,没有方法
  2. 使用 var 变量 结构体名字=结构体名字{初始化},三种:不加参数,按关键字传参数(可以少传,可以打乱顺序)没有指定初始值的字段的值就是该字段类型的零值,按位置传参数必须初始化结构体的所有字段初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 调用属性(取属性,赋值属性) 通过 . 来使用 p.name
  4. 结构体中字段,公有字段,和私有字段(大小写)
  5. 匿名结构体,结构体没有名字和type关键字(定义再函数内部,只使用一次时)
  6. 匿名字段,字段没有名字(字段的名字就是类型名),如果全用匿名字段,一个结构体中,只能有一个相同的类型(字段提升)
  7. 结构体嵌套(一个结构体中套一个结构体,类比:类的继承)
  8. 如果嵌套的结构体中有匿名字段,字段会提升(类比:类的继承)
  9. 结构体的0值,不是nil,他是一个值类型,在函数传递时,在函数内部,不会改变原结构体,要想改变,要传递指针
  10. 结构体指针 指向结构体的指针 var 变量名 *结构体名字 = &结构体名字{}
  11. 结构体相等性:所有字段都可以比较,结构体就可以比较;有不能比较的字段,结构体就不能比较
posted @ 2021-10-17 17:26  RandySun  阅读(728)  评论(0编辑  收藏  举报