Go语言5-结构体

骑士救兵 · · 859 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

结构体

Go中的结构体(就相当于其它语言里的class):

  • 用来定义复杂的数据结构
  • 可以包含多个字段(属性)
  • 结构体类型可以定义方法,注意和函数的区分
  • 结构体是值类型
  • 结构体可以嵌套
  • Go语言没有class类型,只有struct类型

定义结构体

struct 声明:

type (标识符) struct {
    field1 type
    field2 type
}

例子:

type Student struct {
    Name string
    Age int
    Score int
}

结构体中字段的访问,和其他语言一样,使用点:

package main

import "fmt"

type Student struct {
    Name string
    Age int
    Score int
}

func main() {
    var stu Student
    stu.Name = "Adam"
    stu.Age = 18
    stu.Score = 90
    fmt.Println(stu)
    fmt.Println(stu.Name)
    fmt.Println(stu.Age)
    fmt.Println(stu.Score)
}

struct 定义的3种形式:

var stu Student
var stu *Student = new (Student)
var stu *Student = &Student{}

后两种返回的都是指向结构体的指针,所以需要再跟个等号分配内存空间。并且,有些场景应该是需要用指针的结构体会更加方便。
强调一下, struct 是值类型。这里要用new来创建值类型。不是make,make是用来创建 map 、slice、channel 的。
结构体的访问形式如下:

stu.Name
(*stu).Name

用上面两种形式访问都是可以的,但是定义的时候返回指针的话,标准做法还是应该用指针来访问的,不过Go做了处理,可以简化,直接用第一种就够了,但是要知道调用的本质。

struct 的内存布局

结构体是值类型,里面所有的字段在内存里是连续的:

package main

import "fmt"

type Student struct {
    Name string
    Age int
    Score int
}

func main() {
    var stu Student
    stu.Name = "Adam"
    stu.Age = 18
    stu.Score = 90
    fmt.Println(stu)
    fmt.Printf("%p\n", &stu)
    fmt.Printf("%p\n", &stu.Name)
    fmt.Printf("%p\n", &stu.Age)
    fmt.Printf("%p\n", &stu.Score)
}

/* 执行结果
PS H:\Go\src\go_dev\day5\struct\attribute> go run main.go
{Adam 18 90}
0xc04204a3a0
0xc04204a3a0
0xc04204a3b0
0xc04204a3b8
PS H:\Go\src\go_dev\day5\struct\attribute>
*/

struct 初始化

package main

import "fmt"

type Student struct {
    Name string
    Age int
    Score int
}

func main(){
    var stu1 Student
    stu1.Name = "Adam"
    stu1.Age = 16
    stu1.Score = 90

    var stu2 Student = Student{
        Name: "Bob",
        Age: 15,
        Score: 85,
    }

    var stu3 *Student = &Student{
        Name: "Cara",
        Age: 18,
        Score: 80,
    }

    fmt.Println(stu1)
    fmt.Println(stu2)
    fmt.Println(&stu2)
    fmt.Println(stu3)
    fmt.Println(*stu3)
}

/* 执行结果
PS H:\Go\src\go_dev\day5\struct\init> go run .\main.go
{Adam 16 90}
{Bob 15 85}
&{Bob 15 85}
&{Cara 18 80}
{Cara 18 80}
PS H:\Go\src\go_dev\day5\struct\init>
*/

也可以在大括号里按位置传参数进行初始化和定义:

type Student struct {
    Name string
    Age int
    Score int
}

var s1 Student
s1 = Student {"stu1", 18, 90}
var s2 Student = Student{"stu2", 20, 80}

数据结构

用结构体定义数据类型

链表

每个节点包含下一个节点的地址,这样就把所有的节点串起来了。通常把链表中的第一个节点叫做链表头。

type Link struct {
    Name string
    Next *Link
}

下面有头插法和尾插法创建链表,还有遍历链表的方法:

package main

import "fmt"

type Student struct {
    Name string
    next *Student
}

// 遍历链表的方法
func trans(p *Student) {
    for p != nil {
        fmt.Println(*p)
        p = p.next
    }
}

// 头插法,从左边插入
// 每个新加入的元素都插入到头部元素的后面,这样的好处是头部元素的地址不变
func CreateLinkListLeft(p *Student) {
    var head = p
    for i := 0; i < 10; i++ {
        p := Student{
            Name: fmt.Sprintf("stuL%d", i),
        }
        p.next = head.next
        head.next = &p
    }
}

