Golang 复合数据类型:结构体

结构体的定义

结构体也是一种复合类型,通常使用带属性的结构体来表示一个现实中的实体。结构体也是值类型,因此可以通过使用 new 函数来创建。Go语言不是一门传统的面向对象的编程语言,因此 Go 中没有类的概念,也不支持类的面向对象概念。结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。

结构体成员也可以称为“字段”,这些字段有以下特性:

  • 字段拥有自己的类型和值;
  • 字段名必须唯一;
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型。
type Family struct {
    name string
    age int
    language string
    ...
}

type Family struct {
    name, language string //同一类型的变量可以放在一起声明
    age int
    ...
}

结构体的定义只是一种内存布局的描述,只要当结构体实例化时,才会真正的分配内存空间

结构体的实例化

实例化是指根据结构体内定义的格式创建一份与格式一致的内存区域,结构体实例与实例之间是完全独立的。

在 Go 语言中,有多种实例化结构体的方式:

  • 基本的实例化方式(使用 var )

    var a Family //Famlily表示结构体类型, a表示结构体的实例

    无论变量是一个结构体类型还是结构体类型指针,都可以使用选择器(点符号) . 来访问结构体内的成员变量,如 a.namea.age 等,结构体的成员变量赋值方法与普通变量赋值方法一致。

    //定义结构体
    type Family struct {
        name,lang,skill string
        age int
    }
    //实例化结构体
    func main()  {
        //Family是结构体类型,a是结构体的实例
        var a Family
        a.name = "小马"
        a.lang = "zh"
        a.age = 18
        a.skill = "python"
        //打印结构体的属性值
        fmt.Println(Family(a))
    }
    
    /*
    {小马 zh python 18 false}
    */
  • 创建指针类型的结构体(使用 new )

    还可以使用 new 函数对类型(包括结构体、整型、浮点型、字符串等)进行实例化,结构体在使用 new 函数实例化后形成指针类型的结构体。

    a := new(Animal) //Family表示结构体类型,a表示结构体实例(一个指向 Family 的指针)

    前面已经提到了,选择器 . 支持结构体指针类型,因此可以使用它来访问结构体的成员变量

    type Animal struct {
        Name    string
        Age     int
        Sex     string
        Comment string
    }
    
    func main() {
        //使用new函数实例化
        a := new(Animal)
        a.Name = "Panda"
        a.Age = 22
        a.Sex = "female"
        a.Comment = "功夫熊猫"
        //打印指向结构体的指针及其指针地址
        fmt.Println(a,&a)
    }
    
    /*
    &{Panda 22 female 功夫熊猫} 0xc000006028
    */

    通过 fmt.Println 函数打印实例 a 的输出信息,可以知道实例 a 是指向结构体类型 Animal 的一个指针,这个指针在内存中的地址是 0xc000006028

  • 取结构体的地址实例化

    在 Go 语言中,对结构体进行取地址 & 操作时,视为对该结构体类型进行一次 new 实例化操作,取地址格式为:

    a := &Book{} //Book表示结构体类型,a表示结构体实例(一个指向Book的指针)
    type Book struct {
        Name string
        Author string
        Price int
    }
    
    func main(){
        //实例化
        a := &Book{}
        a.Name = "三国演义"
        a.Author = "罗贯中"
        a.Price = 88
        fmt.Println(a)
    }
    
    /*
    &{三国演义 罗贯中 88}
    */

    在以上三种结构体实例化的方式中,使用 new(type)&(type) 对结构体类型进行实例化是等价的,它们都返回了一个执行结构体的指针 *type

  • 函数封装结构体

    为了程序的简洁性,我们也可以将结构体的实例化过程封装在函数中,调用实现即可达到实例化的目的:

    //声明结构体
    type Book struct {
        Name string
        Author string
        Price int
    }
    
    //使用函数封装实例化过程,返回*Book指针
    func newBook (name string,author string,price int) *Book {
        return &Book{
            Name: name,
            Author:author,
            Price:price,
        }
    }
    //调用newBook结构体实例化函数完成实例化
    func main(){
        fmt.Println(newBook("三国","罗贯中",88))
    }
    
    /*
    &{三国 罗贯中 88}
    */

    初始化结构体的成员变量

    允许在结构体实例化的同时初始化它的成员变量,初始化成员变量有两种方式:

    • 字段“键值对”的形式

      键值对形式的初始化适合选择性填充字段较多的结构体; 键值对的填充是可选的,不需要初始化的字段可以不填入初始化列表中

      //声明结构体
      type Book struct {
          Name   string
          Author string
          Price  int
          Version *Book //不需要初始化,默认输出类型零值,为nil
      }
      //使用键值对初始化成员变量
      func main(){
          a := Book{
              Name : "三国演义",
              Author : "罗贯中",
              Price : 88,
          }
          fmt.Println(a)
      }
      
      /*
      {三国演义 罗贯中 88 <nil>}
      */

      结构体实例化后字段的默认值是字段类型的零值,例如 ,数值为 0、字符串为 ""(空字符串)、布尔为 false、指针为 nil 等。

    • “多个值”的列表形式

      多个值的列表形式适合填充字段较少的结构体,可以在 “键值对”初始化的基础上忽略“键” ,也就是说,可以使用 多个值的列表 初始化结构体的字段。 需要注意以下几点:

      • 必须初始化结构体的所有字段
      • 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致
      • 键值对与值列表的初始化形式不能混用
      //声明结构体
      type Book struct {
          Name   string
          Author string
          Price  int
          Version *Book 
      }
      //使用键值对初始化成员变量
      func main(){
          a := Book{ //使用多个值的列表初始化
               "金瓶梅",
              "未名人",
              1998,
              nil,
          }
          fmt.Println(a)
      }
      /*
      {金瓶梅 未名人 1998 <nil>}
      */

