前言

结构体是将多个任意类型的命名变量组合在一起的聚合数据类型,通过结构体,可以多维度/方面的聚合逻辑数据,形成一个整体,其中的这些命名变量叫做结构体的成员。

Struct

声明:
// 声明一个结构体类型
type Employee struct {
	ID        int
	Name      string
	Address   string
	DoB       string
	Position  string
	Salary    int
	ManagerID int
}

结构体成员(变量名称)推荐使用大写字母开头,目的是为了方便与json进行转换,具体说明可以看后面的部分。

简单使用
	// 声明结构体类型的实例,三种方式,前两种声明的都是指针,后面一种是直接生产对象本身,初始属性值都是零值:
	var bob *Employee = new(Employee)  // &{0     0 0}
	var claier *Employee = &Employee{} // &{0     0 0}
	var alice Employee                 // {0     0 0}
	fmt.Println(bob, claier, alice)

	// 访问和赋值
	alice.Salary = 3000
	fmt.Println(alice, alice.Salary) // {0     3000 0} 3000

	// 通过指针访问和赋值
	position := &bob.Position
	*position = "Senior"
	fmt.Println(bob) // &{0    Senior 0 0}

	c := &claier
	(*c).Name = "claier"
	fmt.Println(*c) // &{0 alice   Senior 3000 0}
构造和比较
	/*
		构造函数:
			struct结构体默认没有构造函数,需要自己写一个
	*/
	
    func NewEmployee(id int, name string) *Employee {
    	return &Employee{ID: id, Name: name}
    }

	david := NewEmployee(4, "david")
	fmt.Println(*david) // {4 david    0 0}

	/*
		struct之间的比较
	*/
	// struct是可以直接进行比较的,编译不会报错。此前学习过,map的key必须是可以直接比较的类型,因此struct可以作为map的key
	fmt.Println(alice == *bob) // false

	type url struct {
		host string
		port int
	}
	pageViews := make(map[url]int)
	pageViews[url{"www.baidu.com", 443}]++
结构体嵌套
	/*
										struct嵌套
		struct支持多层嵌套,且可以将一个命名结构体当做另一个结构体的匿名成员使用,下面举一个例子:
		层级关系为: 学生 -> 班级 -> 年级 -> 学校
	*/
	// 基本用法

	type School struct {
		name string
	}

	type Grade struct {
		level  int
		school School
	}

	type Classes struct {
		grade Grade
		id    int
	}
	type Student struct {
		name    string
		age     int
		classes Classes
	}

	var xiaoming Student
	xiaoming.name = "xiaoming"
	xiaoming.classes.grade.school.name = "NO.1 School"
	xiaoming.classes.grade.level = 3
	xiaoming.classes.id = 2
	xiaoming.age = 8
	fmt.Println(xiaoming) // {xiaoming 8 {{3 {NO.1 School}} 2}}
匿名结构体嵌套
	/*
	匿名结构体嵌套:
		匿名用法,结构体内的不带名称的结构体成员,该成员必须是其他类型的对象或指针。使用匿名成员时,可以以穿透的方式给属性赋值,即当前结构体不包含该 \n
		属性名时,则往向上层级的结构体上查找该属性名,最近的匹配属性将获得赋值(最近原则)。例如下方,xiaoli.name的值赋给了Student2.name, 而 \n
		xiaoli.classes.grade.school.name,则可以通过xiaoli.School.name来赋值。
	虽然是比直接用法简化了一些,但感觉层次还是不够清晰,谨慎使用。
	*/

	type Grade2 struct {
		level int
		School
	}

	type Classes2 struct {
		Grade2
		id int
	}
	type Student2 struct {
		name string
		age  int
		Classes2
	}

	var xiaoli Student2
	xiaoli.name = "xiaoli"
	xiaoli.School.name = "NO.2 School" // 等价于上面的 xiaoli.classes.grade.school.name
	xiaoli.level = 4                   // 等价于上面的 xiaoli.classes.grade.level
	xiaoli.id = 3                      // 等价于上面的 xiaoli.classes.id
	xiaoli.age = 9
	fmt.Println(xiaoli) // {xiaoli 9 {{4 {NO.2 School}} 3}}

	// 匿名结构体赋值
	xiaohong := Student2{"xiaohong", 11, Classes2{Grade2{5, School{"NO.2 School"}}, 4}}

	// 等价于上方
	xiaotian := Student2{
		"xiaotian",
		12,
		Classes2{
			Grade2{
				6,
				School{
					"NO.2 School",
				},
			},
			5,
		},
	}

	fmt.Println(xiaohong) // {xiaohong 11 {{5 {NO.2 School}} 4}}
	fmt.Println(xiaotian) // {xiaotian 12 {{6 {NO.2 School}} 5}}

JSON

Json是非常常见的数据接收和发送的格式化标准,其逻辑结构与struct非常相似,go提供标准库encoding/json来支持对json数据的处理和转换操作。
下面举例说明struct和json之间的转换:

