Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合
定义结构体
结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:
type struct_variable_type struct {
member definition
member definition
...
member definition
}
一旦定义了结构体类型,它就能用于变量的声明,语法格式如下
variable_name := structure_variable_type {value1, value2...valuen}
或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
结构体表示一项记录,比如保存图书馆的书籍记录,每本书有以下属性:
- ID 书籍ID
- Title 标题
- Author 作者
- Subject 学科 ```go package main import “fmt”
type Books struct { title string author string subject string book_id int }
func main() { // 创建一个新的结构体 fmt.Println(Books{“Go 语言”, “www.runoob.com”, “Go 语言教程”, 6495407})
// 也可以使用 key => value 格式 fmt.Println(Books{title: “Go 语言”, author: “www.runoob.com”, subject: “Go 语言教程”, book_id: 6495407})
// 忽略的字段为 0 或 空 fmt.Println(Books{title: “Go 语言”, author: “www.runoob.com”}) }
/ 输出结果为: {Go 语言 www.runoob.com Go 语言教程 6495407} {Go 语言 www.runoob.com Go 语言教程 6495407} {Go 语言 www.runoob.com 0} /
<a name="NhOMl"></a>
### 访问结构体成员
如果要访问结构体成员,需要使用点号 **.** 操作符,格式为
```go
结构体.成员名"
结构体类型变量使用 struct 关键字定义,实例如下:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books // 声明 Book1 为 Books 类型
var Book2 Books // 声明 Book2 为 Books 类型
// book 1 描述
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
// book 2 描述
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
// 打印 Book1 信息
fmt.Printf( "Book 1 title : %s\n", Book1.title)
fmt.Printf( "Book 1 author : %s\n", Book1.author)
fmt.Printf( "Book 1 subject : %s\n", Book1.subject)
fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id)
// 打印 Book2 信息
fmt.Printf( "Book 2 title : %s\n", Book2.title)
fmt.Printf( "Book 2 author : %s\n", Book2.author)
fmt.Printf( "Book 2 subject : %s\n", Book2.subject)
fmt.Printf( "Book 2 book_id : %d\n", Book2.book_id)
}
/*
以上实例执行运行结果为:
Book 1 title : Go 语言
Book 1 author : www.runoob.com
Book 1 subject : Go 语言教程
Book 1 book_id : 6495407
Book 2 title : Python 教程
Book 2 author : www.runoob.com
Book 2 subject : Python 语言教程
Book 2 book_id : 6495700
*/
结构体作为函数参数
你可以像其他数据类型一样将结构体类型作为参数传递给函数。并以以上实例的方式访问结构体变量:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books // 声明 Book1 为 Books 类型
var Book2 Books // 声明 Book2 为 Books 类型
// book 1 描述
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
// book 2 描述
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
printBook(Book1) // 打印 Book1 信息
printBook(Book2) // 打印 Book2 信息
}
func printBook( book Books ) {
fmt.Printf( "Book title : %s\n", book.title)
fmt.Printf( "Book author : %s\n", book.author)
fmt.Printf( "Book subject : %s\n", book.subject)
fmt.Printf( "Book book_id : %d\n", book.book_id)
}
/*
Book title : Go 语言
Book author : www.runoob.com
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : www.runoob.com
Book subject : Python 语言教程
Book book_id : 6495700
*/
结构体指针
你可以定义指向结构体的指针类似于其他指针变量,格式如下:
var struct_pointer *Books
以上定义的指针变量可以存储结构体变量的地址。查看结构体变量地址,可以将 & 符号放置于结构体变量前:
struct_pointer = &Book1
使用结构体指针访问结构体成员,使用 “.” 操作符:
struct_pointer.title
接下来让我们使用结构体指针重写以上实例,代码如下:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books // Declare Book1 of type Book
var Book2 Books // Declare Book2 of type Book
// book 1 描述
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
// book 2 描述
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
printBook(&Book1) // 打印 Book1 信息
printBook(&Book2) // 打印 Book2 信息
}
func printBook( book *Books ) {
fmt.Printf( "Book title : %s\n", book.title)
fmt.Printf( "Book author : %s\n", book.author)
fmt.Printf( "Book subject : %s\n", book.subject)
fmt.Printf( "Book book_id : %d\n", book.book_id)
}
/*
Book title : Go 语言
Book author : www.runoob.com
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : www.runoob.com
Book subject : Python 语言教程
Book book_id : 6495700
*/
结构体是作为参数的值传递
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func changeBook(book Books,title string) {
book.title = title
}
func main() {
var book1 Books
book1.title = "book1"
book1.author = "zuozhe"
book1.book_id = 1
changeBook(book1,"title被修改了")
fmt.Println(book1)
}
/*
期盼结果:
{title被修改了 zuozhe 1}
实际结果为:
{book1 zuozhe 1}
*/
如果想在函数里面改变结果体数据内容,需要传入指针:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func changeBook(book *Books,title string) {
book.title = title
}
func main() {
var book1 Books
book1.title = "book1"
book1.author = "zuozhe"
book1.book_id = 1
changeBook(&book1,"title被修改了")
fmt.Println(book1)
}
/*
运行结果:
{title被修改了 zuozhe 1}
*/
struct 中定义成员变量
struct 类似于 java 中的类,但是不需要通过 getter, setter 来设置访问权限。要访问成员变量,可以:
- 通过 struct变量.成员变量来访问、重新赋值
- 通过 struct指针.成员变量来访问、重新赋值 ```go type Rect struct{ //定义矩形类 x,y float64 //类型只包含属性,并没有方法 width,height float64 }
func (r Rect) getArea() float64{ //为Rect类型绑定Area的方法,Rect为指针引用可以修改传入参数的值 return r.width*r.height //方法归属于类型,不归属于具体的对象,声明该类型的对象即可调用该类型的方法 }
//_ package main import ( “fmt” )
type Rect struct{
x,y float64
width,height int
}
func (r Rect) getArea() int{ return r.widthr.height }
func main() { var rectExample Rect rectExample.x = 10 rectExample.width = 20 rectExample.height = 30 rectExample.x = 40 fmt.Println(rectExample.getArea()) }
<a name="HjgZM"></a>
### Golang自定义结构体转map
在Golang中,如何将一个结构体转成map? 本文介绍两种方法。第一种是是使用json包解析解码编码。第二种是使用反射,使用反射的效率比较高
<a name="fCPr5"></a>
#### json包的marshal,unmarshal
```go
package main
import (
"fmt"
"time"
"encoding/json"
// "reflect"
// "strconv"
// "strings"
// "testing"
)
const timeLayout = "2006-01-02 15:04:05"
type User struct {
Name string `map:"name,omitempty"` // string
Github GithubPage `map:"github,dive,omitempty"` // struct dive
NoDive StructNoDive `map:"no_dive,omitempty"` // no dive struct
MyProfile Profile `map:"my_profile,omitempty"` // struct implements its own method
}
type GithubPage struct {
URL string `map:"url"`
Star int `map:"star"`
}
type StructNoDive struct {
NoDive int
}
type Profile struct {
Experience string `map:"experience"`
Date time.Time `map:"time"`
}
// its own toMap method
func (p Profile) StructToMap() (key string, value interface{}) {
return "time", p.Date.Format(timeLayout)
}
func newUser() User {
name := "user"
MyGithub := GithubPage{
URL: "https://github.com/liangyaopei/structmap",
Star: 1,
}
NoDive := StructNoDive{NoDive: 1}
dateStr := "2020-07-21 12:00:00"
date, _ := time.Parse(timeLayout, dateStr)
profile := Profile{
Experience: "my experience",
Date: date,
}
return User{
Name: name,
Github: MyGithub,
NoDive: NoDive,
MyProfile: profile,
}
}
func main() {
user := newUser()
data, _ := json.Marshal(&user)
m := make(map[string]interface{})
json.Unmarshal(data, &m)
fmt.Println(m)
}
先将结构体序列化成 [] byte 数组,再从 [] byte 数组序列化成结构体
data, _ := json.Marshal(&user)
m := make(map[string]interface{})
json.Unmarshal(data, &m)
- 使用简单 劣势
- 效率比较慢
- 不能支持一些定制的键,也不能支持一些定制的方法,例如将struct的域展开等
使用反射
本文实现了使用反射将结构体转成 map 的方法。通过标签(tag)和反射,将上文示例的 newUser() 返回的结果转化成下面的一个map。其中包含struct的域的展开,定制化struct的方法
1.标签识别
使用readTag方法读取域(field)的标签,如果没有标签,使用域的名字。然后读取tag中的选项。目前支持3个选项
- ‘-‘:忽略当前这个域
- omitempty : 当这个域的值为空,忽略这个域
- dive : 递归地遍历这个结构体,将所有字段作为键
如果选中了一个选项,就讲这个域对应的二进制位置为1
const (
OptIgnore = "-"
OptOmitempty = "omitempty"
OptDive = "dive"
)
const (
flagIgnore = 1 << iota
flagOmiEmpty
flagDive
)
func readTag(f reflect.StructField, tag string) (string, int) {
val, ok := f.Tag.Lookup(tag)
fieldTag := ""
flag := 0
// no tag, use field name
if !ok {
return f.Name, flag
}
opts := strings.Split(val, ",")
fieldTag = opts[0]
for i := 1; i < len(opts); i++ {
switch opts[i] {
case OptIgnore:
flag |= flagIgnore
case OptOmitempty:
flag |= flagOmiEmpty
case OptDive:
flag |= flagDive
}
}
return fieldTag, flag
}
2.结构体的域(field)的遍历
遍历结构体的每一个域(field),判断field的类型(kind)。如果是string,int等的基本类型,直接取值,并且把标签中的值作为key
for i := 0; i < t.NumField(); i++ {
...
switch fieldValue.Kind() {
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int, reflect.Int64:
res[tagVal] = fieldValue.Int()
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint, reflect.Uint64:
res[tagVal] = fieldValue.Uint()
case reflect.Float32, reflect.Float64:
res[tagVal] = fieldValue.Float()
case reflect.String:
res[tagVal] = fieldValue.String()
case reflect.Bool:
res[tagVal] = fieldValue.Bool()
default:
}
}
}
3.内嵌结构体的转换
如果是结构体,先检查有没有实现传入参数的方法,如果实现了,就调用这个方法。如果没有实现,就递归地调用StructToMap方法,然后根据是否展开(dive),来把返回结果写入res的map
for i := 0; i < t.NumField(); i++ {
fieldType := t.Field(i)
// ignore unexported field
if fieldType.PkgPath != "" {
continue
}
// read tag
tagVal, flag := readTag(fieldType, tag)
if flag&flagIgnore != 0 {
continue
}
fieldValue := v.Field(i)
if flag&flagOmiEmpty != 0 && fieldValue.IsZero() {
continue
}
// ignore nil pointer in field
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
continue
}
if fieldValue.Kind() == reflect.Ptr {
fieldValue = fieldValue.Elem()
}
// get kind
switch fieldValue.Kind() {
case reflect.Struct:
_, ok := fieldValue.Type().MethodByName(methodName)
if ok {
key, value, err := callFunc(fieldValue, methodName)
if err != nil {
return nil, err
}
res[key] = value
continue
}
// recursive
deepRes, deepErr := StructToMap(fieldValue.Interface(), tag, methodName)
if deepErr != nil {
return nil, deepErr
}
if flag&flagDive != 0 {
for k, v := range deepRes {
res[k] = v
}
} else {
res[tagVal] = deepRes
}
default:
}
}
...
}
// call function
func callFunc(fv reflect.Value, methodName string) (string, interface{}, error) {
methodRes := fv.MethodByName(methodName).Call([]reflect.Value{})
if len(methodRes) != methodResNum {
return "", nil, fmt.Errorf("wrong method %s, should have 2 output: (string,interface{})", methodName)
}
if methodRes[0].Kind() != reflect.String {
return "", nil, fmt.Errorf("wrong method %s, first output should be string", methodName)
}
key := methodRes[0].String()
return key, methodRes[1], nil
}
4.array,slice类型的转换
如果是array,slice类型,类似地,检查有没有实现传入参数的方法,如果实现了,就调用这个方法。如果没有实现,将这个field的tag作为key,域的值作为value
switch fieldValue.Kind() {
case reflect.Slice, reflect.Array:
_, ok := fieldValue.Type().MethodByName(methodName)
if ok {
key, value, err := callFunc(fieldValue, methodName)
if err != nil {
return nil, err
}
res[key] = value
continue
}
res[tagVal] = fieldValue
....
}
5.其他类型
对于其他类型,例如内嵌的map,直接将其返回结果的值
switch fieldValue.Kind() {
...
case reflect.Map:
res[tagVal] = fieldValue
case reflect.Chan:
res[tagVal] = fieldValue
case reflect.Interface:
res[tagVal] = fieldValue.Interface()
default:
}
完整案例
创建 structmap.go 文件,写入代码
package structmap
import (
"fmt"
"reflect"
"strings"
)
const (
methodResNum = 2
)
const (
OptIgnore = "-"
OptOmitempty = "omitempty"
OptDive = "dive"
)
const (
flagIgnore = 1 << iota
flagOmiEmpty
flagDive
)
// StructToMap convert a golang sturct to a map
// key can be specified by tag, LIKE `map:"tag"`.
// If there is no tag, struct filed name will be used instead
// methodName is the name the field has implemented.
// If implemented, it uses the method to get the key and value
func StructToMap(s interface{}, tag string, methodName string) (res map[string]interface{}, err error) {
v := reflect.ValueOf(s)
if v.Kind() == reflect.Ptr && v.IsNil() {
return nil, fmt.Errorf("%s is a nil pointer", v.Kind().String())
}
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
// only accept struct param
if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("s is not a struct but %s", v.Kind().String())
}
t := v.Type()
res = make(map[string]interface{})
for i := 0; i < t.NumField(); i++ {
fieldType := t.Field(i)
// ignore unexported field
if fieldType.PkgPath != "" {
continue
}
// read tag
tagVal, flag := readTag(fieldType, tag)
if flag&flagIgnore != 0 {
continue
}
fieldValue := v.Field(i)
if flag&flagOmiEmpty != 0 && fieldValue.IsZero() {
continue
}
// ignore nil pointer in field
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
continue
}
if fieldValue.Kind() == reflect.Ptr {
fieldValue = fieldValue.Elem()
}
// get kind
switch fieldValue.Kind() {
case reflect.Slice, reflect.Array:
_, ok := fieldValue.Type().MethodByName(methodName)
if ok {
key, value, err := callFunc(fieldValue, methodName)
if err != nil {
return nil, err
}
res[key] = value
continue
}
res[tagVal] = fieldValue
case reflect.Struct:
_, ok := fieldValue.Type().MethodByName(methodName)
if ok {
key, value, err := callFunc(fieldValue, methodName)
if err != nil {
return nil, err
}
res[key] = value
continue
}
// recursive
deepRes, deepErr := StructToMap(fieldValue.Interface(), tag, methodName)
if deepErr != nil {
return nil, deepErr
}
if flag&flagDive != 0 {
for k, v := range deepRes {
res[k] = v
}
} else {
res[tagVal] = deepRes
}
case reflect.Map:
res[tagVal] = fieldValue
case reflect.Chan:
res[tagVal] = fieldValue
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int, reflect.Int64:
res[tagVal] = fieldValue.Int()
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint, reflect.Uint64:
res[tagVal] = fieldValue.Uint()
case reflect.Float32, reflect.Float64:
res[tagVal] = fieldValue.Float()
case reflect.String:
res[tagVal] = fieldValue.String()
case reflect.Bool:
res[tagVal] = fieldValue.Bool()
case reflect.Complex64, reflect.Complex128:
res[tagVal] = fieldValue.Complex()
case reflect.Interface:
res[tagVal] = fieldValue.Interface()
default:
}
}
return
}
// readTag read tag with format `json:"name,omitempty"` or `json:"-"`
// For now, only supports above format
func readTag(f reflect.StructField, tag string) (string, int) {
val, ok := f.Tag.Lookup(tag)
fieldTag := ""
flag := 0
// no tag, use field name
if !ok {
return f.Name, flag
}
opts := strings.Split(val, ",")
fieldTag = opts[0]
for i := 1; i < len(opts); i++ {
switch opts[i] {
case OptIgnore:
flag |= flagIgnore
case OptOmitempty:
flag |= flagOmiEmpty
case OptDive:
flag |= flagDive
}
}
return fieldTag, flag
}
// call function
func callFunc(fv reflect.Value, methodName string) (string, interface{}, error) {
methodRes := fv.MethodByName(methodName).Call([]reflect.Value{})
if len(methodRes) != methodResNum {
return "", nil, fmt.Errorf("wrong method %s, should have 2 output: (string,interface{})", methodName)
}
if methodRes[0].Kind() != reflect.String {
return "", nil, fmt.Errorf("wrong method %s, first output should be string", methodName)
}
key := methodRes[0].String()
return key, methodRes[1], nil
}
创建 to_map_test.go 文件,写入代码
package struct_to_map_test
import (
"encoding/json"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/liangyaopei/structmap"
)
const timeLayout = "2006-01-02 15:04:05"
type Profile struct {
Experience string `map:"experience"`
Date time.Time `map:"time"`
}
// its own toMap method
func (p Profile) StructToMap() (key string, value interface{}) {
return "time", p.Date.Format(timeLayout)
}
type MySlice []int
// its own toMap method
func (a MySlice) StructToMap() (string, interface{}) {
key := "array"
b := strings.Builder{}
if len(a) == 0 {
return key, b.String()
}
for i := 0; i < len(a); i++ {
b.WriteString(strconv.Itoa(a[i]) + ",")
}
return key, b.String()
}
func TestCallMethod(t *testing.T) {
arr := MySlice{1, 2, 3}
v := reflect.ValueOf(&arr)
typ := v.Type()
t.Logf("type:%v,value:%v", v.Type(), v)
methodName := "StructToMap"
method, ok := typ.MethodByName(methodName)
t.Logf("method:%v,ok:%v", method, ok)
methodRes := v.MethodByName(methodName).Call([]reflect.Value{})
key := methodRes[0].String()
resValue := methodRes[1].String()
t.Logf("key:%s,value:%v", key, resValue)
}
// alias type
type Gender int
const (
male Gender = 1
female Gender = 2
)
// Dive struct
type GithubPage struct {
URL string `map:"url"`
Star int `map:"star"`
}
type StructNoDive struct {
NoDive int
}
// User is used for demonstration
type User struct {
Name string `map:"name,omitempty"` // string
Email *string `map:"email_ptr,omitempty"` // pointer
MyGender Gender `map:"gender,omitempty"` // type alias
Github GithubPage `map:"github,dive,omitempty"` // struct dive
NoDive StructNoDive `map:"no_dive,omitempty"` // no dive struct
MyProfile Profile `map:"my_profile,omitempty"` // struct implements its own method
Arr []int `map:"arr,omitempty"` // normal slice
MyArr MySlice `map:"my_arr,omitempty"` // slice implements its own method
}
func newUser() User {
name := "user"
email := "yaopei.liang@foxmail.com"
myGender := male
MyGithub := GithubPage{
URL: "https://github.com/liangyaopei",
Star: 1,
}
NoDive := StructNoDive{NoDive: 1}
dateStr := "2020-07-21 12:00:00"
date, _ := time.Parse(timeLayout, dateStr)
profile := Profile{
Experience: "my experience",
Date: date,
}
arr := []int{1, 2, 3}
myArr := MySlice{11, 12, 13}
return User{
Name: name,
Email: &email,
MyGender: myGender,
Github: MyGithub,
NoDive: NoDive,
MyProfile: profile,
Arr: arr,
MyArr: myArr,
}
}
func TestStructToMap(t *testing.T) {
user := newUser()
tag := "map"
methodName := "StructToMap"
res, err := structmap.StructToMap(&user, tag, methodName)
if err != nil {
t.Errorf("struct to map:%s", err.Error())
return
}
for k, v := range res {
t.Logf("k:%v,v:%v", k, v)
}
}
type benchmarkUser struct {
Name string `json:"name"`
Age int `json:"age"`
Address string `json:"address"`
Contact string `json:"contact"`
}
func newBenchmarkUser() benchmarkUser {
return benchmarkUser{
Name: "name",
Age: 18,
Address: "github address",
Contact: "github contact",
}
}
func BenchmarkStructToMapByJson(b *testing.B) {
user := newBenchmarkUser()
b.ResetTimer()
for i := 0; i < b.N; i++ {
data, _ := json.Marshal(&user)
m := make(map[string]interface{})
json.Unmarshal(data, &m)
}
}
func BenchmarkStructToMapByToMap(b *testing.B) {
user := newBenchmarkUser()
tag := "json"
methodName := ""
b.ResetTimer()
for i := 0; i < b.N; i++ {
structmap.StructToMap(&user, tag, methodName)
}
}