// 尾插法,从右边加入
func CreateLinkListRight(p *Student) {
    var tail = p
    for i := 0; i < 10; i++ {
        p := Student{
            Name: fmt.Sprintf("stuR%d", i),
        }
        tail.next = &p
        tail = &p
    }
}

func main() {
    var headL Student
    fmt.Println("头插法")
    CreateLinkListLeft(&headL)  // 结构体是值类型,要改变里面的值,就是传指针
    trans(&headL)

    var headR Student
    fmt.Println("尾插法")
    CreateLinkListRight(&headR)
    trans(&headR)
}

还有双链表,详细就不展开了:

type Link struct {
    Name string
    Next *Link
    Prev *Link
}

二叉树

每个节点都有2个指针,分别用来指向左子树和右子树:

type binaryTree  struct {
    Name string
    left *binaryTree
    right *binaryTree
}

这里只给一个深度优先的遍历方法:

// 遍历二叉树,深度优先
func trans(root *Student) {
    if root == nil {
        return
    }
    // 前序遍历
    fmt.Println(root)
    trans(root.left)
    trans(root.right)
}

最后3句的相对位置,主要是打印的方法的位置不同又有3种不同的叫法。上面这个是前序遍历。如果打印放中间就是中序遍历。如果打印放最后,就是后序遍历。
广度优先的遍历方法,暂时能力还不够。另外如果要验证上面的遍历方法,也只能用笨办法来创建二叉树。

结构体进阶

看下结构体里的一些高级用法

定义别名

可以给结构体取别名:

type Student struct {
    Name string
}

type Stu Student  // 取个别名

下面的代码,给原生的int类型取了个别名,也是可以像int一样使用的:

package main

import "fmt"

type integer int

func main() {
    var i = integer = 100
        fmt.Println(i)
}

但是定义了别名的类型和原来的类型被系统认为不是同一个类型,不能直接赋值。但是是可以强转类型的:

type integer int

func main() {
    var i integer = 100
        var j int
        // j = i  // 不同的类型不能赋值
        j = int(i)  // 赋值需要强转类型
}

上面都是用原生的 int 类型演示的,自定义的结构体也是一样的。

工厂模式(构造函数)

golang 中的 struct 不像其他语言里的 class 有构造函数。struct 没有构造函数,一般可以使用工厂模式来解决这个问题:

// go_dev/day5/struct/new/model/model.go
package model

type student struct {
    Name string
    Age int
}

func NewStudent(name string, age int) *student {
    // 这里可以补充其他构造函数里的代码
    return &student{
        Name: name,
        Age: age,
    }
}

// go_dev/day5/struct/new/main/main.go
package main

import (
    "../model"
    "fmt"
)

func main() {
    s := model.NewStudent("Adam", 20)
    fmt.Println(*s)
}

struct中的tag

可以为 struct 中的每个字段,写上一个tag。这个 tag 可以通过反射的机制获取到。
为字段加说明

type student struct {
    Name string  "This is name field"
    Age int  "This is age field"
}

json序列化
最常用的场景就是 json 序列化和反序列化。先看一下序列化的用法:

package main

import (
    "fmt"
    "encoding/json"
)

type Stu1 struct{
    name string
    age int
    score int
}

type Stu2 struct {
    Name string
    Age int
    score int  // 这个还是小写,所以还是会有问题
}

func main() {
    var s1 Stu1 = Stu1 {"Adam", 16, 80}
    var s2 Stu2 = Stu2 {"Bob", 17, 90}
    var data []byte
    var err error
    data, err = json.Marshal(s1)
    if err != nil {
        fmt.Println("JSON err:", err)
    } else {
        fmt.Println(string(data))  // 类型是 []byte 转成 string 输出
    }
    data, err = json.Marshal(s2)
    if err != nil {
        fmt.Println("JSON err:", err)
    } else {
        fmt.Println(string(data))
    }
}

/* 执行结果
PS H:\Go\src\go_dev\day5\struct\json> go run main.go
{}
{"Name":"Bob","Age":17}
PS H:\Go\src\go_dev\day5\struct\json>
*/

