一、关于结构体

Golang中没有“类”的概念,Golang中的结构体和其他语言中的类有点相似。和其他面向对象语言中的类相比,Golang中的结构体具有更高的扩展性和灵活性。

tip:结构体差不多是类的定义,go没有类,结构体像个声明

Golang中的基础数据类型可以装示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型就无法满足需求了,Golang提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。也就是我们可以通过struct来定义自己的类型了。

tip:Java里面就是你可以自定义类

二 . Type关键字

Golang中通过type关键词定义一个结构体,需要注意的是,数组和结构体都是值类型 ( !!!),这个和Java是有区别的

tip: 下面会看到,结构体和数组一样,都是占用一块连续的内存空间!那要是赋值也是拷贝,所以我觉得结构体一般会转化成指针类型来用,不然内存累死了!
(存疑。。。。)

2.1 自定义类型

在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型。

  1. type myInt int

上面代码表示:将mylnt定义为int类型,通过type关键字的定义,mylnt就是一种新的类型,它具有int的特性。
示例:如下所示,我们定义了一个myInt类型

  1. type myInt int
  2. func main() {
  3. var a myInt = 10
  4. fmt.Printf("%v %T", a, a)
  5. }

输出查看它的值以及类型,能够发现该类型就是myInt类型

  1. 10 main.myInt

除此之外,我们还可以定义一个方法类型

  1. func fun(x int, y int)int {
  2. return x + y
  3. }
  4. func main() {
  5. var fn myFn = fun
  6. fmt.Println(fn(1, 2)) //3
  7. }

2.2 类型别名

Golang1.9版本以后添加的新功能
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有大名、小名、英文名,但这些名字都指的是他本人

  1. type TypeAlias = Type

我们之前见过的rune 和 byte 就是类型别名,他们的底层代码如下

  1. type byte = uint8
  2. type rune = int32

2.3 类型定义和类型别名

类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

  1. //类型定义
  2. type NewInt int
  3. //类型别名
  4. type MyInt = int
  5. func main() {
  6. var a NewInt
  7. var b MyInt
  8. fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
  9. fmt.Printf("type of b:%T\n", b) //type of b:int
  10. }

结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

三 . 结构体定义和初始化

3.1 结构体的定义

使用typestruct关键字来定义结构体,具体代码格式如下所示:

  1. /**
  2. 定义一个人结构体
  3. */
  4. type Person struct {
  5. name string
  6. age int
  7. sex string
  8. }
  9. func main() {
  10. // 实例化结构体
  11. var person Person
  12. person.name = "张三"
  13. person.age = 20
  14. person.sex = "男"
  15. fmt.Printf("%#v", person)
  16. }

这样我们就拥有了一个person的自定义类型,它有name、age、sex 三个字段,分别表示姓名、年龄、性别。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。(用来描述一组值)

注意:结构体首字母可以大写也可以小写,大写表示这个结构体是公有的,在其它的包里面也可以使用,小写表示结构体属于私有的,在其它地方不能使用(GO里面大小写区分很明显! )
例如:

  1. type Person struct {
  2. Name string
  3. Age int
  4. Sex string
  5. }

3.2 实例化/初始化结构体

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

1 . var person Person实例化结构体

结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
刚刚实例化结构体用到了:var person Person(var 结构体实例 结构体类型)

  1. // 实例化结构体
  2. var person Person
  3. person.name = "张三"
  4. person.age = 20
  5. person.sex = "男"

2. new关键字+创建指针类型

我们下面使用另外一个方式来实例化结构体,通过new关键字来实例化结构体,得到的是结构体的地址,格式如下

  1. var person2 = new(Person)
  2. person2.name = "李四"
  3. person2.age = 30
  4. person2.sex = "女"
  5. fmt.Printf("%#v", person2)

输出如下所示,从打印结果可以看出person2是一个结构体指针

  1. &main.Person{name:"李四", age:30, sex:"女"}

需要注意:在Golang中支持对结构体指针直接使用,来访问结构体的成员

  1. person2.name = "李四"
  2. // 等价于
  3. (*person2).name = "李四"

3. 取地址实例化结构体

使用&对结构体进行取地址操作,相当于对该结构体类型进行了一次new实例化操作

  1. // 第三种方式实例化
  2. var person3 = &Person{}
  3. person3.name = "赵四"
  4. person3.age = 28
  5. person3.sex = "男"
  6. fmt.Printf("%#v", person3)

4. 键值对初始化结构体