struct => json序列化
	type Movie struct {
		Name  string
		Year  int
		actor []string
	}

	var movies = []Movie{
		{Name: "movie1", Year: 1999, actor: []string{"zhangsan", "lisi"}},
		{Name: "movie2", Year: 2000, actor: []string{"wangwu", "zhaoliu"}},
	}

	data, err := json.Marshal(movies)
	if err != nil {
		fmt.Println("Json parse error")
		return
	}
	fmt.Println(movies)      // [{movie1 1999 [zhangsan lisi]} {movie2 2000 [wangwu zhaoliu]}]
	fmt.Printf("%s\n", data) // [{"Name":"movie1","Year":1999},{"Name":"movie2","Year":2000}]
	//这里的打印结果可以看出一个问题: actor属性并没有体现出来,这是因为JSON在处理struct转换的时候,只会处理属性名为大写的属性,小写的直接跳过不处理。

上面这个例子中,json序列化时会忽略小写开头的结构体成员,如果想要在json处理时显示成员为小写或别名,可以显示地为其添加标签:

	type Movie2 struct {
		Name  string
		Year  int
		Actor []string `json:"actor"`
	}
	var movies2 = []Movie2{
		{Name: "movie1", Year: 1999, Actor: []string{"zhangsan", "lisi"}},
		{Name: "movie2", Year: 2000, Actor: []string{"wangwu", "zhaoliu"}},
	}
	data2, _ := json.Marshal(movies2)
	fmt.Printf("%s\n", data2) // [{"Name":"movie1","Year":1999,"actor":["zhangsan","lisi"]},{"Name":"movie2","Year":2000,"actor":["wangwu","zhaoliu"]}]

json:"actor"
json => struct反序列化
    // 沿用上面例子的变量和结构体
	//var movies3 Movie2
	// 这里犯了一个错误,导致运行报错.记一笔:
	// 将变量定义为Movie2结构体类型之后去接收这里的data2的json反序列化数据时,报错:json: cannot unmarshal array into Go value of type main.Movie2
	// 原因为: 当json串处理之后的结果是数组类型的时候,不能直接赋值给struct类型的值,因此,这里定义的movies3变量应该是[]Movie2的slice类型。go语言之中类型必须严格匹配,注意!
	// 另外,除了这里的unmarshal方式之外,还有一种json.Decoder的方式可以依次从字节流里逐个解码多个json对象,这个在后面的例子json_template.go中做解释。

	var movies3 []Movie2
    
	err2 := json.Unmarshal(data2, &movies3)
	if err2 != nil {
		fmt.Println("Json parse error", err2)
	}
	fmt.Printf("%v\n", movies3) // [{movie1 1999 [zhangsan lisi]} {movie2 2000 [wangwu zhaoliu]}]
	fmt.Println(movies3[0].Year)      // 1999
[]Movie2[]Movie2

这里使用的Unmarshal方法适用的对象为已经在单个或已内存中的json对象,而涉及文件(socket/http等)读取的byte流json数据,则适合使用另一种反序列化方式json.Decoder(),这种方式不会一次把字节流读取进内存中,而是会逐个读取,适用于这些场景。

使用举例
package main

/*
通过Github issue api获取到json数据并解析为go struct的demo
*/

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

const IssueURL = "https://api.github.com/search/issues"

type IssuesSearchResult struct {
	TotalCount int `json:"total_count"`
	Items      []*Issue
}

type Issue struct {
	Number   int
	HTMLURL  string `json:"html_url"`
	Title    string
	State    string
	User     *User
	CreateAt time.Time `json:"created_at"`
	Body     string
}

type User struct {
	Login   string
	HTMLURL string `json:"html_url"`
}

func SearchIssues(terms []string) (*IssuesSearchResult, error) {
	q := url.QueryEscape(strings.Join(terms, " ")) // slice组合为字符串
	resp, err := http.Get(IssueURL + "?q=" + q)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, err
	}
	var result IssuesSearchResult
	// 使用json.NewDecoder().decode()方法来解码整个字节流,这里不使用unmarshal方法了,unmarshal方法只能解码单个对象。
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		resp.Body.Close()
		return nil, err
	}
	resp.Body.Close()
	return &result, nil
}

func main() {
	res, err := SearchIssues(os.Args[1:]) // 运行命令为: go run ./json_template.go repo:golang/go is:open json decoder  后方为参数,可以组合为url去浏览器打开对比
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Items[0].Title)
	fmt.Printf("%d issues: \n", res.TotalCount)
	for _, item := range res.Items {
		fmt.Printf(
			"IssueID: #%d\tTime: %s\tUser: %s\tTitle: %s\n", item.Number, item.CreateAt, item.User.Login, item.Title)
	}

}

运行结果:

