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的别名,本质上TypeAliasType是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

  • MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

    // 类型别名:只存在代码编写过程中,代码编译之后根本不存在MyInt
    // 提高代码的可读性
    type MyInt = int
    
    func main() {
    	var b MyInt
    	fmt.Println(b)
    	fmt.Printf("%T\n",b)
    }
    // 0
    // int
    
  • 我们之前见过的runebyte就是类型别名

    type byte = uint8
    type rune = int32
    

结构体

  • 在Go语言中,是通过struct来实现面向对象。
  • struct是值类型

结构体定义

  • 使用typestruct关键字来定义结构体:

    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)
    

    使用这种格式初始化时,需要注意:

    1. 必须把结构体的所有字段都赋值初始化
    2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
    3. 该方式不能和键值初始化方式混用。

结构体内存布局

  • 结构体占用一块连续的内存。注意观察内存地址,都是连续的。

    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 (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
        函数体
    }
    
    • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 pConnector类型的接收者变量应该命名为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指针,所以只是拷贝了一份,并没有修改值
    

什么时候使用指针类型接收者

  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

任意类型添加方法

  • 在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的情况,在实际编码过程中一定要注意这个问题。

posted @ 2020-02-11 13:01  薛定谔的猫儿  阅读(84)  评论(0编辑  收藏  举报