10-Go语言之结构体和JSON
内容目录
- 类型别名和自定义类型
内容详细
自定义类型和类型别名
自定义类型
-
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。
-
通过
type
关键字的定义,自定义的MyInt
就是一种新的类型,它具有int
的特性。// NewInt 是一个新的类型 type NewInt int func main() { var a NewInt fmt.Println(a) fmt.Printf("a的类型是:%T\n",a) } // 0 // a的类型是:main.NewInt main包下面的自定义变量NewInt
类型别名
-
类型别名规定:
TypeAlias
只是Type
的别名,本质上TypeAlias
与Type
是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。 -
MyInt
类型只会在代码中存在,编译完成时并不会有MyInt
类型。// 类型别名:只存在代码编写过程中,代码编译之后根本不存在MyInt // 提高代码的可读性 type MyInt = int func main() { var b MyInt fmt.Println(b) fmt.Printf("%T\n",b) } // 0 // int
-
我们之前见过的
rune
和byte
就是类型别名type byte = uint8 type rune = int32
结构体
- 在Go语言中,是通过
struct
来实现面向对象。 struct
是值类型
结构体定义
-
使用
type
和struct
关键字来定义结构体:type 类型名 struct { 字段名 字段类型 字段名 字段类型 … } 类型名:标识自定义结构体的名称,在同一个包内不能重复。 字段名:表示结构体字段名。结构体中的字段名必须唯一。 字段类型:表示结构体字段的具体类型。
-
结构体是可以用
.
来调取结构体本身的类型的// 结构体 // 创建新的类型要使用type关键字 type student struct { name string age int gender string hobby []string } func main() { var xiaoming = student{ name: "小明", age: 19, gender: "男", hobby: []string{"篮球","足球","羽毛球"}, } // 结构体支持 . 访问属性 fmt.Println(xiaoming) fmt.Println(xiaoming.name) fmt.Println(xiaoming.age) fmt.Println(xiaoming.gender) } // {小明 19 男 [篮球 足球 羽毛球]} 小明 19 男
结构体的实例化
- 意为声明一个结构体,其值全为类型的空值
基本实例化
type student struct {
name string
age int
gender string
hobby []string
}
func main{
// 实例化方法1
// 如果初始化时没有给属性(字段)设置对应的初始值,那么对应属性就是其类型的空值
var zhangsan = student{}
fmt.Println(zhangsan.name) // 空字符串
fmt.Println(zhangsan.age) // 0int类型
}
匿名实例化
-
在定义一些临时数据结构等场景下还可以使用匿名结构体。
func main() { var user struct{Name string; Age int} user.Name = "小王子" user.Age = 18 fmt.Printf("%#v\n", user) }
实例化指针类型结构体
-
通过使用
new
关键字对结构体进行实例化,得到的是结构体的地址func main{ // 实例化方法2:使用new来创建指针实例化,得到结构体的指针 var yawei = new(student) // 已经初始化了 fmt.Println(yawei) yawei.name = "亚伟" yawei.age = 26 yawei.gender = "男" yawei.hobby = []string{"篮球", "足球", "羽毛球"} fmt.Print(yawei.name, yawei.age) // 实例化方法3:跟方法2类似,得到结构体的指针 var nazha = &student{} fmt.Println(nazha) nazha.name = "沙河娜扎" nazha.age = 28 }
结构体初始化
- 初始化后将可以进行赋值操作,开辟内存空间
使用键值对初始化
-
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
xiaoming := student{ name: "小明", age: 19, gender: "男", hobby: []string{"篮球", "足球", "羽毛球"}, } fmt.Print(xiaoming.name)
-
也可以对结构体指针进行键值对初始化
-
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
stu2 := &student{ name: "小王子", age: 23, } fmt.Printf("stu2=%#v\n", stu2) // stu2=&main.student{name:"小王子", age:23, gender:"", hobby:[]string(nil)}
使用值的列表初始化
-
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
var stu1 = student{ "知春路吴彦祖", 26, "男", []string{"琴","棋","书","画"}, } fmt.Print(stu1.name, stu1.age)
使用这种格式初始化时,需要注意:
- 必须把结构体的所有字段都赋值初始化
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
结构体内存布局
-
结构体占用一块连续的内存。注意观察内存地址,都是连续的。
type test struct { a int8 b int8 c int8 d int8 } n := test{ 1, 2, 3, 4, } fmt.Printf("n.a %p\n", &n.a) fmt.Printf("n.b %p\n", &n.b) fmt.Printf("n.c %p\n", &n.c) fmt.Printf("n.d %p\n", &n.d) /* n.a 0xc0000a0060 n.b 0xc0000a0061 n.c 0xc0000a0062 n.d 0xc0000a0063 */
空结构体
- 空结构体是不占用内存空间的
构造函数
-
Go语言的结构体没有构造函数
-
因为
struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。type student struct { name string age int gender string hobby []string } // 实现一个构造函数 func newStudent(name string, age int, gender string, hobby []string) *student { return &student{ name: name, age: age, gender: gender, hobby: hobby, } } func main() { xiaoming := newStudent("小明", 19, "男", []string{"篮球", "足球", "羽毛球"}) fmt.Print(xiaoming.name) }
方法和接收者
-
Go语言中的
方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者self
。func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 }
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
// 函数就是谁都可以调用的 // 方法就是某个具体的类型才能调用的函数 type people struct { name string gender string } // 函数指定接受者之后就是方法 // 在go语言中约定成俗不用this/self,而是使用后面类型的首字母小写 func (p *people) dream() { fmt.Println("没有梦想那跟咸鱼有什么区别") } func main() { var xiaoming = people{ name : "小明", gender : "男", } xiaoming.dream() fmt.Println(xiaoming.name,xiaoming.gender) } // 没有梦想那跟咸鱼有什么区别 // 小明,男
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
指针类型的接收者
-
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
func (p *people) dream() { p.name = "小红" p.gender = "女" // 如果要改值的话,必须传入结构体的指针 fmt.Println("没有梦想那跟咸鱼有什么区别") } func main() { var xiaoming = people{ name : "小明", gender : "男", } xiaoming.dream() fmt.Println(xiaoming.name,xiaoming.gender) } // 没有梦想那跟咸鱼有什么区别 // 小红,女
值类型的接收者
-
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
func (p people) setName(name string) { p.name = name } func main() { var xiaoming = people{ name : "小明", gender : "男", } xiaoming.setName("明辉") fmt.Println(xiaoming.name,xiaoming.gender) } // 小明,男 // 因为setName方法传值时不是传入的people指针,所以只是拷贝了一份,并没有修改值
什么时候使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
任意类型添加方法
-
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
//MyInt 将int定义为自定义MyInt类型 type MyInt int //SayHello 为MyInt添加一个SayHello的方法 func (m MyInt) SayHello() { fmt.Println("Hello, 我是一个int。") } func main() { var m1 MyInt m1.SayHello() //Hello, 我是一个int。 m1 = 100 fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt }
-
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
嵌套结构体(重要)
-
一个结构体中可以嵌套包含另一个结构体或结构体指针
type address struct { province string city string } type student struct { name string age int addr address // 嵌套了别的结构体 } func main() { var stu1 = student{ name: "阿水", age: 18, addr: address{ province:"河北", city:"雄安", }, } fmt.Println(stu1) fmt.Println(stu1.name) fmt.Println(stu1.addr.city) } // {阿水 18 {河北 雄安}} // 阿水 // 雄安
嵌套匿名字段
-
上面user结构体中嵌套的
Address
结构体也可以采用匿名字段的方式 -
当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。
-
注意:如果嵌套了有多个匿名结构体,为避免字段冲突,建议还是用显示调用
func main() { var stu1 = student{ name: "阿水", age: 18, address: address{ // 使用结构体的名称 province:"河北", city:"雄安", }, } fmt.Println(stu1.address.city) fmt.Println(stu1.city) // 嵌套的匿名结构体可以简写,与上面的相同 }
继承
-
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
type animal struct { name string } // 定义一个动物的方法 func (a *animal) move() { fmt.Printf("%s会动~\n", a.name) } // 定义了一个狗的结构体 type dog struct { feet int animal } // 定义了一个狗的方法 func (d *dog) wangwang() { fmt.Printf("%s 再叫:汪汪汪~\n", d.animal.name) } func main() { var a = dog{ feet: 4, animal: animal{ name: "旺财", }, } a.wangwang() // 调用狗的方法 a.move() // 调用动物的方法 }
结构体字段的可见性
- 结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
结构体与JSON序列化
-
JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号
""
包裹,使用冒号:
分隔,然后紧接着值;多个键值之间使用英文,
分隔。 -
注意:结构体转JSON时首字母必须大写!首字母大写!字段也需要首字母大写!
import ( "encoding/json" "fmt" ) // json序列化 // Student 是一个结构体 type Student struct { // 结构体内部字段首字母必须使用大写 ID int Gender string Name string } func main() { var stu1 = Student{ ID: 1, Gender: "男", Name: "阿飞", } // 序列化:把编程语言里面的数据转换成JSON格式的字符串 v, err := json.Marshal(stu1) if err != nil { fmt.Println("Json格式化出错了!") fmt.Println(err) } fmt.Println(v) // []byte类型 fmt.Println(string(v)) // 把[]byte类型转换成string fmt.Printf("%#v\n", string(v)) // 把[]byte类型转换成string // 反序列化:把满足JSON格式的字符串转换成 当前编程语言里面的对象 str := "{\"ID\":1,\"Gender\":\"男\",\"Name\":\"阿飞\"}" var stu2 = &Student{} json.Unmarshal([]byte(str), stu2) fmt.Println(stu2) fmt.Printf("%T\n", stu2) } // [123 34 73 68 34 58 49 44 34 71 101 110 100 101 114 34 58 34 231 148 183 34 44 34 78 97 109 101 34 58 34 233 152 191 233 163 158 34 125] // {"ID":1,"Gender":"男","Name":"阿飞"} // "{\"ID\":1,\"Gender\":\"男\",\"Name\":\"阿飞\"}" // &{1 男 阿飞} // *main.Student
结构体标签(Tag)
-
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag
在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:`key1:"value1" key2:"value2"` // 结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
-
例:为结构体的每个字段定义json序列化时使用的Tag:
-
使用tag后,json默认转换后就变成json的值了
type Student struct { // 结构体内部字段首字母必须使用大写 ID int `json:"id"` Gender string `json:"gender"` Name string `json:"name"` } // json序列化后的结构体: {"id":1,"gender":"男","name":"阿飞"} :首字母变小写
结构体和方法补充知识点
-
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。
// 注意,此时SetDreams中将传参改成了data,修改data的值,也将修改p1.dreams的值 type Person struct { name string age int8 dreams []string } func (p *Person) SetDreams(dreams []string) { p.dreams = dreams } func main() { p1 := Person{name: "小王子", age: 18} data := []string{"吃饭", "睡觉", "打豆豆"} p1.SetDreams(data) // 你真的想要修改 p1.dreams 吗? data[1] = "不睡觉111" fmt.Println(p1.dreams) // [吃饭 不睡觉111 打豆豆] fmt.Println(data) // [吃饭 不睡觉111 打豆豆] }
-
正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。
func (p *Person) SetDreams(dreams []string) { p.dreams = make([]string, len(dreams)) copy(p.dreams, dreams) } func main() { p1 := Person{name: "小王子", age: 18} data := []string{"吃饭", "睡觉", "打豆豆"} p1.SetDreams(data) // 你真的想要修改 p1.dreams 吗? data[1] = "不睡觉111" fmt.Println(p1.dreams) // [吃饭 睡觉 打豆豆] fmt.Println(data) // [吃饭 不睡觉111 打豆豆] }
同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。