Go语言中的结构体

Go语言中提供了对struct的支持,struct,中文翻译称为结构体,与数组一样,属于复合类型,并非引用类型。

Go语言的struct,与C语言中的struct或其他面向对象编程语言中的类(class)类似,可以定义字段(属性)和方法,但也有很不同的地方,需要深入学习,才能区分他们之间的区别。

别名和类型

在前面我们说到声明一个函数我们可以用关键词func,那么声明一个结构,我们可以用关键词type

type name 类型 {}

我们也可以用这种语法给一个内置的类型起别名,例如下面的代码

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

// ...

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

这是go的源码,可以看到rune实质上是int32类型的别名,byte实质上是uint8的别名

当然,我们也可以生命自己的别名比如

package main

import "fmt"

type double = float64

func main() {
	var d1 double = double(3.1415926)
	fmt.Printf("value:%v, type:%T\n", d1, d1)
}

使用type 自定义名称 = 类型的方式可以给类型起别名

然后我们来来看看这段代码

package main

import "fmt"

type double float64

func main() {
	var d1 double = double(3.1415926)
	fmt.Printf("value:%v, type:%T\n", d1, d1)
}

在这段代码中,我们就是声明了一个double类型

定义自己的结构体

刚才我们说了可以使用type关键字来起别名和定义类型,它也可以用于定义结构体

但是我们首先得明白什么是结构体,在知道这个之前,我们得明白Go语言它并非像Java、C#这样的语言一样是一门纯面向对象的语言,那么在这种函数式语言中,我们想要表示现实生活中某样事务的多维度属性,就要用到结构体这一引用数据类型。就比如,我们现在有一个用户,他有自己的昵称、用户名、密码、手机号、年龄和爱好、,我们就可以这样表示:

package main

import "fmt"

type user struct {
	nickname string
	username string
	password string
	mobile   string
	age      int
	hobby    []string
}

func main() {
	var farajmujey user
	farajmujey.nickname = "努力做JPG程序员的FarajMujey"
	farajmujey.username = "mujeyf@gmail.com"
	farajmujey.password = "111111"
	farajmujey.age = 21
	farajmujey.hobby = append(farajmujey.hobby, "敲代码")
	farajmujey.hobby = append(farajmujey.hobby, "听音乐")
	farajmujey.hobby = append(farajmujey.hobby, "汽车")

	fmt.Printf("name: %s, all: %v, type: %T\n", farajmujey.nickname, farajmujey, farajmujey)
}

匿名结构体

和函数一样,结构体也可以声明成匿名的,但是这种匿名结构体只能声明在函数内部,并不能放在全局作用域中:

	// 匿名结构体
	var faraj struct {
		name string
		age  int
	}
	faraj.name = "FarajMujey"
	faraj.age = 21

	fmt.Printf("value: %v, type: %T\n", faraj, faraj)

结构体初始化

上面我们通过var farajmujey user的形式初始化过结构体

初次之外,我们还可以

kv初始化

	u2 := user{
		name: "FarajMujey",
		age:  20,
	}

通过key-value的形式初始化,中间用,分开,注意🎋最后一个,也不能省略。这种初始化的形式有点类似于JavaScript声明对象

值列表初始化

u3 := user{"Faraj", 20}

通过值列表初始化是最简单的初始化步骤,有点像Java、C#实例化对象的步骤,注意🎄值的顺序要严格按照结构体字段声明的顺序来

方法操作结构体

首先我们要明确一个概念:在Go语言中方法传递参数的方式永远是复制

既然方法会复制我们传递的参数,那么我们通过方法修改一个结构体中的内容的时候就需要注意了,来看一段错误的代码:

func f(user user) {
	user.name = "Mujey"
}

func main() {
	var user user
	user.age = 18
	user.name = "Faraj"
	f(user)
	fmt.Println(user)
}

当我们以为我们修改了user结构体的name属性时,其实它并没有修改,就像上面所说的,我们修改的是user的副本,和实际user没有任何的关系

想要修改真正的user,就需要通过指针(pointer)来传递对象的地址值

func f(user *user) {
	user.name = "Mujey"
}

func main() {
	var user user
	user.age = 18
	user.name = "Faraj"
	f(&user)
	fmt.Println(user)
}

有没有发现上面的代码有个小问题,user不是个指针吗,为什么可以调用name属性,其实这是Go语言自带的一个语法糖。它会自动检查这个user是不是指针,如果是指针类型的话,会自动转换成该指针对应的类型进行操作,也就是(*user).name。但是需要注意,这个语法糖只对结构体有效,基础类型还是需要自己加*取值的!

