结构体

Golang 语言面向对象编程说明

  1. Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
  2. Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
  3. Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
  4. Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
  5. Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。也就是说在Golang 中面向接口编程是非常重要的特性。

    快速入门-面向对象的方式(struct)解决养猫问题

    ```go //定义一个Cat结构体,将Cat的各个字段/属性信息,放入到Cat结构体进行管理 type Cat struct { Name string Age int Color string Hobby string Scores [3]int // 字段是数组… }

func main() { // 张老太养了20只猫猫:一只名字叫小白,今年3岁,白色。还有一只叫小花, // 今年100岁,花色。请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字, // 年龄,颜色。如果用户输入的小猫名错误,则显示 张老太没有这只猫猫。

  1. // 使用struct来完成案例
  2. // 创建一个Cat的变量
  3. var cat1 Cat // var a int
  4. fmt.Printf("cat1的地址=%p\n", &cat1)
  5. cat1.Name = "小白"
  6. cat1.Age = 3
  7. cat1.Color = "白色"
  8. cat1.Hobby = "吃<・)))><<"
  9. fmt.Println("cat1=", cat1)//cat1= {小白 3 白色 吃<・)))><< [0 0 0]}
  10. fmt.Println("猫猫的信息如下:")
  11. fmt.Println("name=", cat1.Name)
  12. fmt.Println("Age=", cat1.Age)
  13. fmt.Println("color=", cat1.Color)
  14. fmt.Println("hobby=", cat1.Hobby)

}

  1. <a name="DlY9b"></a>
  2. ## 结构体和结构体变量(实例)的区别和联系
  3. 1. 结构体是自定义的数据类型,代表一类事物.
  4. 1. 结构体变量(实例)是具体的,实际的,代表一个具体变量
  5. <a name="hTHjC"></a>
  6. ## 💡结构体变量(实例)在内存的布局
  7. ![](https://cdn.nlark.com/yuque/0/2022/jpeg/2608713/1653014291331-f5582858-3040-43ef-99b9-b993c293db9c.jpeg)
  8. <a name="Pv4r4"></a>
  9. ## 如何声明结构体
  10. <a name="JtB4i"></a>
  11. ### 基本语法
  12. ```go
  13. type 结构体名称 struct {
  14. field1 type
  15. field2 type
  16. }

举例

type Student struct { 
    Name string //姓名
    Age int //年龄
    Score float32 //成绩
}

字段/属性

基本介绍

  1. 从概念或叫法上看: 结构体字段 = 属性 = field (字段)
  2. 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。

    注意事项和细节说明

  3. 字段声明语法同变量,示例:字段名 字段类型

  4. 字段的类型可以为:基本类型、数组或引用类型
  5. 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:布尔类型是 false ,数值是 0 ,字符串是 “”。数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]指针,slice,和 map 的零值都是 nil ,即还没有分配空间
  6. 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型

    案例演示

    ```go package main import ( “fmt” )

//如果结构体的字段类型是: 指针,slice,和map的零值都是 nil ,即还没有分配空间 //如果需要使用这样的字段,需要先make,才能使用.

type Person struct{ Name string Age int Scores [5]float64 ptr *int //指针 slice []int //切片 map1 map[string]string //map }

type Monster struct{ Name string Age int }

func main() {

//定义结构体变量
var p1 Person
fmt.Println(p1)

if p1.ptr == nil {
    fmt.Println("ok1")
}

if p1.slice == nil {
    fmt.Println("ok2")
}

if p1.map1 == nil {
    fmt.Println("ok3")
}

//使用slice, 再次说明,一定要make
p1.slice = make([]int, 10)
p1.slice[9] = 100 //ok

//使用map, 一定要先make
p1.map1 = make(map[string]string)
p1.map1["key1"] = "tom~" 
fmt.Println(p1)

//不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,
//不影响另外一个, 结构体是值类型
var monster1 Monster
monster1.Name = "牛魔王"
monster1.Age = 500

monster2 := monster1 //结构体是值类型,默认为值拷贝
monster2.Name = "青牛精"

fmt.Println("monster1=", monster1) //monster1= {牛魔王 500}
fmt.Println("monster2=", monster2) //monster2= {青牛精 500}

}

输出
```go
{ 0 [0 0 0 0 0] <nil> [] map[]}
ok1
ok2
ok3
{ 0 [0 0 0 0 0] <nil> [0 0 0 0 0 0 0 0 0 100] map[key1:tom~]}
monster1= {牛魔王 500}
monster2= {青牛精 500}