初始化匿名结构体

  • 匿名结构体的定义

    当定义一个 struct 时,定义的时候字段名与其类型是一一对应的;当匿名字段是一个 struct 时,那么这个 struct 所拥有的全部字段都被隐式地引入当前定义的这个 struct 。匿名结构体初始化是可选的,因此在代码中可以只写定义不写初始化,也可两者都写:

    func main(){
        //匿名结构体定义,不需要用type定义就可以使用
        lala := struct{
            name string
            age int
        }{//匿名结构体初始化(可选)
            name:"钓鱼城大学完满主任",
            age:18,
        }
        //打印匿名结构体属性值
        fmt.Println(lala.name,lala.age)
    }
    
    /*
    钓鱼城大学完满主任 18
    */

    可以看出,匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。 匿名字段(也称为嵌入字段)本身可以是一个结构体类型,即结构体可以包含内嵌结构体。

### 使用匿名结构体(demo)

  • 定义一个 wanmanLeader 函数,参数为 wanman ,类型为 *struct { id string; post string; name string; food string; weight int } 因为类型没有使用 type 定义,所以需要在每次用到的地方定义类型。
  • 使用格式化字符串 %T 将匿名结构体的类型打印出来,使用 %v 将匿名结构体的默认值打印出来
  • main 函数中定义匿名结构体并使用多个值初始化成员变量
  • 将匿名结构体 wanman 传入 wanmanLeader 函数进行调用,打印匿名结构体的类型
  • 使用 fmt.Printf 函数打印匿名结构体的属性值和成员变量及其内存地址

    //构造完满主任函数,传入匿名结构体
    func wanmanLeader(wanman *struct{
        id,post,name,food string
        weight int
    }){
        //打印匿名结构体类型
        fmt.Printf("%T\n",wanman)
    }
    
    func main(){
        //匿名结构体定义
        wanman := &struct{
            id,post,name,food string
            weight int
    
        }{//匿名结构体初始化
            "WM001",
            "完满主任",
            "Xiaoma",
            "米饭",
            80,
        }
        //调用完满主任函数,打印匿名结构体的类型
        wanmanLeader(wanman)
        //打印信息
        fmt.Printf("匿名结构体的属性值:%v\n匿名结构体的内存地址:%v\n",wanman,&wanman)
    }
    
    /*
    *struct { id string; post string; name string; food string; weight int }
    匿名结构体的属性值:&{WM001 完满主任 Xiaoma 米饭 80}
    匿名结构体的内存地址:0xc00008a018
    */