结构体中,小写的字段外部是访问不了的,所以第一个输出是空的。而第二个结构体中只有首字母大写的字段才做了序列化。
所以一般结构体里的字段名都是首字母大写的,这样外部才能访问到。不过这样的话,序列化之后的变量名也是首字母大写的。而json是可以实现跨语言传递数据的,但是在其他语言里,都是习惯变量小写的。这样go里json序列化出来的数据在别的语言里看就很奇怪。
在go的json包里,通过tag帮我们做了优化。会去读取字段的tag,去里面找到json这个key,把对应的值,作为字段的别名。具体做法如下:

package main

import (
    "fmt"
    "encoding/json"
)

type Student struct{
    Name string `json:"name"`
    Age int `json:"age"`
    Score int `json:"score"`
}

func main() {
    var stu Student = Student{"Cara", 16, 95}
    data, err := json.Marshal(stu)
    if err != nil {
        fmt.Println("JSON err:", err)
        return
    }
    fmt.Println(string(data))  // 类型是 []byte 转成 string 输出
}

/* 执行结果
PS H:\Go\src\go_dev\day5\struct\json_tag> go run main.go
{"name":"Cara","age":16,"score":95}
PS H:\Go\src\go_dev\day5\struct\json_tag>
*/

反引号,作用和双引号一样,不过内部不做转义。

匿名字段

结构体力的字段可以没有名字,即匿名字段。

type Car struct {
    Name string
    Age int
}

type Train struct {
    Car  // 这个Car也是类型,上面定义的。这里没有名字
    Start time.TIme
    int  // 这个字段也没有名字,即匿名字段
}

访问匿名字段
可以直接通过匿名字段的类型来访问,所以匿名字段的类型不能重复:

var t Train
t.Car.Name = "beemer"
t.Car.Age = 3
t.int = 100

对于结构体类型,还可以在简化,结构体的名字可以不写,下面的赋值和上面的效果一样:

var t Train
t.Name = "beemer"
t.Age = 3

匿名字段冲突处理

type Car struct {
    Name string
    Age int
}

type Train struct {
    Car
    Start time.TIme
    Age int  // 这个字段也叫 Age
}

var t Train
t.Age  // 这个Age是Train里的Age
t.Car.Age  // Car里的Age现在只能把类型名加上了

通过匿名字段实现继承
匿名字段在需要有继承的的场景下很好用:

type Animal struct {
    Color string
    Age int
    Weight int
    Type string
}

type Dog Struct {
    Animal 
    Name string
    Weight float32
}

定义了一个 Animal 动物类,里面有很多属性。再定义一个 Dog 狗的类,也属于动物,需要继承动物的属性。这里用匿名字段就方便的继承过来了。并且有些字段还可以再重新定义覆盖原先的,比如例子里的 Weight 。这样 Dog 就有 Animal 的所有的字段,并且 Dog 还能添加自己的字段,也可以利用冲突覆盖父类里的字段。

方法(method)

Golang 中的方法是作用在特定类型的变量上的。因此自定义类型也可以有方法,而不仅仅是 struct 。

定义方法

func (变量名 方法所属的类型) 方法名 (参数列表) (返回值列表) {}
方法和函数的区别就是在func关键字后面多了 (变量名 方法所属的类型) 。这个也别称为方法的接收器(receiver)。这个是声明这个方法是属于哪个类型,这里的类型也包括 struct。
Golang里的接收器没有 this 或者 self 这样的特殊关键字,所以名字可以任意取的。一般而言,出于对一致性和简短的需要,我们使用类型的首字母。类比 self ,也就知道这个接收器的变量在方法定义的代码块里就是代指当前类型的实例。

package main

import "fmt"

type Student struct {
    Name string
    Age int
}

func (s *Student) growup () {
    s.Age++
} 

func (s *Student) rename (newName string) {
    s.Name = newName
} 

func main() {
    var stu Student = Student{"Adam", 17}
    fmt.Println(stu)
    stu.growup()
    fmt.Println(stu)
    stu.rename("Bob")
    fmt.Println(stu)
}

/* 执行结果
PS H:\Go\src\go_dev\day5\method\beginning> go run main.go
{Adam 17}
{Adam 18}
{Bob 18}
PS H:\Go\src\go_dev\day5\method\beginning>
*/

