Go 结构体

Oct 11, 2018 22:00 · 1417 words · 3 minute read Golang

和 C/C++ 的结构体类似,Go 语言的结构体 struct 是一种聚合的数据类型,可以包含任意类型的值。

自动转换

先定义一个 struct

type Person struct {
    Name string
    Age int
}

初始化

func main() {
    me := &Person{
        Name: "HF",
        Age:  1,
    }

    fmt.Printf("type of me: %T\n", me)
    fmt.Printf("my name: %s, type of my name: %T\n", (*me).Name, (*me).Name)
    fmt.Printf("my age: %d, type of my age: %T\n", (*me).Age, (*me).Age)

    fmt.Printf("my name: %s, type of my name: %T\n", me.Name, me.Name)
    fmt.Printf("my age: %d, type of my Age: %T\n", me.Age, me.Age)
}

执行命令源码文件后输出:

type of me: *main.Person
my name: HF, type of my name: string
my age: 1, type of my age: int
my name: HF, type of my name: string
my age: 1, type of my Age: int

me 是一个指针型变量,访问 (*me).XXXme.XXX 的效果是一样的,说明编译器会将 me.XXX 自动转换成 (*me).XXX,前者稍显笨拙。

内存分配

import (
    "fmt"
    "unsafe"
)

func main() {
    me := &Person{
        Name: "HF",
        Age:  1,
    }

    fmt.Printf("pointer of me: %p, size of me: %d\n", me, unsafe.Sizeof(me))
    fmt.Printf("pointer of my name: %p, size of my name: %d\n", &me.Name, unsafe.Sizeof(me.Name))
    fmt.Printf("pointer of my age: %p, size of my age: %d\n", &me.Age, unsafe.Sizeof(me.Age))
}

执行命令源码文件后输出:

pointer of me: 0xc00000a060, size of me: 8
pointer of my name: 0xc00000a060, size of my name: 16
pointer of my age: 0xc00000a070, size of my age: 8

指针变量 me 指向从 0xc00000a060 开始的内存区域,也是 Person 结构体 Name 字段的内存起始地址。Name 字段是一个 string 类型的变量,占用16字节内存,所以 Age 字段的内存起始地址是 0xc00000a060 + 0x10 也就是 0xc00000a070。而 me 变量只是存放了结构体变量的内存地址,所以占用8个字节的内存而不是24字节。

import (
    "fmt"
    "unsafe"
)

func main() {
    me := &Person{
        Name: "HF",
        Age:  1,
    }

    fmt.Printf("pointer of me: %p, size of real struct: %d\n", me, unsafe.Sizeof(*me))
}

执行命令源码文件后输出:

pointer of me: 0xc00000a060, size of real struct: 24

这里真正的结构体变量确实占用了16 + 8也就是24字节的内存。

结构体比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的。

func main() {
    me := Person{Name: "HF"}
    he := Person{}

    fmt.Println(me == he) // false
    fmt.Println(me.Name == he.Name && me.Age == he.Age) // false
}

上下两种写法是等价的。

继承(嵌入)

一个结构体可以继承多个结构体。 继承的结构体不能有同名字段。

import (
    "net/http"
)
type Request struct{}
type T struct {
    http.Request // field name is "Request"
    Request // field name is "Request"
}

编译时会报错 duplicate field Request

Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字,叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。

type Name struct {
    FirstName string
    LastName  string
}

type Person struct {
    Name
    Age int
}

func main() {
    me := new(Person)
    me.Name.FirstName = "Fluoride"
    me.Name.LastName = "Hydrogen"
    fmt.Printf("me: %#v", *me) // me: main.Person{Name:main.Name{FirstName:"Fluoride", LastName:"Hydrogen"}, Age:0}
}

但是这样类似文件目录递进的访问形式略显繁琐,得益于匿名成员的特性,直接访问子字段而不必给出完整的路径:

func main() {
    // me := &Person{FirstName: "Fluoride", LastName: "Hydrogen", Age: 1}
    me := new(Person)
    me.FirstName = "Fluoride"
    me.LastName = "Hydrogen"
    fmt.Printf("me: %#v", *me)
}

所有的内置类型和自定义类型都是可以作为匿名字段的,但是结构体面值并不支持省略写法。

反射

Go 语言通过 reflect 包实现运行时的反射,允许程序操作任意类型的对象。 典型用法是用静态类型 interface{} 保存一个值,通过调用 TypeOf() 方法获取其动态类型信息,返回一个 Type 类型值。调用 ValueOf 方法返回一个 Value 类型值,该值代表运行时的数据。

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string
    Age  int
}

func getFieldString(person *Person, field string) string {
    r := reflect.ValueOf(person)
    f := reflect.Indirect(r).FieldByName(field)
    return f.String()
}

func getFieldInteger(person *Person, field string) int {
    r := reflect.ValueOf(person)
    f := reflect.Indirect(r).FieldByName(field)
    return int(f.Int())
}

func main() {
    me := &Person{Name: "HF", Age: 1}
    fmt.Printf("my name: %s\n", getFieldString(me, "Name")) // my name: HF
    fmt.Printf("my age: %d\n", getFieldInteger(me, "Age")) // my age: 1
}

标签

在 Go 语言中首字母大小写有特殊的语法含义,小写包外无法引用。由于需要和其它的系统进行数据交互,例如转换成 JSON。这时如果用属性名来作为键值可能不一定会符合项目要求。tag 在转换成其它数据格式的时候,强制使用自定义的字段作为键值。

import (
    "encoding/json"
    "fmt"
)

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

func main() {
    me := &Person{Name: "HF", Age: 1}
    json, err := json.Marshal(me)
    if err != nil {
        panic(err)
    }
    fmt.Printf("JSON of me: %s\n", string(json)) // JSON of me: {"name":"HF","age":1}
}

如果不带上自定义 tag,编组后的 JSON 是 {"Name":"HF","Age":1},直接使用 Person 结构体的字段名作为 JSON 字段名。

tag 甚至支持更丰富的自定义项,比如 omitempty

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age,omitempty"`
}

func main() {
    me := &Person{Name: "HF"}
    json, err := json.Marshal(me)
    if err != nil {
        panic(err)
    }
    fmt.Printf("JSON of me: %s\n", string(json)) // JSON of me: {"name":"HF"}
}

如果结构体的某个字段为零值或者空值,JSON 编组时将跳过这个字段。