创建结构体变量和访问结构体字段

  1. 直接声明 案例演示: var person Person
  2. {} 案例演示: var person Person = Person{}
  3. & 案例: var person *Person = new (Person)
  4. &{} 案例: var person *Person = &Person{} ```go package main import ( “fmt” )

type Person struct{ Name string Age int } func main() { //方式1 var p1 Person p1.Name = “jack” p1.Age = 11 fmt.Println(p1) fmt.Printf(“%T\n”,p1) //方式2 p2 := Person{“mary”, 20} // p2.Name = “tom” // p2.Age = 18 fmt.Println(p2) fmt.Printf(“%T\n”,p2)

//方式3-&
var p3 *Person= new(Person)
//new关键字是用来分配内存的函数,new(Type)作用是为Type类型分配并清零一块内存,并将这块内存地址作为结果返回
//new(Person)是为Person分配内存空间,返回指针类型 *Person 
//因为p3是一个指针,因此标准的给字段赋值方式 (*p3).Name = "smith"
//也可以这样写 p3.Name = "smith"

//原因: go的设计者 为了程序员使用方便,底层会对 p3.Name = "smith" 进行处理
//会给 p3 加上 取值运算 (*p3).Name = "smith"
(*p3).Name = "smith" 
// p3.Name = "john" //ok

// (*p3).Age = 30
p3.Age = 100
fmt.Println(*p3)
fmt.Printf("%T\n",p3)

//方式4-{}
//下面的语句,也可以直接给字符赋值
// var person *Person = &Person{"mary", 60} 
var person *Person = &Person{}

//因为person 是一个指针,因此标准的访问字段的方法
// (*person).Name = "scott"
// go的设计者为了程序员使用方便,也可以 person.Name = "scott"
// 原因和上面一样,底层会对 person.Name = "scott" 进行处理, 会加上 (*person)
(*person).Name = "scott"
person.Name = "scott~~"

(*person).Age = 88
person.Age = 10
fmt.Println(*person)
fmt.Printf("%T\n",person)

}

输出
```go
{jack 11}
main.Person
{mary 20}
main.Person
{smith 100}
*main.Person
{scott~~ 10}
*main.Person

补充说明

  1. 第 3 种和第 4 种方式返回的是 结构体指针
  2. 结构体指针访问字段的标准方式应该是:**(*结构体指针).字段名** ,比如(*person).Name = "tom"
  3. 但 go 做了一个简化,也支持 **结构体指针.字段名**, 比如 person.Name = “tom”。更加符合程序员使用的习惯,go 编译器底层 对 person.Name 做了转化 (*person).Name

    创建结构体变量时指定字段值

    Golang 在创建结构体实例(变量)时,可以直接指定字段的值 ```go type Stu struct { Name string Age int }

func main() { //方式1 //在创建结构体变量时,就直接指定字段的值 var stu1 = Stu{“小明”, 19} // stu1—-> 结构体数据空间 stu2 := Stu{“小明~”, 20}

//在创建结构体变量时,把字段名和字段值写在一起, 这种写法,就不依赖字段的定义顺序.
var stu3 = Stu{
    Name: "jack",
    Age:  20,
}
stu4 := Stu{
    Age:  30,
    Name: "mary",
}

fmt.Println(stu1, stu2, stu3, stu4)

//方式2, 返回结构体的指针类型(!!!)
var stu5 *Stu = &Stu{"小王", 29} // stu5--> 地址 ---》 结构体数据[xxxx,xxx]
stu6 := &Stu{"小王~", 39}

//在创建结构体指针变量时,把字段名和字段值写在一起, 这种写法,就不依赖字段的定义顺序.
var stu7 = &Stu{
    Name: "小李",
    Age:  49,
}
stu8 := &Stu{
    Age:  59,
    Name: "小李~",
}
fmt.Println(*stu5, *stu6, *stu7, *stu8) //

}

<a name="Su9Zf"></a>
## struct 类型的内存分配机制
```go
type Person struct {
    Name string
    Age  int
}