### 模拟构造函数重载——多种方式创建和初始化结构体

还记得 C++ 对函数重载的定义是什么吗? 指可以有多个同名函数,因此对函数名称进行了重载 (它们完成相同的工作,但使用不同的参数列表)。且仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应该使用函数重载,这是C++的用法。

Golang 的类型或者结构体没有构造函数的功能,但依旧可以用结构体初始化来模拟函数重载。请看如下例子,如果使用结构体描述鸭子的特性,根据鸭子的叫声和颜色可以有不同种类的鸭子,那么不同种颜色和叫声就是结构体的字段,同时可以使用颜色和叫声构造不同种类鸭子的实例:

//定义鸭子特性结构体
type Duck struct {
    Call string
    Color string
}
//用叫声构造函数
func NewDuckByCall (call string)*Duck{
    return &Duck{//取地址实例化结构体
        Call:call,
    }
}
//用颜色构造函数
func NewDuckByColor (color string)*Duck{
    return &Duck{//取地址实例化结构体
        Color:color,
    }
}

在以上代码中,由于 Golang 没有函数重载的功能,为了避免函数名字冲突,只能使用 NewDuckByCall()NewDuckByColor() 两个不同函数名(模拟函数重载)来实现不同鸭子的构造过程。

模拟父级构造调用——带有父子关系的结构体

让我们来回顾 C++ 中对于基类和派生类的定义:当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。 这个已有的类称为基类,新建的类称为派生类

在 Golang 中没有提供构造函数相关的特殊机制,用户根据自己的需求,将参数使用函数传递到结构体构造参数中即可完成构造函数的任务。由于 Go 不是传统的 OOP 语言,也没有类的定义,但是可以用结构体模拟实现类的继承。

例如,小黄鸭是鸭子的一个种类,鸭子是小黄鸭的泛称,同时描述这两种概念时,就是派生 小黄鸭派生自鸭子的种类,鸭子是小黄鸭的基类 。使用结构体描述小黄鸭与鸭子的关系时, 将鸭子(Duck)的结构体内嵌到小黄鸭(littleDuck)中,表示小黄鸭拥有鸭子的特性 (相当于派生),然后再使用两个不同的构造函数分别构造出小黄鸭和鸭子的两个实例:

//定义鸭子特性结构体
type Duck struct {
    Call string
    Color string
}
//派生
type littleDuck struct {
    Duck //内嵌鸭子结构体,表示小黄鸭拥有鸭子的特性
}
//构造基类
func NewDuck(call string) *Duck{
    return &Duck{
        Call: "gaga",
    }
}
//构造子类
func NewlittleDuck(color string) *littleDuck {
    duck := new(littleDuck) //实例化小黄鸭结构体,此时鸭子也被实例化
    duck.Color = color  
    return duck
}
  • 定义小黄鸭结构体 littleDuck ,内嵌了鸭子结构体 DucklittleDuck 拥有 Duck 的所有成员,实例化后可以访问所有的 Duck 成员;
  • NewDuck 函数定义了 Duck 的构造过程,使用鸭子叫声 call 作为参数,返回一个指向鸭子结构体 Duck 的指针;
  • NewlittleDuck 函数定义 littleDuck 的构造过程,使用颜色 color 作为参数,返回执行小黄鸭结构体的指针;
  • 实例化 littleDuck 结构体,此时 Duck 也被实例化;
  • 填充 littleDuck 中嵌入的 Duck 颜色属性, littleDuck 没有任何成员,所有的成员都来自于 Duck

【注】:在这个例子中, Duck 结构体类似于面向对象中的 基类 ,小黄鸭结构体 littleDuck ,内嵌鸭子结构体 Duck 则类似于面向对象中的 派生 ,实例化 littleDuck 结构体,此时 Duck 也被实例化。

欢迎关注我们的微信公众号,每天学习Go知识

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章