没有初始化的结构体,其成员变量都是对应其类型的零值。
使用键值对的方式来实例化结构体,实例化的时候,可以直接指定对应的值

  1. // 第四种方式初始化
  2. var person4 = Person{
  3. name: "张三",
  4. age: 10,
  5. sex: "女",
  6. }
  7. fmt.Printf("%#v", person4)

5. 结构体指针初始化结构体

第五种和第四种差不多,不过是用了取地址,然后返回的也是一个地址

  1. // 第五种方式初始化
  2. var person5 = &Person{
  3. name: "孙五",
  4. age: 10,
  5. sex: "女",
  6. }
  7. fmt.Printf("%#v", person5)

6. 不写key初始化结构体

第六种方式是可以简写结构体里面的key

  1. var person6 = Person{
  2. "张三",
  3. 5,
  4. "女",
  5. }
  6. fmt.Println(person6)

这里漏了一个知识

结构体占用一块连续的内存。(和数组一样)
另:也有匿名结构体,和匿名方法什么差不多,不说了!
传送门:结构体详解!

构造函数

Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

  1. func newPerson(name, city string, age int8) *person {
  2. return &person{
  3. name: name,
  4. city: city,
  5. age: age,
  6. }
  7. }

调用构造函数

  1. p9 := newPerson("张三", "沙河", 90)
  2. fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"沙河", age:90}

四 、结构体方法和接收者

在go语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接收者的函数。接收者的概念就类似于其他语言中的this (Java里的)或者self(这个是啥?)。
方法的定义格式如下:

  1. func (接收者变量 接收者类型) 方法名(参数列表)(返回参数) {
  2. 函数体
  3. }

其中

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为p,Connector类型的接收者变量应该命名为c等。
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
    • 非指针类型:表示不修改结构体的内容
    • 指针类型:表示修改结构体中的内容 ( 果然!)
  • 方法名、参数列表、返回参数:具体格式与函数定义相同

如果示例所示:

  1. /**
  2. 定义一个人结构体
  3. */
  4. type Person struct {
  5. name string
  6. age int
  7. sex string
  8. }
  9. // 定义一个结构体方法
  10. func (p Person) PrintInfo() {
  11. fmt.Print(" 姓名: ", p.name)
  12. fmt.Print(" 年龄: ", p.age)
  13. fmt.Print(" 性别: ", p.sex)
  14. fmt.Println()
  15. }
  16. //搞一个setInfo的方法!
  17. func (p *Person) SetInfo(name string, age int, sex string) {
  18. p.name = name
  19. p.age = age
  20. p.sex = sex
  21. }
  22. func main() {
  23. var person = Person{
  24. "张三",
  25. 18,
  26. "女",
  27. }
  28. person.PrintInfo()
  29. person.SetInfo("李四", 18, "男")
  30. person.PrintInfo()
  31. }

运行结果为:

  1. 姓名: 张三 年龄: 18 性别:
  2. 姓名: 李四 年龄: 18 性别:

注意,指针改的是地址,值类型改的是副本,无法修改本身!下面是值类型

  1. // SetAge2 设置p的年龄
  2. // 使用值接收者
  3. func (p Person) SetAge2(newAge int8) {
  4. p.age = newAge
  5. }
  6. func main() {
  7. p1 := NewPerson("小王子", 25)
  8. p1.Dream()
  9. fmt.Println(p1.age) // 25
  10. p1.SetAge2(30) // (*p1).SetAge2(30)
  11. fmt.Println(p1.age) // 25
  12. }

五、 给任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

  1. type myInt int
  2. func fun(x int, y int)int {
  3. return x + y
  4. }
  5. func (m myInt) PrintInfo() {
  6. fmt.Println("我是自定义类型里面的自定义方法")
  7. }
  8. func main() {
  9. var a myInt = 10
  10. fmt.Printf("%v %T \n", a, a)
  11. a.PrintInfo()
  12. }

注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。(这谁没事定义啊)

六、 结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就被称为匿名字段
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能一个

  1. /**
  2. 定义一个人结构体
  3. */
  4. type Person struct {
  5. string
  6. int
  7. }
  8. func main() {
  9. // 结构体的匿名字段
  10. var person = Person{
  11. "张三",
  12. 18
  13. }
  14. }

结构体的字段类型可以是:基本数据类型,也可以是切片、Map 以及结构体
如果结构体的字段类似是:指针、slice、和 map 的零值都是nil,即还没有分配空间
如果需要使用这样的字段,需要先make,才能使用