func main() {

    var p1 Person
    p1.Age = 10
    p1.Name = "小明"
    var p2 *Person = &p1 //这里是关键-->画出示意图

    fmt.Println((*p2).Age)
    fmt.Println(p2.Age)
    p2.Name = "tom~"
    fmt.Printf("p2.Name=%v p1.Name=%v \n", p2.Name, p1.Name)    // tom~ tom~
    fmt.Printf("p2.Name=%v p1.Name=%v \n", (*p2).Name, p1.Name) // tom~ tom~

    fmt.Printf("p1的地址%p\n", &p1)
    fmt.Printf("p2的地址%p p2的值%p\n", &p2, p2)

}

输出

10
10
p2.Name=tom~ p1.Name=tom~
p2.Name=tom~ p1.Name=tom~
p1的地址0xc000004078
p2的地址0xc000006028 p2的值0xc000004078

上面代码对应的内存图示意图
面向对象编程(上) - 图1

⚠️结构体使用注意事项和细节

  1. 结构体的所有字段在内存中是连续的 ```go package main import “fmt”

//结构体 type Point struct { x int y int }

//结构体 type Rect struct { leftUp, rightDown Point }

//结构体 type Rect2 struct { leftUp, rightDown *Point }

func main() {

r1 := Rect{Point{1,2}, Point{3,4}} 

//r1有四个int, 在内存中是连续分布
//打印地址
fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p \n", 
&r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)

//r2有两个 *Point类型,这个两个*Point类型的本身地址也是连续的,
//但是他们指向的地址不一定是连续

r2 := Rect2{&Point{10,20}, &Point{30,40}} 

//打印地址
fmt.Printf("r2.leftUp 本身地址=%p r2.rightDown 本身地址=%p \n", 
    &r2.leftUp, &r2.rightDown)

//他们指向的地址不一定是连续..., 这个要看系统在运行时是如何分配
fmt.Printf("r2.leftUp 指向地址=%p r2.rightDown 指向地址=%p \n", 
    r2.leftUp, r2.rightDown)

}

输出
```go
r1.leftUp.x 地址=0xc0000a8060 r1.leftUp.y 地址=0xc0000a8068 r1.rightDown.x 地址=0xc0000a8070 r1.rightDown.y 地址=0xc0000a8078 
r2.leftUp 本身地址=0xc000088220 r2.rightDown 本身地址=0xc000088228
r2.leftUp 指向地址=0xc0000aa080 r2.rightDown 指向地址=0xc0000aa090

上面代码对应的内存图示意图
面向对象编程(上) - 图2

  1. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型) ```go type A struct { Num int } type B struct { Num int }

func main() { var a A var b B a = A(b) // ? 可以转换,但是有要求,就是结构体的的字段要完全一样(包括:名字、个数和类型!) fmt.Println(a, b)//{0} {0} }


3. 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
```go
type A struct {
    Num int
}
type B struct {
    Num int
}
type C A //进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型

func main() {
    var a A
    var b B
    var c C
    //c = a //错误,需要进行强转
    c = C(a) //ok
    c = C(b) //ok
    fmt.Println(a, b,c)//{0} {0} {0}
}
  1. struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化反序列化 ``go type Monster struct{ Name stringjson:”name”//json:”name”就是 struct tag Age intjson:”age”Skill stringjson:”skill”` }

func main() { //1. 创建一个Monster变量 monster := Monster{“牛魔王”, 500, “芭蕉扇~”}

//2. 将monster变量序列化为 json格式字串
//   json.Marshal 函数中使用反射
jsonStr, err := json.Marshal(monster)
if err != nil {
    fmt.Println("json 处理错误 ", err)
}
fmt.Println("jsonStr", string(jsonStr))//jsonStr {"name":"牛魔王","age":500,"skill":"芭蕉扇~"}

}

<a name="ZyWHD"></a>
# 方法
<a name="pa2Nl"></a>
## 基本介绍
> 在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名..),Person 结构体还有一些行为比如:可以说话、跑步..,通过学习,还可以做算术题。这时就要用方法才能完成。