上面的2个方法里的接收器类型加了星号。如果不用指针的话,传入的是对象的副本,方法改变是副本的值,不会改变原来的对象。
另外上面调用方法的用法也已经简写了,实际是通过结构体的地址调用的 (&stu).growup()

继承

匿名字段就是继承的用法。不但可以继承字段,方法也是继承的:

package main

import "fmt"

type Animal struct {
    Type string
}

func (a Animal) hello() {
    fmt.Println(a.Type, "Woo~~")
}

type Dog struct {
    Animal
    Name string
}

func main() {
    var a1 Animal = Animal{"Tiger"}
    var d1 Dog
    d1.Type = "Labrador"
    d1.Name = "Seven"
    a1.hello()
    d1.hello()  // Dog 也能调用 Animal 的方法
}

多继承
一个 struct 里用了多个匿名结构体,那么这个结构体就可以直接访问多个匿名结构体的方法,从而实现了多继承。

组合

如果一个 struct 嵌套了另一个匿名 struct,这个结果可以直接访问匿名结构的方法,从而实现了继承。
如果一个 struct 嵌套了另一个有名 struct,这个模式就叫组合:

package main

import "fmt"

type School struct {
    Name string
    City string
}

type Class struct {
    s School
    Name string
}

func main() {
    var s1 School = School{"SHHS", "DC"}
    var c1 Class
    c1.s = s1
    c1.Name = "Class One"
    fmt.Println(c1)
    fmt.Println(c1.s.Name)
}

继承与组合的区别:

  • 继承:建立了派生类与基类之间的关系,它是一种“是”的关系,比如:狗是动物。当类之间有很多相同的功能,提取这些共同的功能做成基类,用继承比较好。
  • 组合:建立了类与组合类之间的关系,它是一种“有”的关系,比如老师有生日,老师有课程,老师有学生

实现 String()

如果一个变量实现了 String() 方法,那么 fmt.Println 默认会调用这个变量的 String() 进行输出。

package main

import "fmt"

type Animal struct {
    Type string
    Weight int
}

type Dog struct {
    Animal
    Name string
}

func (d Dog) String () string{
    return d.Name + ": " + d.Type
}

func main(){
    var a1 Animal = Animal{"Tiger", 230}
    var d1 Dog = Dog{Animal{"Labrador", 100}, "Seven"}
    fmt.Println(a1)
    fmt.Println(d1)
}

/* 执行结果
PS H:\Go\src\go_dev\day5\method\string_method> go run main.go
{Tiger 230}
Seven: Labrador
PS H:\Go\src\go_dev\day5\method\string_method>
*/

注意传值还是传指针,例子里定义的时候没有星号,是传值的,打印的时候也是传值就有想过。打印的使用用 fmt.Println(&d1) 也是一样的。但是如果定义的时候用了星号,就是传指针,打印的时候就必须加上&把地址传进去才有效果。否则就是按照原生的方法打印出来。

接口(多态)

这是go语言多态的实现方式
Interface 类型可以定义一组方法,但是这些不需要实现,并且 interface 不能包含任何变量。

定义接口

定义接口使用 interface 关键字。然后只需要再里面定义一个或者多个方法就好,不需要实现:

type 接口名 interface {
    方法名1(参数列表)
    方法名2(参数列表) [返回值]
    方法名3(参数列表) [返回值]
}

interface 类型默认是一个指针,默认值是空 nil 。

接口实现

Golang 中的接口,不需要显式的实现,只要一个变量,含有接口类型中的所有方法,那么这个变量就实现了这个接口。因此,golang 中没有 implement 类型的关键字
如果一个变量含有了多个 interface 类型的方法,那么这个变量就实现了多个接口。
下面是一个接口实现的示例:

package main

import "fmt"

// 定义一个接口
type AnimalInterface interface {
    Sleep()  // 定义一个方法
    GetAge() int  // 再定义一个有返回值的方法
}

// 定义一个类
type Animal struct {
    Type string
    Age int
}  // 接下来要实现接口里的方法

// 实现接口的一个方法
func (a Animal) Sleep() {
    fmt.Printf("%s need sleep\n", a.Type)
}

// 实现了接口的另一个方法
func (a Animal) GetAge() int {
    return a.Age
}

// 又定义了一个类,是上面的子类
type Pet struct {
    Animal
    Name string
}