(非常重要!!!)

  1. /**
  2. 定义一个人结构体
  3. */
  4. type Person struct {
  5. name string
  6. age int
  7. hobby []string
  8. mapValue map[string]string
  9. }
  10. func main() {
  11. // 结构体的匿名字段
  12. var person = Person{}
  13. person.name = "张三"
  14. person.age = 10
  15. // 给切片申请内存空间
  16. person.hobby = make([]string, 4, 4)
  17. person.hobby[0] = "睡觉"
  18. person.hobby[1] = "吃饭"
  19. person.hobby[2] = "打豆豆"
  20. // 给map申请存储空间
  21. person.mapValue = make(map[string]string)
  22. person.mapValue["address"] = "北京"
  23. person.mapValue["phone"] = "123456789"
  24. // 加入#打印完整信息
  25. fmt.Printf("%#v", person)
  26. }

同时我们还支持结构体的嵌套,如下所示

  1. // 用户结构体
  2. type User struct {
  3. userName string
  4. password string
  5. sex string
  6. age int
  7. address Address // User结构体嵌套Address结构体
  8. }
  9. // 收货地址结构体
  10. type Address struct {
  11. name string
  12. phone string
  13. city string
  14. }
  15. func main() {
  16. var u User
  17. u.userName = "moguBlog"
  18. u.password = "123456"
  19. u.sex = "男"
  20. u.age = 18
  21. var address Address
  22. address.name = "张三"
  23. address.phone = "110"
  24. address.city = "北京"
  25. u.address = address
  26. fmt.Printf("%#v", u)
  27. }

七 、嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名,这个时候为了避免歧义,需要指定具体的内嵌结构体的字段。
(例如,父结构体中的字段 和 子结构体中的字段相似)
默认会从父结构体中寻找,如果找不到的话,再去子结构体中在找
如果子类的结构体中,同时存在着两个相同的字段,那么这个时候就会报错了,因为程序不知道修改那个字段的为准。

  1. //Address 地址结构体
  2. type Address struct {
  3. Province string
  4. City string
  5. CreateTime string
  6. }
  7. //Email 邮箱结构体
  8. type Email struct {
  9. Account string
  10. CreateTime string
  11. }
  12. //User 用户结构体
  13. type User struct {
  14. Name string
  15. Gender string
  16. Address
  17. Email
  18. }
  19. func main() {
  20. var user3 User
  21. user3.Name = "沙河娜扎"
  22. user3.Gender = "男"
  23. // user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
  24. user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
  25. user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
  26. }

八 、结构体的继承

结构体的继承,其实就类似于结构体的嵌套,如下所示,我们定义了两个结构体,分别是Animal 和 Dog,其中每个结构体都有各自的方法,然后通过Dog结构体 继承于 Animal结构体

  1. // 用户结构体
  2. type Animal struct {
  3. name string
  4. }
  5. func (a Animal) run() {
  6. fmt.Printf("%v 在运动 \n", a.name)
  7. }
  8. // 子结构体
  9. type Dog struct {
  10. age int
  11. // 通过结构体嵌套,完成继承
  12. Animal
  13. }
  14. func (dog Dog) wang() {
  15. fmt.Printf("%v 在汪汪汪 \n", dog.name)
  16. }
  17. func main() {
  18. var dog = Dog{
  19. age: 10,
  20. Animal: Animal{
  21. name: "阿帕奇",
  22. },
  23. }
  24. dog.run();
  25. dog.wang();
  26. }

运行后,发现Dog拥有了父类的方法

  1. 阿帕奇 在运动
  2. 阿帕奇 在汪汪汪

九、 Go中的结构体和Json相互转换

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。RESTfull Api接口中返回的数据都是json数据。

  1. {
  2. "name": "张三",
  3. "age": 15
  4. }

比如我们Golang要给App或者小程序提供Api接口数据,这个时候就需要涉及到结构体和Json之间的相互转换 Golang JSON序列化是指把结构体数据转化成JSON格式的字符串 Golang JSON的反序列化是指把JSON数据转化成Golang中的结构体对象

(这俩句车轱辘,就是 序列化————反序列化 是个逆过程!)
Golang中的序列化和反序列化主要通过“encoding/json”包中的json.Marshal()son.Unmarshal()

  1. // 定义一个学生结构体,注意结构体的首字母必须大写,代表公有,否则将无法转换
  2. type Student struct {
  3. ID string
  4. Gender string
  5. Name string
  6. Sno string
  7. }
  8. func main() {
  9. var s1 = Student{
  10. ID: "12",
  11. Gender: "男",
  12. Name: "李四",
  13. Sno: "s001",
  14. }
  15. // 结构体转换成Json(返回的是byte类型的切片)
  16. jsonByte, _ := json.Marshal(s1)
  17. jsonStr := string(jsonByte)
  18. fmt.Printf(jsonStr)
  19. }