Golang 中的方法是**作用在指定的数据类型上的**(即:和指定的数据类型绑定),因此**自定义类型**, **都可以有方法**,而不仅仅是 **struct**
<a name="ulEl0"></a>
## 方法的声明和调用
<a name="bFteL"></a>
### 基本语法
```go
func (recevier type) methodName(参数列表) (返回值列表){
    方法体
    return 返回值
}
  1. 参数列表:表示方法输入
  2. recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
  3. receiver type : type 可以是结构体,也可以其它的自定义类型
  4. receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
  5. 返回值列表:表示返回的值,可以多个
  6. 方法体:表示为了实现某一功能代码块
  7. return 语句不是必须的。

    举例说明

    ```go package main

import ( “fmt”
)

type Person struct{ Name string }

//给Person结构体添加speak 方法,输出 xxx是一个好人 func (p Person) speak() { fmt.Println(p.Name, “是一个goodman~”)//tom 是一个goodman~ }

func main() { var p Person p.Name = “tom” p.speak() //调用方法 }

<a name="cCT6D"></a>
### 注意事项

1. speak 方法和 Person 类型绑定
1. speak方法只能通过 Person 类型的变量来调用,而**不能直接调用**,也**不能使用其它类型变量来调用**
```go
func (p Person) speak() {
    fmt.Println(p.Name, "是一个goodman~")
}

type Dog struct {

}

func main() {
    var p Person
    p.speak() //调用方法
    //下面的使用方式都是错误的
    speak() //不能直接调用
    var dog Dog    
    dog.speak() //不能使用其它类型变量来调用
}
  1. func (p Person) speak() {}… p 表示哪个 Person 变量调用,这个 p 就是它的副本, 这点和函数传参非常相似。
  2. p 这个名字,有程序员指定,不是固定, 比如修改成 person 也是可以

    方法快速入门

    ```go package main

import ( “fmt”
)

type Person struct{ Name string }

//给Person结构体添加speak 方法,输出 xxx是一个好人 func (p Person) speak() { fmt.Println(p.Name, “是一个goodman~”) }

//给Person结构体添加jisuan 方法,可以计算从 1+..+1000的结果, //说明方法体内可以和函数一样,进行各种运算 func (p Person) jisuan() { res := 0 for i := 1; i <= 1000; i++ { res += i } fmt.Println(p.Name, “计算的结果是=”, res) }

//给Person结构体jisuan2 方法,该方法可以接收一个参数n,计算从 1+..+n 的结果 func (p Person) jisuan2(n int) { res := 0 for i := 1; i <= n; i++ { res += i } fmt.Println(p.Name, “计算的结果是=”, res) }

//给Person结构体添加getSum方法,可以计算两个数的和,并返回结果 func (p Person) getSum(n1 int, n2 int) int { return n1 + n2 }

func main() { var p Person p.Name = “tom” //调用方法 p.speak() p.jisuan() p.jisuan2(20) n1 := 10 n2 := 20 res := p.getSum(n1, n2) fmt.Println(“res=”, res) }

```go
tom 是一个goodman~
tom 计算的结果是= 500500
tom 计算的结果是= 210
res= 30

💡方法的调用和传参机制原理

方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法

//1)声明一个结构体Circle, 字段为 radius
type Circle struct {
    radius float64
}

//2)声明一个方法area和Circle绑定,可以返回面积。
func (c Circle) area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    //创建一个Circle 变量
    var c Circle 
    c.radius = 4.0
    res := c.area()
    fmt.Println("面积是=", res)
}

面向对象编程(上) - 图3

//1)声明一个结构体Circle, 字段为 radius
type Circle struct {
    radius float64
}

//2)声明一个方法area和Circle绑定,可以返回面积。
func (c Circle) area() float64 {
    return 3.14 * c.radius * c.radius
}

//为了提高效率(减少数据拷贝),通常我们方法和结构体的指针类型绑定
func (c *Circle) area2() float64 {
    //因为 c是指针,因此我们标准的访问其字段的方式是 (*c).radius
    //return 3.14 * (*c).radius * (*c).radius
    // (*c).radius 等价  c.radius 
    fmt.Printf("c 是  *Circle 指向的地址=%p", c)
    return 3.14 * c.radius * c.radius
}

func main() {
    //创建一个Circle 变量
    var c Circle 
    fmt.Printf("main c 结构体变量地址 =%p\n", &c)
    c.radius = 4.0
    //res2 := (&c).area2()
    //编译器底层做了优化  (&c).area2() 等价 c.area()
    //因为编译器会自动的给加上 &c
    res2 := c.area2()
    fmt.Println("面积=", res2)
    fmt.Println("c.radius = ", c.radius) 
}
main c 结构体变量地址 =0xc000016088
c 是  *Circle 指向的地址=0xc000016088面积= 50.24
c.radius =  4

面向对象编程(上) - 图4

方法的注意事项和细节

  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  2. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
  3. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法 ```go type integer int

func (i integer) print() { fmt.Println(“i=”, i) } //编写一个方法,可以改变i的值 func (i integer) change() { i = *i + 1 }

func main() { var i integer = 10 i.print() i.change() fmt.Println(“i=”, i) }


4. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问
4. 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出
```go
type Student struct {
    Name string
    Age int
}

//给*Student实现方法String()
func (stu *Student) String() string {
    str := fmt.Sprintf("Name=[%v] Age=[%v]", stu.Name, stu.Age)
    return str
}

func main() {
    //定义一个Student变量
    stu := Student{
        Name : "tom",
        Age : 20,
    }
    //如果你实现了 *Student 类型的 String方法,就会自动调用
    fmt.Println(&stu) //Name=[tom] Age=[20]
}

方法和函数区别

  1. 调用方式不一样
    1. 函数的调用方式: 函数名(实参列表)
    2. 方法的调用方式: 变量.方法名(实参列表)
  2. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
  3. 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以 ```go type Person struct { Name string }

//函数 //对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然

func test01(p Person) { fmt.Println(p.Name) }

func test02(p *Person) { fmt.Println(p.Name) }

//对于方法(如struct的方法), //接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以

func (p Person) test03() { p.Name = “jack” fmt.Println(“test03() =”, p.Name) // jack }

func (p *Person) test04() { p.Name = “mary” fmt.Println(“test03() =”, p.Name) // mary }

func main() { p := Person{“tom”} test01(p) test02(&p)

p.test03()
fmt.Println("main() p.name=", p.Name) // tom

(&p).test03() // 从形式上是传入地址,但是本质仍然是值拷贝

fmt.Println("main() p.name=", p.Name) // tom

(&p).test04()
fmt.Println("main() p.name=", p.Name) // mary
p.test04() // 等价 (&p).test04 , 从形式上是传入值类型,但是本质仍然是地址拷贝

}

总结:不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.如果是和值类型,比如    `(p Person) `, 则是值拷贝, 如果和指针类型,比如是` (p *Person) `则是地址拷贝
<a name="CAHhK"></a>
# 工厂模式
<a name="L6a0A"></a>
## 说明
Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题
<a name="tnN9S"></a>
## 需求
一个结构体的声明是这样的: 
```go
package model

type Student struct { 
    Name string...
}

因为这里的Student 的首字母S 是大写的,如果我们想在其它包创建 Student 的实例(比如main 包), 引入 model 包后,就可以直接创建 Student 结构体的变量(实例)。但是问题来了,如果首字母是小写的, 比如 是 type student struct {….} 就不不行了,怎么办—-> 工厂模式来解决

工厂模式跨包创建结构体实例(变量)

如果 model 包的 结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决

package model

//定义一个结构体
type student struct{
    Name string //首字母是大写的-->共有属性
    score float64 //首字母是小写的-->私有属性
}

//因为student结构体首字母是小写,因此是只能在model使用
//我们通过工厂模式来解决
func NewStudent(n string, s float64) *student {
    return &student{
        Name : n,
        score : s,
    }
}

//如果score字段首字母小写,则在其它包不可以直接访问,我们可以提供一个方法
func (s *student) GetScore() float64{
    return s.score //ok
}
package main
import (
    "fmt"
    "go_code/chapter10/factory/model"
)

func main() {
    //定student结构体是首字母小写,我们可以通过工厂模式来解决
    var stu = model.NewStudent("tom~", 98.8)

    fmt.Println(*stu)
    fmt.Println("name=", stu.Name, " score=", stu.GetScore())
}