encoding/json: second decode after error impossible
35 issues: 
IssueID: #31701 Time: 2019-04-26 20:50:17 +0000 UTC     User: lr1980    Title: encoding/json: second decode after error impossible
IssueID: #31789 Time: 2019-05-01 18:30:06 +0000 UTC     User: mgritter  Title: encoding/json: provide a way to limit recursion depth
IssueID: #29688 Time: 2019-01-11 17:07:05 +0000 UTC     User: sheerun   Title: proposal: encoding/json: add InputOffset to json decoder
IssueID: #29686 Time: 2019-01-11 16:38:04 +0000 UTC     User: sheerun   Title: json: Add InputOffset for stream byte offset access
IssueID: #28923 Time: 2018-11-22 13:50:18 +0000 UTC     User: mvdan     Title: encoding/json: speed up the decoding scanner
IssueID: #11046 Time: 2015-06-03 19:25:08 +0000 UTC     User: kurin     Title: encoding/json: Decoder internally buffers full input
IssueID: #29035 Time: 2018-11-30 11:21:31 +0000 UTC     User: jaswdr    Title: proposal: encoding/json: add error var to compare  the returned error when using json.Decoder.DisallowUnknownFields()
IssueID: #30301 Time: 2019-02-18 19:49:27 +0000 UTC     User: zelch     Title: encoding/xml: option to treat unknown fields as an error
IssueID: #30701 Time: 2019-03-09 12:16:34 +0000 UTC     User: LouAdrien Title: encoding/json: ignore tag "-" not working on embedded sub structure when decoding
IssueID: #12001 Time: 2015-08-03 19:14:17 +0000 UTC     User: lukescott Title: encoding/json: Marshaler/Unmarshaler not stream friendly
IssueID: #16212 Time: 2016-06-29 16:07:36 +0000 UTC     User: josharian Title: encoding/json: do all reflect work before decoding
IssueID: #28143 Time: 2018-10-11 07:08:25 +0000 UTC     User: Carpetsmoker      Title: proposal: encoding/json: add "readonly" tag
IssueID: #26946 Time: 2018-08-12 18:19:01 +0000 UTC     User: deuill    Title: encoding/json: clarify what happens when unmarshaling into a non-empty interface{}
IssueID: #5901  Time: 2013-07-17 16:39:15 +0000 UTC     User: rsc       Title: encoding/json: allow override type marshaling
IssueID: #27179 Time: 2018-08-23 18:21:32 +0000 UTC     User: lavalamp  Title: encoding/json: no way to preserve the order of map keys
IssueID: #21823 Time: 2017-09-09 21:43:31 +0000 UTC     User: 243083df  Title: encoding/xml: very low performance in xml parser
IssueID: #22752 Time: 2017-11-15 23:46:13 +0000 UTC     User: buyology  Title: proposal: encoding/json: add access to the underlying data causing UnmarshalTypeError
IssueID: #14750 Time: 2016-03-10 13:04:44 +0000 UTC     User: cyberphone        Title: encoding/json: parser ignores the case of member names
IssueID: #20754 Time: 2017-06-22 14:19:31 +0000 UTC     User: rsc       Title: encoding/xml: unmarshal only processes first XML element
IssueID: #28189 Time: 2018-10-13 16:22:53 +0000 UTC     User: adnsv     Title: encoding/json: confusing errors when unmarshaling custom types
IssueID: #7213  Time: 2014-01-27 00:23:01 +0000 UTC     User: davecheney        Title: cmd/compile: escape analysis oddity
IssueID: #7872  Time: 2014-04-26 17:47:25 +0000 UTC     User: extemporalgenome  Title: encoding/json: Encoder internally buffers full output
IssueID: #20528 Time: 2017-05-30 11:45:14 +0000 UTC     User: jvshahid  Title: net/http: connection reuse does not work happily with normal use of json package
IssueID: #17609 Time: 2016-10-26 16:07:27 +0000 UTC     User: nathanjsweet      Title: encoding/json: ambiguous fields are marshalled
IssueID: #19636 Time: 2017-03-21 12:25:10 +0000 UTC     User: josselin-c        Title: encoding/base64: decoding is slow
IssueID: #22816 Time: 2017-11-20 13:12:06 +0000 UTC     User: ganelon13 Title: encoding/json: include field name in unmarshal error messages when extracting time.Time
IssueID: #21092 Time: 2017-07-19 23:11:40 +0000 UTC     User: trotha01  Title: encoding/json: unmarshal into slice reuses element data between len and cap
IssueID: #28941 Time: 2018-11-25 14:06:38 +0000 UTC     User: mvdan     Title: cmd/compile: teach prove about slice expressions
IssueID: #15808 Time: 2016-05-24 02:13:10 +0000 UTC     User: randall77 Title: cmd/compile: loads/constants not lifted out of loop
IssueID: #28952 Time: 2018-11-26 09:59:26 +0000 UTC     User: mvdan     Title: cmd/compile: consider teaching prove about unexported integer fields

总结

Golang结构体有许多特性,且与json结合紧密,日后会经常用到,需要熟练掌握。