可以通过new函数来创建一个指定结构体类型的指针

结构体内存布局

一个结构体内部的字段的内存都是连续的

但是不同的数据类型所占的内存字节大小是不同的,int8占一个字节所以的步长是1,但是其他的数据类型就涉及到一个内存对齐的机制,但是他们都是连续的

构造函数

Go语言是一种面向接口的函数,它没有内置的构造函数,需要完成类面向对象编程的特性,需要自己写构造函数返回一个结构体

package main

import "fmt"

type user struct {
	username string
	password string
}

type student struct {
	id        int
	name      string
	specialty string
	score     int
	awards    []string
}

func newUser(username, password string) user {
	return user{
		username: username,
		password: password,
	}
}

func newStudent(id int, name, specialty string, score int, awards []string) *student {
	return &student{id, name, specialty, score, awards}
}

func main() {
	u1 := newUser("FarajMujey", "111111")
	fmt.Printf("%v\n", u1)
	stu1 := *newStudent(1, "Mujey", "E-commerce", 100, nil)
	fmt.Printf("%v\n", stu1)
}

需要注意的是,当结构体比较大的时候,也就是字段较多的时候建议使用返回值为结构体类型的指针,这样可以减少不必要的内存开销。

方法

在Go语言中的方法时作用于特别类型的函数

func (接收者) 函数名(参数) 返回值类型{}

方法接收者的类型名一般都用结构体的第一个字母小写

package main

import "fmt"

type student struct {
	name string
}

// student的构造方法
func newStudent(name string) student {
	return student{name}
}

// 用于student结构体的方法
func (s student) sayHi() {
	fmt.Printf("%s say Hi!\n", s.name)
}

func main() {
	// 构建一个student类型的结构体
	stu := newStudent("Faraj")
	stu.sayHi()
}

和其他的函数一样,方法的接收者也是按值传递的,也就是拷贝一份新的,所以在方法体内修改的结构体,方法体外的结构体将不受改变,所以涉及到修改值得操作,就需要用到指针了。

package main

import "fmt"

type student struct {
	name string
	age  int
}

// student的构造方法
func newStudent(name string, age int) student {
	return student{name, age}
}

// 操作结构体内部字段的方法
func (s *student) happyNewYear() {
	s.age++
}

func main() {
	// 构建一个student类型的结构体
	stu := newStudent("Faraj", 20)
	//(&stu).happyNewYear()
	stu.happyNewYear()
	fmt.Println(stu)
}

什么时候使用指针接收者

  1. 需要修改接收者中的值
  2. 接收者拷贝代价比较大的结构体
  3. 保证一致性,如果有某一个方法使用了指针接收者,那么其他的方法最好也都使用指针接收者

使用自定义类型可以定义方法

只能为自己包中的结构体定义方法,如果我们想定义int类型为接收者的方法,这时候就可以定义一个自定义的int类型的结构体

package main

import "fmt"

type d int

func (d d) value() string {
	return fmt.Sprintf("类型是%T,值是%v\n", d, d)
}

func main() {
	aInt := d(10)
	dStr := aInt.value()
	fmt.Printf("%v\n", dStr)
}

小Demo

函数版学生管理系统
能够列表显示,新增,删除,修改学生

package main

import (
	"fmt"
	"os"
)

/*
	函数版学生管理系统
	!能够列表显示,新增,删除,修改学生
*/

var students []student

type student struct {
	id    int
	name  string
	score int
}

func listStudent() {
	if len(students) == 0 {
		fmt.Println("暂无学生")
	} else {
		fmt.Printf("%-4s\t%-10s\t%s\n", "id", "姓名", "分数")
		for _, v := range students {
			fmt.Printf("%-4d\t%-10s\t%d\n", v.id, v.name, v.score)
		}
	}

}

func newStudent(id int, name string, score int) *student {
	return &student{id, name, score}
}

func addStudent(name string, score int) {
	var id int
	if len(students) > 0 {
		id = students[len(students)-1].id + 1
	} else {
		id = 1
	}
	obj := *newStudent(id, name, score)
	students = append(students, obj)
}

func deleteStudent(id int) {
	var index int
	for i := 0; i < len(students); i++ {
		if id == students[i].id {
			index = i
		}
	}
	students = append(students[:index], students[index+1:]...)
}

func updateStudentScore(id, newScore int) {
	for i := 0; i < len(students); i++ {
		if id == students[i].id {
			students[i].score = newScore
		}
	}
}