反序列化: 将字符串转换成结构体类型

  1. // 定义一个学生结构体,注意结构体的首字母必须大写,代表公有,否则将无法转换
  2. type Student struct {
  3. ID string
  4. Gender string
  5. Name string
  6. Sno string
  7. }
  8. func main() {
  9. // Json字符串转换成结构体
  10. var str = `{"ID":"12","Gender":"男","Name":"李四","Sno":"s001"}`
  11. var s2 = Student{}
  12. // 第一个是需要传入byte类型的数据,第二参数需要传入转换的地址
  13. err := json.Unmarshal([]byte(str), &s2)
  14. if err != nil {
  15. fmt.Printf("转换失败 \n")
  16. } else {
  17. fmt.Printf("%#v \n", s2)
  18. }
  19. }

注意
我们想要实现结构体转换成字符串,必须保证结构体中的字段是公有的,也就是首字母必须是大写的,这样才能够实现结构体 到 Json字符串的转换。

十、 结构体标签Tag

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

  1. `key1:"value1" key2:"value2"`

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

注意事项:为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

费劲!!
如下所示,我们通过tag标签,来转换字符串的key

  1. // 定义一个Student体,使用结构体标签
  2. type Student2 struct {
  3. Id string `json:"id"` // 通过指定tag实现json序列化该字段的key
  4. Gender string `json:"gender"`
  5. Name string `json:"name"`
  6. Sno string `json:"sno"`
  7. }
  8. func main() {
  9. var s1 = Student2{
  10. Id: "12",
  11. Gender: "男",
  12. Name: "李四",
  13. Sno: "s001",
  14. }
  15. // 结构体转换成Json
  16. jsonByte, _ := json.Marshal(s1)
  17. jsonStr := string(jsonByte)
  18. fmt.Println(jsonStr)
  19. // Json字符串转换成结构体
  20. var str = `{"Id":"12","Gender":"男","Name":"李四","Sno":"s001"}`
  21. var s2 = Student2{}
  22. // 第一个是需要传入byte类型的数据,第二参数需要传入转换的地址
  23. err := json.Unmarshal([]byte(str), &s2)
  24. if err != nil {
  25. fmt.Printf("转换失败 \n")
  26. } else {
  27. fmt.Printf("%#v \n", s2)
  28. }
  29. }

十一、 嵌套结构体和Json序列化反序列化

和刚刚类似,我们同样也是使用的是 json.Marshal()

  1. // 嵌套结构体 到 Json的互相转换
  2. // 定义一个Student结构体
  3. type Student3 struct {
  4. Id int
  5. Gender string
  6. Name string
  7. }
  8. // 定义一个班级结构体
  9. type Class struct {
  10. Title string
  11. Students []Student3
  12. }
  13. func main() {
  14. var class = Class{
  15. Title: "1班",
  16. Students: make([]Student3, 0),
  17. }
  18. for i := 0; i < 10; i++ {
  19. s := Student3{
  20. Id: i + 1,
  21. Gender: "男",
  22. Name: fmt.Sprintf("stu_%v", i + 1),
  23. }
  24. class.Students = append(class.Students, s)
  25. }
  26. fmt.Printf("%#v \n", class)
  27. // 转换成Json字符串
  28. strByte, err := json.Marshal(class)
  29. if err != nil {
  30. fmt.Println("打印失败")
  31. } else {
  32. fmt.Println(string(strByte))
  33. }
  34. }

十二、 面试题

  1. type student struct {
  2. name string
  3. age int
  4. }
  5. func main() {
  6. m := make(map[string]*student)
  7. stus := []student{
  8. {name: "小王子", age: 18},
  9. {name: "娜扎", age: 23},
  10. {name: "大王八", age: 9000},
  11. }
  12. for _, stu := range stus {
  13. m[stu.name] = &stu
  14. }
  15. for k, v := range m {
  16. fmt.Println(k, "=>", v.name)
  17. }
  18. }

结果:不管前面怎么变,后面都是大王八!
image.png
因为它在切片中取得的地址值每次是一样的,遍历后取得永远是最后一个
如果想不一样,可以改成

  1. func main() {
  2. m := make(map[string]*student)
  3. stus := []student{
  4. {name: "小王子", age: 18},
  5. {name: "娜扎", age: 23},
  6. {name: "大王八", age: 9000},
  7. }
  8. for _, stu := range stus {
  9. stutemp := &student{
  10. name : stu.name,
  11. age : stu.age,
  12. }
  13. m[stu.name] = stutemp
  14. }
  15. fmt.Println(m)
  16. for k, v := range m {
  17. fmt.Println(k, "=>", v.name)
  18. }
  19. }