// 重构了一个方法
func (p Pet) sleep() {
    fmt.Printf("%s need sleed\n", p.Name)
}  // 有继承,所以Age方法会继承父类的

func main() {
    var a1 Animal = Animal{"Dog", 5}  // 创建一个实例
    var aif AnimalInterface  // 创建一个接口
    aif = a1  // 因为类里实现了接口的方法,所以可以赋值给接口
    aif.Sleep()  // 可以用接口调用
    a1.Sleep()  // 使用结构体调用也是一样的效果,这就是多态

    var p1 Pet = Pet{Animal{"Labrador", 4}, "Seven"}
    aif = p1
    aif.Sleep()
    fmt.Println(aif.GetAge())
}

多态

一种食物的多种形态,都可以按照统一的接口进行操作。
多态,简单点说就是:"一个接口,多种实现"。比如 len(),你给len传字符串就返回字符串的长度,传切片就返回切片长度。

package main

import "fmt"

func main() {
    var s1 string = "abcdefg"
    fmt.Println(len(s1))
    var l1 []int =  []int{1, 2, 3, 4}
    fmt.Println(len(l1))
}

参照上面的,自己写的方法也可以接收接口作为参数,对不同的类型对应多种实现:

package main

import "fmt"

type Msg interface {
    Print()
}

type Message struct {
    msg string
}

func (m Message) Print() {
    fmt.Println("Message:", m.msg)
}

type Information struct {
    msg string
    level int
}

func (i Information) Print() {
    fmt.Println("Information:", i.level, i.msg)
}

func interfaceUse(m Msg) {
    m.Print()
}

func main() {
    message := Message{"Hello"}  // 定义一个结构体
    information := Information{"Hi", 2}  // 定义另外一个类型的结构体
    // 这里并不需要 var 接口,以及赋值
    interfaceUse(message)  // 参数不看类型了,而看你是否满足接口
    interfaceUse(information)  // 虽然这里的参数和上面不是同一个类型,但是这里对参数的要求是接口
}

这里并不需要显示的 var 接口以及赋值。

接口嵌套

一个接口可以嵌套另外的接口:

type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}

type Lock interface {
    Lock()
    Unlock
}

type File interface {
    ReadWrite
    Lock
    Close
}

嵌套的用法类似结构体的继承。

类型断言

类型断言,由于接口是一般类型,不知道具体的类型。如果要转成具体类型,可以采用一下方法进行转换:

package main

import "fmt"

func main() {
    i := 10
    var j interface{}
    j = i  // 这里通过将变量作为一个interface{}的方法来进行下面的类型断言
    res := j.(int)  // 在这里只要调用的不是interface{}就不行,所以上面要赋值一下
    fmt.Println(res)
    fmt.Println(interface{}(j).(int))  // 上面几行可以这么写
}

上面是不带检查的,如果类型转换不成功,会报错。下面是带检查的类型断言:

package main

import "fmt"

type Num struct {
    n int
}

func main() {
    var i Num = Num{1}
    var j interface{}
    j = i  // 这里通过将变量作为一个interface{}的方法来进行下面的类型断言
    res, ok := j.(int)  // 在这里只要调用的不是interface{}就不行,所以上面要赋值一下
    fmt.Println(res, ok)
    res, ok = interface{}(j).(int)  // 上面几行可以这么写
    fmt.Println(res, ok)
}

面向对象

golang 中并没有明确的面向对象的说法,可以将 struct 类比作其它语言中的 class。

constructor 构造函数
通过结构体的工厂模式返回实例来实现

Encapsulation 封装
通过自动的大小写控制可见

Inheritance 继承
结构体嵌套匿名结构体

Composition 组合
结构体嵌套有名结构体

Polymorphism 多态
通过接口实现

课后作业

实现一个图书管理系统,具有以下功能:

  • 书籍录入功能:书籍信息包括书名、副本数、作者、出版日期
  • 书籍查询功能:按照书名、作者、出版日期等条件检索
  • 学生信息管理功能:管理每个学生的姓名、年级、×××、性别、借了什么书等信息
  • 借书功能:学生可以查询想要的书籍,进行借出
  • 书籍管理功能:可以看到每种书被哪些人借出了

有疑问加站长微信联系(非本文作者)

本文来自:51CTO博客

感谢作者:骑士救兵

查看原文:Go语言5-结构体

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

859 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传