func console() {
	for {
		fmt.Println("1.查看所有学生\t2.新增一个学生\t3.删除一个学生\t4.修改学生分数\t5.退出")
		var key int
		fmt.Scanln(&key)
		switch key {
		case 1:
			listStudent()
		case 2:
			var (
				name  string
				score int
			)
			fmt.Println("请输入学生名称")
			fmt.Scanln(&name)
			fmt.Println("请输入学生分数")
			fmt.Scanln(&score)
			addStudent(name, score)
		case 3:
			var id int
			fmt.Scanln(&id)
			deleteStudent(id)
		case 4:
			var (
				id, newScore int
			)
			fmt.Println("请输入学生id")
			fmt.Scanln(&id)
			fmt.Println("请输入学生新分数")
			fmt.Scanln(&newScore)
			updateStudentScore(id, newScore)
		case 5:
			os.Exit(0)
		default:
			fmt.Println("???")
		}

	}
}

func main() {
	console()
}

匿名结构体

匿名结构体就是结构体中的字段没有名字,适合字段比较少的结构体使用,但是不推荐且不常用,了解即可

调用匿名结构体中的字段时,用结构体.字段类型的方式调用,每个类型只能有一个字段

type user struct {
    name string
    age  string
}

func main() {
    u := user{"ABC", 1}
    fmt.Println(u.string)
}

嵌套结构体

在一个结构体中,可以声明另一个结构体类型的字段:

package main

import "fmt"

type address struct {
	country  string
	province string
	city     string
	region   string
}

type company struct {
	id   int64
	name string
	addr address
}

func main() {
	obj := company{
		id:   1,
		name: "上海极梦",
		addr: address{
			country:  "中国",
			province: "上海",
			city:     "上海",
			region:   "浦东新区",
		},
	}
	fmt.Printf("region=%s\n", obj.addr.region)
}

匿名嵌套结构体

如果结构体中的嵌套结构体中的字段没有重复的话,可以使用匿名嵌套结构体的方式来简写,在调用它的时候直接可以用结构体.出匿名嵌套结构体中的字段

type company struct {
	id   int64
	name string
	address // 匿名嵌套结构体
}

func main() {
	obj := company{
		id:   1,
		name: "上海极梦",
		address: address{
			country:  "中国",
			province: "上海",
			city:     "上海",
			region:   "浦东新区",
		},
	}
	fmt.Printf("region=%s\n", obj.region) // obj.region == obj.address.region
}

Go语言中的模拟继承

Go语言是没有继承这一概念的,但是可以模拟其他面向对象语言完成继承

package main

import "fmt"

type animal struct {
	name string
	age  int
}

type dog struct {
	feet int
	animal
}

func (a animal) move() {
	fmt.Printf("%s can move\n", a.name)
}

func (d dog) shut() {
	fmt.Printf("%s can shut: wolf~\n", d.name)
}

func main() {
	d := dog{
		animal: animal{
			name: "Cola",
			age:  1,
		},
		feet: 4,
	}

	d.move()
	d.shut()
}

结构体和json转换

在前后端分离项目中,数据通常是用json传递的,在go语言的标准库中包含有结构体和json互相转换的包

结构体转换成json

使用json.Marshal()函数序列化结构体为字符串

package main

import (
	"encoding/json"
	"fmt"
)

type user struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	user := user{"Faraj", 18}

	b, err := json.Marshal(user)
	if err != nil {
		fmt.Printf("JSON Marshal err %v\n", err)
		return
	}
	fmt.Printf("%v\n", string(b))
}

{%span red, 注意🧨:在声明一个结构体的时候字段要大写,这样在encoding包下才能访问到该字段,如果想让序列化出的json小写,需要加上 %} {% inlineimage https://faraj-picgo.oss-cn-shanghai.aliyuncs.com/20200821160439.png %}

json解析成结构体

使用json.Unmarshal()函数反序列化(解析)json为结构体

package main

import (
	"encoding/json"
	"fmt"
)

type user struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	obj := user{"Faraj", 18}

	b, err := json.Marshal(obj)
	if err != nil {
		fmt.Printf("JSON Marshal err %v\n", err)
		return
	}
	str := string(b)

	var userObj user
	json.Unmarshal([]byte(str), &userObj)

	fmt.Printf("%v\n", userObj)
}

posted @ 2021-01-29 11:47  FarajMujey  阅读(218)  评论(0)    收藏  举报