go语言结构体详解与代码演示


golang结构体定义

go语言的结构体定义需要使用关键字type, 格式如下

type 结构体名 struct{
    
    
  成员1 类型
  成员2 类型
  。。。
  。。。
}

例如下面定义一个Person结构体

//定义结构体
type Person struct {
    
    
   name   string
   age    int
   gender string
   hobby  []string
}

结构体的访问属性

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

结构体的初始化、赋值

结构体的初始化、赋值大概有四种方式,

方式1:

//结构体定义方法 1
var p Person
p.name = "zhangsan"
p.age = 20
p.gender = "男"
p.hobby = []string{
    
    "篮球", "皮球", "双色球"}

//打印结构体内容
fmt.Println(p)

//访问字段
fmt.Println(p.name)

访问字段直接用结构体变量点出来即可

方式2:

//结构体定义方法 2
//new结构体
p2 := new(Person)
p2.age = 30
p2.name = "zhaoliu"
fmt.Printf("p2 type=%T \n", p2)
fmt.Printf("p2=%p \n", p2)

方式3:

//结构体定义方法 3
//声明时进行初始化
//注意需要使用逗号
p3 := Person{
    
    
   name: "tianqi",
   age:  17,
}
fmt.Println("p3=", p3)

方式4:

//结构体定义方法 4
//值列表初始化
//必须严格按照结构体里成员的顺序, 这种需要把所有字段都进行赋值,不然无法编译
p4 := Person{
    
    
   "zhangwuji",
   20,
   "男",
   []string{
    
    "乾坤大挪移", "九阳神功", "太极拳"},
}
fmt.Println("p4=", p4)

全部代码

package main
import "fmt"

//定义结构体
type Person struct {
    
    
   name   string
   age    int
   gender string
   hobby  []string
}

func main() {
    
    
   //结构体定义方法 1
   var p Person
   p.name = "zhangsan"
   p.age = 20
   p.gender = "男"
   p.hobby = []string{
    
    "篮球", "皮球", "双色球"}
   //打印结构体内容
   fmt.Println(p)
   //访问字段
   fmt.Println(p.name)
   //结构体定义方法 2
   //new结构体
   p2 := new(Person)
   p2.age = 30
   p2.name = "zhaoliu"
   fmt.Printf("p2 type=%T \n", p2)
   fmt.Printf("p2=%p \n", p2)
   //结构体定义方法 3
   //声明时进行初始化
   //注意需要使用逗号
   p3 := Person{
    
    
      name: "tianqi",
      age:  17,
   }
   fmt.Println("p3=", p3)
   //结构体定义方法 4
   //值列表初始化
   //必须严格按照结构体里成员的顺序, 这种需要把所有字段都进行赋值,不然无法编译
   p4 := Person{
    
    
      "zhangwuji",
      20,
      "男",
      []string{
    
    "乾坤大挪移", "九阳神功", "太极拳"},
   }
   fmt.Println("p4=", p4)
   //匿名结构体
   //不需要名字
   var s struct {
    
    
      x int
      y int
   }
   s.x = 100
   s.y = 100
   // 打印结构体值用%v
   fmt.Printf("s type = %T, s vale %v", s, s)
}

匿名结构体

可以在函数里直接定义结构体类型,不需要使用type关键字,这种称为匿名结构体.

//匿名结构体
//不需要名字
var s struct {
    
    
   x int
   y int
}
s.x = 100
s.y = 100
// 打印结构体值用%v
fmt.Printf("s type = %T, s vale %v", s, s)

结构体的匿名字段

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

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

func main() {
    
    
	p1 := Person{
    
    
		"小王子",
		18,
	}
	fmt.Printf("%#v\n", p1)        //main.Person{string:"北京", int:18}
	fmt.Println(p1.string, p1.int) //北京 18
}

注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

结构体的构造函数

go语言没有像C++、java、c#这种面向对象的特性,在结构体里只能声明变量,不能声明方法,为了构造结构体更方便,可以在同一个包的外面给结构体声明方法,例如可以写构造函数:

package main
import "fmt"

type Student struct {
    
    
   id   int
   name string
}

func newStudent(_id int, _name string) Student {
    
    
   return Student{
    
    
      _id, _name,
   }
}

func newStudentPtr(_id int, _name string) *Student {
    
    
   return &Student{
    
    
      _id, _name,
   }
}

//给结构体添加方法
func (this Student) sayHello() {
    
    
   fmt.Printf("我的名字是%s, 学号%d\n", this.name, this.id)
}

//值接收
func (this Student) changeId() {
    
    
   this.id++
}

//指针接收者
func (this *Student) changeIdYes() {
    
    
   this.id++
}

func main() {
    
    
   p1 := newStudent(1001, "张三")
   fmt.Println(p1)
   p1.sayHello()
   p1.changeId()
   fmt.Printf("id = %d\n", p1.id)
   p1.changeIdYes()
   fmt.Printf("id = %d\n", p1.id)
   p2 := newStudentPtr(1002, "李四")
   fmt.Println(p2)
}

