【三】结构体(struct)详解、链表

普通玩家选择标准配置,高端玩家选择自定义配置。

一、struct简介

go语言中没有像类的概念,但是可以通过结构体struct实现oop(面向对象编程)。struct的成员(也叫属性或字段)可以是任何类型,如普通类型、复合类型、函数、map、interface、struct等,所以我们可以理解为go语言中的“类”。

二、struct详解

1、struct定义

在定义struct成员时候区分大小写,若首字母大写则该成员为公有成员(对外可见),否则是私有成员(对外不可见)。

struct {
    字段1 类型
    字段2 字段3 类型
    ...
}

自定义一个结构体类型

type struct_variable_type struct {
   member member_type
   member member_type
   .....
   member member_type

// 示例
type student struct{
    name string
    sex string
    id int
    score int
}

声明与初始化

var stu1 Student
var stu2 *Student = &Student{}  //简写stu2 := &Student{}
var stu3 *Student = new(Student)  // 简写stu3 := new(Student)
2、struct使用

  在struct中,无论使用的是指针的方式声明还是普通方式,访问其成员都使用“.”,在访问的时候编译器会自动把stu2.name转为(*stu2).name。

struct分配内存使用new,返回的是指针
struct没有构造函数,但是可以自定义“构造函数”。
struct是自定义的类型,不能和其他类型进行强制转换。

package main

import "fmt"

type Student struct{
    name string
    age int
    Class string
}
func main(){
    var stu1 Student
    stu1.age = 22
    stu1.name = "wpr"
    stu1.Class = "Class1"
    fmt.Println(stu1.name,stu1.age,stu1.Class)  // wpr 22 Class1

    var stu2 *Student = new(Student)
    stu2.name = "wpr"
    stu2.age = 33
    fmt.Println(stu2.name,(*stu2).age)  // wpr 33

    var stu3 *Student = &Student{name:"wpp",age:18,Class:"class3"}
    fmt.Println(stu3.name,(*stu3).Class)  // wpp class3
}
3、自定义构造函数

以下是通过工厂模式自定义构造函数方法(工厂模式:不管丢进去什么,都产生一个对象)

package main

import "fmt"

type Student struct{
    name string
    age int
    Class string
}

func Newstu(name1 string,age1 int,class1 string) *Student {
    return &Student{name:name1,age:age1,Class:class1}
}

func main()  {
    stu1 := Newstu("wpr",22,"math")
    fmt.Println(stu1.name)  // wpr
}
4、tag

tag可以为结构体的成员添加说明或者标签便于使用,这些说明可以通过反射获取到。
结构体中的成员首字母小写对外不可见,但是我们把成员定义为首字母大写,这样与外界进行数据交互会带来极大的不便,此时tag带来了解决方法。

type Student struct {
    Name string "the name of student"
    Age int "the age of student"
    Class string "the class of student"
}

应用场景,json序列化操作:

package main

import (
    "encoding/json"
    "fmt"
)

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

func main()  {
    var stu = Student{Name:"wpr",Age:22}
    data,err := json.Marshal(stu)  // json.Marshal是将数据编码成json字符串
    if err != nil{
        fmt.Println("json encode failed err:",err)
        return
    }
    fmt.Println(string(data))  // {"Name":"wpr","age":22}
}
5、匿名成员(字段、属性)

结构体中,每个成员不一定都有名称,也允许字段没有名字,即匿名成员。
匿名成员的一个重要作用,可以用来实现oop中的继承
同一种类型匿名成员只允许最多存在一个。
当匿名成员是结构体时,且两个结构体中都存在相同字段时,优先选择最近的字段。

package main

import "fmt"

type Person struct {
    Name string
    Age int
}
type Student struct{
    score string
    Age int
    Person
}

func main()  {
    var stu = new(Student)
    stu.Age = 22  // 优先选择Student中的Age
    fmt.Println(stu.Person.Age,stu.Age)  // 0 22
}
6、继承、多继承

  当结构体中的成员也是结构体时,该结构体就继承了这个结构体,继承了其所有的方法与属性,当然有多个结构体成员也就是多继承。
  访问父结构中属性也使用“.”,但是当子结构体中存在和父结构中的字段相同时候,只能使用:“子结构体.父结构体.字段”访问父结构体中的属性,如上面示例的stu.Person.Age。
  继承结构体可以使用别名,访问的时候通过别名访问,如下面示例man1.job.Salary:

package main

import "fmt"

type Person struct{
    Name string
    Age int
}
type Teacher struct{
    Salary int
    Classes string
}
type man struct{
    sex string
    job Teacher // 别名,继承Teacher
    Person // 继承Person
}

func main()  {
    var man1 = new(man)
    man1.Age = 22
    man1.Name = "wpr"
    man1.job.Salary = 8500
    fmt.Println(man1,man1.job.Salary)  // &{ {8500 } {wpr 22}} 8500
}
7、结构体中的方法

  go语言中的方法是作用在特定类型的变量上,因此自定义的类型都可以有方法,不仅仅是在结构体中。
  go中的方法和传统的类的方法不太一样,方法和类并非组织在一起,传统的oop方法和类放在一个文件里面,而go语言只要在同一个包里就可,可分散在不同文件里。go的理念就是数据和实现分离,引用官方说法:“Methods are not mixed with the data definition (the structs): they are orthogonal to types; representation(data) and behavior (methods) are independent”。
  方法的调用通过recv.methodName(),其访问控制也是通过大小写区分。
方法定义,其中recv代表方法作用的结构体:

func (recv type) methodName(parameter_list) (return_value_list) {...}
package main

import "fmt"
type Person struct{
    Name string
    Age int
}

func (p Person) Getname() string {  //p代表结构体本身的实例,类似python中的self,这里p可以写为self
    fmt.Println(p.Name)
    return p.Name
}

func main()  {
    var person1 = new(Person)
    person1.Age = 22
    person1.Name = "wpr"
    person1.Getname()
}
// 结果:wpr

当有了结构的方法时候,我们可以自定义其初始化方法,由于结构体是值类型,所以我们使用指针才能改变其存储的值

package main

import "fmt"

type Person struct{
    Name string
    Age int
}

func (self *Person) init(name string,age int){
    self.Name = name
    self.Age = age
}
func main(){
    var person1 = new(Person)
    person1.init("wpr",23)
    // (&person1).init("wpr",23)
    fmt.Println(person1)  //&{wpr 23}
}

如果实现了结构体中的String方法,在使用fmt打印时候会调用该方法,类似与python中的_ str _方法。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (self *Person) String() string {
    return self.Name
}

func main() {
    var person1 = new(Person)
    person1.Name = "wpr"
    person1.Age = 22
    fmt.Println(person1)  // wpr
}
8、内存分布

go中的结构体内存布局和c结构体布局类似,每个成员的内存分布是连续的,在以下示例中通过反射进行进一步说明:

package main

import (
    "fmt"
    "reflect"
)

type Student struct {
    Name  string
    Age   int64
    wight int64
    high  int64
    score int64
}

func main() {
    var stu1 = new(Student)
    fmt.Printf("%p\n", &stu1.Name)
    fmt.Printf("%p\n", &stu1.Age)
    fmt.Printf("%p\n", &stu1.wight)
    fmt.Printf("%p\n", &stu1.high)
    fmt.Printf("%p\n", &stu1.score)
    typ := reflect.TypeOf(Student{})
    fmt.Printf("Struct is %d bytes long\n", typ.Size())
    n := typ.NumField()
    for i := 0; i < n; i++ {
        field := typ.Field(i)
        fmt.Printf("%s at offset %v,size=%d,align=%d\n", field.Name, field.Offset, field.Type.Size(), field.Type.Align())
    }

}
/* 结果
0xc00008e000
0xc00008e010
0xc00008e018
0xc00008e020
0xc00008e028
Struct is 48 bytes long
Name at offset 0,size=16,align=8
Age at offset 16,size=8,align=8
wight at offset 24,size=8,align=8
high at offset 32,size=8,align=8
score at offset 40,size=8,align=8
*/

在以上结果中,可以看到内存地址的偏移总是以8字节偏移(使用的是int64,刚好是8字节),再观察其内存地址,也是连续的,所以go语言中的结构体内存布局是连续的,如下图:

三、使用struct实现链表

  链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
  链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
  链表有很多种不同的类型:单向链表,双向链表以及循环链表。

单链表

单链表:每个结点包含下一个结点的地址,这样把所有结点都串起来的链式数据结构叫做链表通常把链表中的第一个结点叫做表头。
为了方便,数据区域这里使用int

type Node struct {
    data int
    next *node
}

链表遍历
  链表的遍历是通过移动指针进行遍历,当指针移动到最后一个结点时,其next指针为nil。

package main

import "fmt"

type Node struct {
    data int
    next *Node
}

// 遍历
func Shownode(p *Node) {
    for p != nil {
        fmt.Println(*p)
        p = p.next // 移动指针
    }
}

func main() {
    var head = new(Node)
    head.data = 1
    var node1 = new(Node)
    node1.data = 2

    head.next = node1
    var node2 = new(Node)
    node2.data = 3

    node1.next = node2
    Shownode(head)
}
/* 结果
{1 0xc000082030}
{2 0xc000082040}
{3 <nil>}
*/

插入结点
单链表的结点插入方法一般使用头插法或者尾插法。
头插法:每次插入在链表的头部插入结点。

package main

import "fmt"

type Node struct {
    data int
    next *Node
}

func Shownode(p *Node) { // 遍历
    for p != nil {
        fmt.Println(*p)
        p = p.next // 移动指针
    }
}

func main() {
    var head = new(Node)
    head.data = 0
    var tail *Node
    tail = head // tail用于记录头结点的地址,刚开始tail的指针指向头结点
    for i := 1; i < 10; i++ {
        var node = Node{data: i}
        node.next = tail // 将新插入的node的next指向头结点
        tail = &node     // 重新赋值头结点
    }
    Shownode(tail) // 遍历结果
}
/* 
{9 0xc0000880b0}
{8 0xc0000880a0}
{7 0xc000088090}
{6 0xc000088080}
{5 0xc000088070}
{4 0xc000088060}
{3 0xc000088050}
{2 0xc000088040}
{1 0xc000088030}
{0 <nil>}
*/

尾插法:每次插入结点在尾部

package main

import "fmt"

type Node struct {
    data int
    next *Node
}

func Shownode(p *Node) { // 遍历
    for p != nil {
        fmt.Println(*p)
        p = p.next // 移动指针
    }
}

func main() {
    var head = new(Node)
    head.data = 0
    var tail *Node
    tail = head // tail用于记录最末尾的结点的地址,刚开始tail的指针指向头结点
    for i := 1; i < 10; i++ {
        var node = Node{data: i}
        (*tail).next = &node
        tail = &node
    }
    Shownode(head) // 遍历结果
}
/* 结果
{0 0xc000010050}
{1 0xc000010060}
{2 0xc000010070}
{3 0xc000010080}
{4 0xc000010090}
{5 0xc0000100a0}
{6 0xc0000100b0}
{7 0xc0000100c0}
{8 0xc0000100d0}
{9 <nil>}
*/
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容