Go基础|第7章:struct

  • 原创
  • Madman
  • /
  • /
  • 0
  • 2865 次阅读

Golang.jpg

Synopsis: A struct type consists a collection of member variable declarations. 结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成、每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。struct 类似于面向对象的编程语言中的一个没有方法的轻量级 class,不过因为 Go 语言中没有类的概念,所以在 Go 语言中结构体有着更为重要的地位

1. 结构体类型

struct 是一种复合数据类型,由零个或多个任意类型的 字段(field) 聚合而成。结构体的每个字段必须是一个已知类型,可以是基本类型,也可以是 用户定义的类型(defined type)(其它结构体或命名类型)

用户定义的类型:

  1. struct
  2. 使用 type NewTypeName SourceType 这种形式基于一个已有的类型定义新类型。比如 type Duration int64,从基本类型创建出功能更加明确的类型 Duration 来描述单位为纳秒的时间间隔。在 Duration 类型的声明中,我们把 int64 类型叫作 Duration 的基础类型。不过,虽然 int64 是基础类型,但是 Go 并不认为 Duration 和 int64 是同一种类型。两种不同类型的值即便互相兼容,也不能互相赋值,Go 编译器不会对不同类型的值做隐式转换

通常结构体中的每一行对应一个字段(字段名在前、类型在后),但是如果相邻的字段的类型相同的话,可以被合并到一行。如果结构体字段名字是以大写字母开头的,那么该字段就是导出的,一个结构体可以同时包含导出和未导出的字段

// 声明的语法为: type 结构体名称 struct {...}
type Employee struct {
  ID int
  Name, Address string  // 类型相同的字段可以合并到一行
  Position string
  Salary int  // 此字段已导出
  age int     // 此字段未导出
}

没有任何字段的结构体叫空结构体 struct{}

注意: struct{} 类型值的表示法只有 struct{}{}。并且,它占用的内存空间是 0 字节

确切地说,这个值在整个 Go 程序中永远都只会存在一份。虽然我们可以无数次地使用这个值字面量,但是用到的却都是同一个值。当我们仅仅把通道当作传递某种简单信号的介质的时候,用 struct{} 作为其元素类型是再好不过的了,比如:

sign := make(chan struct{}) // 创建 channel
sign <- struct{}{} // 发送信号给 channel

结构体类型的零值是每个字段的零值。可以使用点号 . 来访问结构体的字段:

package main

import "fmt"

type Vertex struct {
  X int
  Y int
}

func main() {
  var v Vertex // 声明结构体,并初始化为零值
  fmt.Printf("%#v \n", v)

  v.X = 1
  fmt.Printf("%#v \n", v)
}

/* Output:
main.Vertex{X:0, Y:0}
main.Vertex{X:1, Y:0}
*/

对于每一个变量必然有对应的内存地址(变量有时候也被称为可寻址的值),而结构体 S 的每个字段都对应一个变量,因此 结构体可以被取地址(创建指向结构体的指针)

一个命名为 S 的结构体类型将不能再包含 S 类型的字段:因为一个聚合的值不能包含它自身(该限制同样适应于 array)。但是 S 类型的结构体可以包含 *S 指针类型的字段,这可以让我们创建递归的数据结构,比如链表和树结构等:

type tree struct {
  value int
  left, right *tree
}

2. 创建与初始化

2.1 结构体字面量

结构字面量有两种形式,第一种形式提供每个字段的名字以及对应的值,字段名与值用冒号分隔,被忽略的字段默认使用零值。因为提供了字段的名称,所以字段出现的顺序并不重要:

package main

import "fmt"

type Persion struct {
  Name string
  age  int
}

func main() {
  v1 := Persion{
    Name: "Alice",
    age:  18, // 如果没有逗号时,syntax error: unexpected newline, expecting comma or }
  }
  fmt.Printf("%#v \n", v1)

  v2 := Persion{Name: "Bob", age: 20}
  fmt.Printf("%#v \n", v2)

  v3 := Persion{Name: "Baby"} // age: 0 被隐式地赋予
  fmt.Printf("%#v \n", v3)

  v4 := Persion{} // Name: "", age: 0
  fmt.Printf("%#v \n", v4)

  p := &Persion{Name: "Tom", age: 24} // 创建一个 Persion 类型结构体,并返回结构体的地址
  fmt.Printf("%#v \n", p)
}

/* Output:
main.Persion{Name:"Alice", age:18}
main.Persion{Name:"Bob", age:20}
main.Persion{Name:"Baby", age:0}
main.Persion{Name:"", age:0}
&main.Persion{Name:"Tom", age:24}
*/

第二种形式没有字段名,只声明对应的值,值的顺序很重要,必须要和结构声明中字段的顺序一致:

package main

import "fmt"

type Persion struct {
  Name string
  age  int
}

