Go中的结构体

前面我们或多或少的都使用了结构体这种数据结构,本身结构体也有很多特性,我们一一来看。

结构体的作用是将一个或者多个任一类型的变量组合在一起的数据类型,类似于我们在Java中class的作用。在结构体重也可以嵌套结构体。结构体还可以有自己的方法。

1.定义结构体

我们先定义一个结构体:

结构体定义如下:

type 标识符 struct {
        field1 type
        field2 type
}

例子:

type Staff struct {
        UserId int16
        UserName string
        Sex byte
        Age int8
}

2. 使用结构体

有三种方式可以使用结构体:

var staff Staff
staff1 := new(Staff)
staff2 := &Staff{}

上面2和3的效果是一样的,返回的都是指向结构体的指针。

Go中的结构体不像class一样有构造函数,一般会使用上述第三种方式来构造出一个结构体对象,简称Go中的工厂模式:

package main

import "fmt"

type Staff struct {
        UserId int16
        UserName string
        Sex byte
        Age int8
}

func NewStaff(userId int16, userName string, sex byte, age int8) *Staff {
        return &Staff{
                UserId:userId,
                UserName:userName,
                Sex:sex,
                Age:age,
        }
}

func main() {
        staff := NewStaff(123,"xiaoming",byte(1),13)
        fmt.Println(staff)
}

3. 带标签的结构体

结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。 标签的内容不可以在一般的编程中使用,只有通过反射机制才能能获取它

我们现在有这样一段程序:从json文件中读取json字符串,然后转为json对象:

json文件内容:

{
    "port": "7788",
    "address": "47.95.34.2"
}

代码如下:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
)

type MainConfig struct {
    port                string
    address             string

}


func LoadConfig(path string) *MainConfig {
    buf, err := ioutil.ReadFile(path)
    if err != nil {
        log.Panicln("load config conf failed: ", err)
    }
    mainConfig := &MainConfig{}
    err = json.Unmarshal(buf, mainConfig)
    if err != nil {
        log.Panicln("decode config file failed:", string(buf), err)
    }
    fmt.Println(mainConfig.address,mainConfig.port)

    return mainConfig
}

func main() {
    LoadConfig("c:/test.json")

}

执行以上程序可以发现打印出来的结果是空的,原因是:Go开发规范认为:只有开头是大写字母的对象,方法才被认为是公开的,可以在包外访问,否则就是私有的,外部对象无法访问。

那我们定义结构体大写之后,但是想让结构体中的字段json格式化为小写应该怎么做呢?这时候就可以通过tag来指定:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
)

type MainConfig struct {
    Port string `json:"port"`
    Address string `json:"address"`
}

//反射获取字段中的tag
func reflectTag(mg MainConfig)  {
    mgType := reflect.TypeOf(mg)
    tag0 := mgType.Field(0).Tag
    tag1 := mgType.Field(0).Tag
    fmt.Println(tag0,tag1)
}

func LoadConfig(path string) *MainConfig {
    buf, err := ioutil.ReadFile(path)
    if err != nil {
        log.Panicln("load config conf failed: ", err)
    }
    mainConfig := &MainConfig{}
    err = json.Unmarshal(buf, mainConfig)
    if err != nil {
        log.Panicln("decode config file failed:", string(buf), err)
    }
    bytes, err := json.Marshal(mainConfig)
    fmt.Println(string(bytes))
    return mainConfig
}

func main() {
    LoadConfig("c:/test.json")
    config := MainConfig{"1234", "123.221.134"}
    reflectTag(config)

}
打印结果:
{"port":"7788","address":"47.95.34.2"}
json:"port" json:"port"

上面的代码是一段从json文件中读取配置文件的例子,也使用了反射机制来获取tag标签,使用 json.Unmarshal来反序列化,使用json.Marshal将对象序列化为json字符串。

4.方法

其实Go中任何自定义的类型都可以有方法,不仅仅是struct才有。除了指针和interface。

看一个自定义类型带方法的例子:

package main

import "fmt"

type MainConfig1 struct {
    Port string `json:"port"`
    Address string `json:"address"`
}

type Str string

func (s Str) Compact(str string,str1 string) string  {
    return str + str1
}

func (s *Str) Compact1(str string,str1 string) Str {
    return Str(str + str1)
}

//mf相当于其他语言中的this,self表示当前对象本身
func (mf MainConfig1) Compact2() Str {
    return Str(mf.Port + "|" + mf.Address)
}


func main() {
    var s Str
    compact1 := s.Compact("a", "b")
    str := s.Compact1("c", "d")
    fmt.Println(compact1,str)
    
    var mf MainConfig1
    mf.Port = "2"
    mf.Address = "3333"
    mf.Compact2()

}

声明 Str类型的s,通过s可以调用这两个方法。另外还记得Go的访问控制规范吧,首字母大写表示公共方法,小写表示私有,自定义类型的方法同样遵循这个原则,试着把 Compact1方法的首字母改为小写,你会发现通过 s.Compact1("c", "d")找不到方法。

再看Compact2()方法的使用,使用当前接收者本身来获取属性,跟Java中的this关键字相似。

上面还有一个疑问点:

使用 Str 和使用 *Str作为接受者有什么不同呢?

区别就在于:在接收者是指针时,方法可以改变接收者的值(或状态)

我们来改造一下Compact2()方法:

func (mf MainConfig1) Compact2() Str {
    mf.Address = "rrrr"
    return Str(mf.Port + "|" + mf.Address)
}

var mf MainConfig1
mf.Port = "2"
mf.Address = "3333"
compact2 := mf.Compact2()
fmt.Println(compact2)
bytes, _ := json.Marshal(mf)
fmt.Println(string(bytes))

输出的结果是:

2|rrrr
{"port":"2","address":"3333"}

函数作用域内的Address值被改变了,但是实际上mf对象的属性值并没有被改变。

那么我们将receiver 改为指针试一下:

func (mf *MainConfig1) Compact2() Str {
    mf.Address = "rrrr"
    return Str(mf.Port + "|" + mf.Address)
}

var mf MainConfig1
mf.Port = "2"
mf.Address = "3333"
compact2 := mf.Compact2()
fmt.Println(compact2)
bytes, _ := json.Marshal(mf)
fmt.Println(string(bytes))

输出的结果是:

2|rrrr
{"port":"2","address":"rrrr"}

会发现这次mf的属性值也会改变。因为本身我们使用的this对象就是 *MainConfig1自然可以改变对象内部的值。

5. 函数和方法的区别

函数将变量作为参数:Function1(recv)

方法在变量上被调用:recv.Method1()

在接收者是指针时,方法可以改变接收者的值(或状态)

receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。

在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。