例如上面的代码,声明了一个Student结构体,newStudent, newStudentPtr是两个构造函数,go语言没有方法重载的概念,所以不能有同名函数。

构造函数

func newStudent(_id int, _name string) Student {
    
    
   return Student{
    
    
      _id, _name,
   }
}

func newStudentPtr(_id int, _name string) *Student {
    
    
   return &Student{
    
    
      _id, _name,
   }
}

指针构造与对象构造

当结构体成员比较多时,建议使用返回指针的形式进行构造,减少内存的开销,也就是newStudentPtr这种写法;当结构体成员比较少时,可以使用返回对象的写法,也就是newStudent这种写法。

给结构体添加方法

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

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

在上面的代码中,给结构体添加了3个方法

//给结构体添加方法
func (this Student) sayHello() {
    
    
   fmt.Printf("我的名字是%s, 学号%d\n", this.name, this.id)
}

//值接收
func (this Student) changeId() {
    
    
   this.id++
}

//指针接收者
func (this *Student) changeIdYes() {
    
    
   this.id++
}

注意上面的this可以随便命名,只要是合理的标识符即可,用this,主要是参考C++的写法,更形象,也可以写成self。

结构体是值类型

在方法changeId中,给id成员执行++操作,但是执行后并没有++, 如果想要用方法改变传入的值,得用指针传参,也就是changeIdYes()这种写法

结构体做结构体的类型成员

package main
import "fmt"

type BaseInfo struct {
    
    
   id   int
   name string
}

type Student struct {
    
    
   gender string
   baInfo BaseInfo
}
func main() {
    
    
   var s1 Student
   s1.baInfo.name = "张三"
   s1.baInfo.id = 1001
   s1.gender = "男"
   fmt.Println(s1)
}

上面代码是把BaseInfo作为Student的成员
变量名baInfo 也可以不写,例如

package main
import "fmt"

type BaseInfo struct {
    
    
   id   int
   name string
}

//type Student struct {
    
    
// gender string
// baInfo BaseInfo
//}

type Student struct {
    
    
   gender string
   BaseInfo
}

func main() {
    
    
   //var s1 Student
   //s1.baInfo.name = "张三"
   //s1.baInfo.id = 1001
   //s1.gender = "男"
   var s1 Student
   s1.name = "张三"
   s1.id = 1001
   s1.gender = "男"
   fmt.Println(s1)
}

省略baInfo后,就可以用s1直接访问
这种嵌套定义有点类似C++、java的类继承,不过由于golang的特点,看起来不是那么明显

结构体内存布局与字节对齐问题

go结构体有类似C/C++的内存对齐问题,例如下面的代码

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 0xc000128058
n.b 0xc000128059
n.c 0xc00012805a
n.d 0xc00012805b

内存字节对齐

由于是int8,站一个字节,所以此时a,b,c,d的顺序是一致的,如果换成int32就不同了,如下所示

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

输出结果
n.a 0xc00001a0b0
n.b 0xc00001a0b4
n.c 0xc00001a0b8
n.d 0xc00001a0bc
由于要处理内存对齐问题,虽然a是8位,占一个字节,但是它的下一个成员b是占4个字节,为了字节对齐,那么a也是得占用4个字节,c, d类似,如下图:
在这里插入图片描述

结构体大小

fmt.Printf("结构体大小:%d", unsafe.Sizeof(n))

例如如上面结构体大小为16
n.a 0xc0000aa070
n.b 0xc0000aa074
n.c 0xc0000aa078
n.d 0xc0000aa07c
结构体大小:16

空结构体

空结构体是不占用空间的。

var v struct{
    
    }
fmt.Println(unsafe.Sizeof(v))  // 0

结构体切片成员的传值问题

看下面的代码

package main
import "fmt"

type Person struct {
    
    
   name   string
   age    int8
   dreams []string
}

func (p *Person) SetDreams(dreams []string) {
    
    
   p.dreams = dreams
}

func main() {
    
    
   p1 := Person{
    
    name: "小王子", age: 18}
   data := []string{
    
    "吃饭", "睡觉", "打豆豆"}
   p1.SetDreams(data)
   fmt.Println(p1.dreams)
   
   // 突然修改了data[1]的值
   data[1] = "不睡觉"
   fmt.Println(p1.dreams)
}

在设置Person的成员dreams后,不小心修改了slice的值,因为slice类型包含了指向底层数据的指针,此时Person的成员dreams也会被跟着修改
输出结果
[吃饭 睡觉 打豆豆]
[吃饭 不睡觉 打豆豆]
为了避免这种情况,正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。

func (p *Person) SetDreams(dreams []string) {
    
    
	p.dreams = make([]string, len(dreams))
	copy(p.dreams, dreams)
}

这种问题存在于slice和map,在实际编码过程中一定要注意这个问题。

猜你喜欢

转载自blog.csdn.net/yao_hou/article/details/123895107