func main() {
  v1 := Persion{"Alice", 18}
  fmt.Printf("%#v \n", v1)

  // cannot use 20 (type untyped int) as type string in field value
  // cannot use "Bob" (type untyped string) as type int in field value
  v2 := Persion{20, "Bob"}
  fmt.Printf("%#v \n", v2)

  p := &Persion{"Tom", 24} // 创建一个 Persion 类型结构体,并返回结构体的地址
  fmt.Printf("%#v \n", p)
}

2.1 new() 函数创建结构体并返回指针

通过内建函数 new() 来创建并初始化结构体(零值)

package main

import "fmt"

type Person struct {
  Name string
  age  int
}

func main() {
  var p *Person // 声明 *Person 类型的指针,初始化为零值 nil
  fmt.Printf("%#v \n", p)

  p = new(Person)          // new() 创建新的匿名结构体并返回其内存地址,再赋值给指针 p
  fmt.Printf("%#v \n", *p) // *p 表示通过指针 p 读取刚创建的匿名结构体

  p.Name = "Alice" // 其实是 (*p).Name = "Alice"
  fmt.Printf("%#v \n", *p)
}

/* Output:
(*main.Person)(nil)
main.Person{Name:"", age:0}
main.Person{Name:"Alice", age:0}
*/

结构体字段也可以通过结构体指针来访问,如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。Go 语言也允许我们使用隐式间接引用,直接写 p.X 就可以了

结构体可以作为函数的参数和返回值,如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回

func Bonus(e *Employee, percent int) int {
  return e.Salary * percent / 100
}

如果要在函数内部修改结构体 T 的字段值的话,必须传入 *T 类型的结构体指针

func AwardAnnualRaise(e *Employee) {
  e.Salary = e.Salary * 105 / 100
}

3. 结构体比较

如果结构体的全部字段都是可以比较的,那么可以用 ==!= 来比较两个结构体(比较两个结构体的每个字段)

type Point struct{ X, Y int }

p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y)  // false
fmt.Println(p == q)                    // false

可比较的结构体类型和其他可比较的类型一样,可以用于 map 的 key:

type address struct {
  hostname string
  port int
}

hits := make(map[address]int)
hits[address{"golang.org", 443}]++

4. 类型嵌入

上面说过结构体字段可以是基本类型或用户定义的类型:

type Point struct {
    X, Y int
}

type Circle struct {
    Center Point  // 组合(composition)
    Radius int
}

type Wheel struct {
    Circle Circle  // 组合(composition)
    Spokes int
}

组合(composition) 会导致 Wheel 访问字段变得繁琐:

var w Wheel
w.Circle.Center.X = 0
w.Circle.Center.Y = 1
w.Circle.Radius = 3
w.Spokes = 5

Go 语言还允许用户使用 类型嵌入(type embedding) 功能来扩展或者修改已有类型的行为,它可以修改已有类型以符合新类型,实现代码复用!

类型嵌入是将已有的类型直接声明在新的结构类型里,被嵌入的类型被称为新的外部类型的内部类型或 匿名字段(Anonymous Fields),我们只需声明一个字段对应的数据类型而无需定义字段名

注意:匿名字段的数据类型必须是命名的类型或指向一个命名的类型的指针

package main

import "fmt"

type Point struct {
  X, Y int
}

type Circle struct {
  Point // 类型嵌入,匿名字段
  Radius int
}

type Wheel struct {
  Circle // 类型嵌入,匿名字段
  Spokes int
}

func main() {
  var w Wheel
  w.X = 0      // 等价于 w.Circle.Point.X = 0,此语法依然有效
  w.Y = 1      // 等价于 w.Circle.Point.Y = 1,此语法依然有效
  w.Radius = 3 // 等价于 w.Circle.Radius = 3,此语法依然有效
  w.Spokes = 5

  fmt.Printf("%#v", w)
}

/* Output:
main.Wheel{Circle:main.Circle{Point:main.Point{X:0, Y:1}, Radius:3}, Spokes:5}
*/

注意:因为匿名字段也有一个隐式的名字(就是命名的类型名字),因此不能同时包含两个类型相同的匿名字段,这会导致名字冲突

通过类型嵌入,与内部类型(Point / Circle)相关的标识符会提升到外部类型(Wheel)上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,也是外部类型的一部分。这样外部类型就 组合 了内部类型包含的所有属性和 方法集,并且可以添加新的字段和方法。外部类型也可以通过声明与内部类型标识符同名的标识符来 覆盖 内部标识符的字段或者方法

唯一不方便的地方就是使用结构体字面量进行初始化时:

// 编译错误:
// cannot use 0 (type int) as type Circle in field value
// too many values in Wheel literal
w := Wheel{0, 1, 3, 5}

// 编译错误:
// cannot use promoted field Circle.Point.X in struct literal of type Wheel
// cannot use promoted field Circle.Point.Y in struct literal of type Wheel
// cannot use promoted field Circle.Radius in struct literal of type Wheel
w := Wheel{X: 0, Y: 1, Radius: 3, Spokes: 5}

// 正确,字段名: 字段值
w := Wheel{
  Circle: Circle{
    Point: Point{X: 0, Y: 1},
    Radius: 3,
  },
  Spokes: 5,
}

// 正确,按顺序的字段值
w := Wheel{Circle{Point{8, 8}, 5}, 20}

通过嵌入结构体来扩展类型,可以用于将一些只有简单行为的对象 组合 成具有复杂行为的对象,组合是 Go 语言中面向对象编程的核心

5. 结构体与 JSON 格式互相转换

5.1 编码 marshal

package main

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

type Person struct {
  Name string
  age  int    // 只有导出的结构体字段才会被编码
  Sex  string `json:"gender"`         // 转化为 JSON 时,字段名为 gender
  Dead bool   `json:"dead,omitempty"` // 转化为 JSON 时,字段名为 dead。选项 omitempty 表示当 Go 语言结构体字段为空或零值时(bool 类型零值为 false),JSON 中不包含此字段
}

func main() {
  p1 := Person{"Alice", 18, "woman", false}
  data1, err := json.Marshal(p1)
  if err != nil {
    log.Fatalf("Failed to marshal struct to JSON: %s", err)
  }
  fmt.Printf("%T, %s\n", data1, data1)

  p2 := Person{"Albert Einstein", 76, "man", true}
  data2, err := json.Marshal(p2)
  if err != nil {
    log.Fatalf("Failed to marshal struct to JSON: %s", err)
  }
  fmt.Printf("%T, %s\n", data2, data2)
}

/* Output:
[]uint8, {"Name":"Alice","gender":"woman"}  // age 字段未导出;Sex 字段改名为 gender;Dead 字段值由于是零值 false,所以未导出
[]uint8, {"Name":"Albert Einstein","gender":"man","dead":true}
*/

上例中 json:"gender"json:"dead,omitempty" 是结构体字段 tags,通常是一系列用空格分隔的 key:"value" 键值对序列,因为使用了双引号包裹 value,所以 tags 一般用原生字符串字面量 `` 形式书写

json.MarshalIndent() 函数可以比 json.Marshal() 函数编码生成格式更好看的 JSON(包含缩进):

data1, err := json.MarshalIndent(p1, "", "  ")

生成的 JSON 如下:

{
  "Name": "Alice",
  "gender": "woman"
}

5.2 解码 unmarshal

编码的逆操作是解码,可以使用 json.Unmarshal() 函数将 JSON 数据解码为 Go 语言的数据结构

var p3 Person
if err := json.Unmarshal(data2, &p3); err != nil { // 必须传入指针,否则修改的是 p3 的副本,那么下一行代码打印 p3 时是 Person 类型的零值: main.Person{Name:"", age:0, Sex:"", Dead:false}
  log.Fatalf("Failed to unmarshal JSON to struct: %s", err)
}
fmt.Printf("%T, %#v\n", p3, p3) // main.Person, main.Person{Name:"Albert Einstein", age:0, Sex:"man", Dead:true}

通过定义合适的 Go 语言数据结构,我们可以选择性地解码 JSON 中感兴趣的内容:

type Person2 struct {
  Name string // 必须是大写的可导出字段,否则无法解码、默认是字段类型零值
}

var p4 Person2
if err := json.Unmarshal(data2, &p4); err != nil {
  log.Fatalf("Failed to unmarshal JSON to struct: %s", err)
}
fmt.Printf("%T, %#v\n", p4, p4) // main.Person2, main.Person2{Name:"Albert Einstein"}

json.Unmarshal 函数将 JSON 格式的字符串解码为字节切片 []byte,日常使用中访问 HTTP 接口获取到的 JSON 响应是 字节流,我们要使用 json 包的 NewDecoder 函数以及 Decode 方法来解码输入流。如果要处理来自网络响应或者文件的 JSON,那么一定会用到这个函数及方法

package main

import (
  "encoding/json"
  "fmt"
  "log"
  "net/http"
)

type Response struct {
  Origin string
  URL    string
}

func main() {
  // 请求 API 接口
  resp, err := http.Get("http://httpbin.org/get")
  if err != nil {
    log.Fatalf("Failed to call API: %s", err)
  }
  defer resp.Body.Close()

  // 将 JSON 响应解码到结构体类型
  var payload Response
  err = json.NewDecoder(resp.Body).Decode(&payload) // 基于流式的解码器 json.Decoder,它可以从一个输入流解码 JSON 数据
  if err != nil {
    log.Fatalf("Failed to unmarshal JSON to struct: %s", err)
  }
  fmt.Printf("%T, %#v\n", payload, payload)
}

/* Output:
main.Response, main.Response{Origin:"1.2.3.4", URL:"http://httpbin.org/get"}
*/
分类: Go Basic
标签: struct new()
未经允许不得转载: LIFE & SHARE - 王颜公子 » Go基础|第7章:struct

分享

作者

作者头像

Madman

如需 Linux / Python 相关问题付费解答,请按如下方式联系我

0 条评论

暂时还没有评论.