1. 解决养猫问题,引出结构体前的demo
看一个问题:
张老太养了两只猫猫:一只名字叫小白,今年3岁,白色。还有一只叫小花,今年100岁,花色。请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,年龄,颜色。如果用户输入的小猫名错误,则显示张老太没有这只猫。
使用现有技术解决以上问题,如下代码:
package main
import (
"fmt"
)
func main() {
// 问题:
// 张老太养了两只猫猫:一只名字叫小白,今年3岁,白色。还有一只叫小花,今年100岁,花色。
// 请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,年龄,颜色。如果用户输入的小猫名错误,
// 则显示张老太没有这只猫。
// 使用现有技术解决
// 1. 单独的定义变量解决
var cat1Name string = "小白"
var cat1Age byte = 3
var cat1Color string = "白色"
var cat2Name string = "小花"
var cat2Age byte = 100
var cat2Color string = "花色"
// 2. 使用数组解决
var cat2Names [2]string = [...]string{"小白", "小花"}
var cat2Age [2]byte = [...]byte{3,100}
var cat2Color [2]string = [...]string{"白色", "花色"}
}
2. 使用现有技术解决的缺点分析
1)使用变量或者数组来解决养猫问题,不利于数据的管理和维护。因为名字,年龄,颜色都是属于一只猫,但是这里是分开保存。
2)如果我们希望对一只猫的属性(名字、年龄、颜色)进行操作(绑定方法),也不好处理。
3)引出我们要讲解的技术 =》 结构体
3. 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中面向接口编程是非常重要的特性。
4. 结构体与结构体变量(实例、对象)的关系示意图
* 对上图的说明:
1)将一类事物的特性提取出来(比如猫类),形成一个新的数据类型,就是一个结构体。
2)通过这个结构体,我们可以创建多个变量(实例、对象)
3)事物可以猫类,也可以是Person,Fish或者是某个工具类。
注意:
从猫结构体到变量,就是创建一个Cat结构体变量,也可以说是定义一个Cat结构体变量。当然:上面的猫也可以是鱼、狗、人。
5. struct快速入门-面向对象的方式(struct)解决养猫的问题
package main
import (
"fmt"
)
type Cat struct {
Name string
Age byte
Color string
}
func main() {
// 创建一个cat变量
var cat1 Cat
cat1.Name = "小白"
cat1.Age = 3
cat1.Color = "白色"
var cat2 Cat
cat2.Name = "花花"
cat2.Age = 100
cat2.Color = "花色"
fmt.Println("猫猫的信息如下:")
fmt.Println("name=",cat1.Name)
fmt.Println("age=",cat1.Age)
fmt.Println("color=",cat1.Color)
fmt.Println("name=",cat2.Name)
fmt.Println("age=",cat2.Age)
fmt.Println("color=",cat2.Color)
}
6. 结构体和结构体变量(实例)的区别和联系
通过上面的案例和讲解我们可以看出:
1)结构体是自定义的数据类型,代表一类事物
2)结构体变量(实例)是具体的,实际的,代表一个具体变量
7. 结构体变量(实例)在内存的布局(重要!)
8. 如何声明结构体
9.字段/属性
基本介绍
1)从概念或叫法上看:结构体字段 = 属性 = field(即授课中,统一叫字段)
2)字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫的结构体的Name string 就是属性
注意事项和细节说明
1)字段声明语法同变量,示例:字段名,字段类型
2)字段的类型可以为:基本类型、数组或引用类型
3)在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认),规则同前面讲的一样:
布尔类型是 false,数值是 0,字符串是 ""
数据类型的默认值和它的元素类型相关,比如score[3]int 则为[0,0,0]
指针、slice、和map的零值都是nil,即还没有分配空间。
案例演示:
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
Scores [5]int
ptr *int
slice []int
map1 map[string]string
}
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[0] = 100 //ok
// 使用map,一定要先make
p1.map1 = make(map[string]string)
p1.map1["key1"] = "tom"
fmt.Println(p1)
}
4)不同结构体变量的字段是独立的,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型。
package main
import (
"fmt"
)
type Monster struct {
Name string
Age byte
}
func main() {
// 不同结构体变量的字段是独立的,互不影响,一个结构体变量字段的更改
// 不影响另外一个,结构体是值类型
var monster1 Monster
monster1.Name = "牛魔王"
monster1.Age = 200
monster2 := monster1 // 结构体是值类型,默认为值拷贝
monster2.Name = "青牛精"
fmt.Println("monster1=",monster1)
fmt.Println("monster2=",monster2)
}
画出上面的内存示意图:
10. 创建结构体变量和访问结构体字段
1)方式1 - 直接声明
var 变量名 自定义结构体类型
2)方式2 - {}
var 变量名 自定义结构体类型 = 自定义结构体类型{}
3)方式3 - &
var 变量名 *自定义结构体类型 = new (自定义结构体类型)
4)方式4 - {}
var 变量名 *自定义结构体类型 = &自定义结构体类型
package main
import (
"fmt"
)
type Person struct {
Name string
Age byte
}
func main() {
// 1. 直接声明
var p1 Person
p1.Name = "tom"
p1.Age = 200
fmt.Println("p1\t",p1)
// 2. {}
var p2 Person = Person{"mary",20}
// p2.Name = "mary"
// p2.Age = 20
fmt.Println("p2\t",p2)
// 3. &
var p3 *Person = new(Person)
(*p3).Name = "smith"
// .的优先级比*更高
// 也可以写成 p3.Name
// 原因:go的设计者, 为了程序员使用方便,底层会对p3.Name = "smith进行处理"
// 会给 p3 加上取值运算(*p3).Name = "smith"
(*p3).Age = 20
fmt.Println("*p3",*p3)
// 4. {}
var p4 *Person = &Person{}
(*p4).Name = "scott"
(*p4).Age = 18
fmt.Println("p4",*p4)
}
说明:
1)第3种和第4种方式返回的是结构体指针
2)结构体指针访问字段的标准方式应该是:(*结构体指针).字段名,比如(*person).Name = "tom"
3)但go做了一个简化,也支持 结构指针.字段名,比如person.Name = "tom"。更加符合程序员的使用习惯,go编译器底层对person.Name做了转化(*person).Name。
11. struc类型的内存分配机制
看一个思考题
11. 结构体使用注意事项和细节
1)结构体的所有字段在内存种是连续存储的
代码如下:
package main
import (
"fmt"
)
type Point struct {
x int
y int
}
// 结构体
type Rect struct {
leftUp, rightDow Point
}
type Rect2 struct {
leftUp, rightDow *Point
}
func main() {
r1 := Rect{Point{1, 2}, Point{3, 4}}
// r1有四个int,在内存种是连续分布的
fmt.Printf("r1.leftUp.x的地址:%p,r1.leftUp.y的地址:%p,r1.rightDow.x的地址:%p,r1.rightDow.y的地址:%p \n",&r1.leftUp.x, &r1.leftUp.y,&r1.rightDow.x,&r1.rightDow.y)
// 如果结构体中的变量存的不是具体的值而是指针,指针本身的地址是连续的,但它们指向的地址不一定是连续的
// r2有两个 *Point类型,这两个*Point类型的本身地址也是连续的,但是它们指向的地址不一定是连续的
r2 := Rect2{&Point{1,2}, &Point{3,4}}
// 打印指针的地址
fmt.Printf("r2.leftUp的本身地址是:%p, r2.rightDow的本身地址是:%p \n",&r2.leftUp,&r2.rightDow)
// 他们指向的地址不一定是连续的...,这个要看系统在运行时是如何分配的
fmt.Printf("r2.leftUp指向的地址是:%p,r2.rightDow的指向地址是:%p \n",r2.leftUp,r2.rightDow)
}
2)结构体是用户单独定义的类型,和其它类型进行转化时需要有完全相同的字段(名字、个数和类型)
package main
import (
"fmt"
)
type A struct {
Num int
}
type B struct {
Num int
}
func main() {
// 2. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段 (名字、个数和类型)
var a A
var b B
// a = b // 报错,因为类型不一样
a = A(b) // 正确
fmt.Println(a,b)
}
3)结构体进行type重新定义(详单与取别名),Golang认为是新的数据类型,但是相互间可以强转
package main
import (
"fmt"
)
type Student struct {
Name string
Age int
}
type Stu Student
type integer int
func main() {
var stu1 Student
var stu2 Stu
stu2 = stu1 // 正确吗? 错误,可以这样修改 stu2 = stu(stu1)
fmt.Println(stu1,stu2)
var i interger = 10
var j int = 20
j = i // 正确吗? 错误,修改为 j = int(i)
}
4)struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
序列号的使用场景:
package main
import (
"fmt"
"encoding/json"
)
type Monster struct {
Name string `json: "name"`
Age int `json: "age"`
Skill string `json: "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))
}
12. 方法的基本介绍
在某些情况下,我们需要声明(定义)方法。比如Person结构体:除了有一些字段外(年龄,姓名...),Person结构体还有一些行为,比如:可以说话、跑步...,通过学习,还可以做算术题。这时就要用方法才能完成。
Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。
13. 方法的声明和调用
type A struct {
Num int
}
func (a A) test() {
fmt.Println(a.Num)
}
对上面的语法的说明
1)func(a A) test() {} 表示A结构体有一方法,方法名为test
2)(a A) 体现test方法是和A类型绑定的
package main
import (
"fmt"
)
type Person struct {
Name string
}
// 给Person类型绑定一个方法
func (p Person) test() {
fmt.Println("test()",p.Name)
}
func main() {
var p Person
p.Name = "tom"
// 调用方法
p.test()
}
对上面的总结
1)test方法和Person类型绑定
2)test方法只能通过Person类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
3)func (p Person) test() {} ...p 表示哪个Person变量调用,这个p就是它的副本,这点和函数传参非常相似。
package main
import (
"fmt"
)
type Person struct {
Name string
}
func (person Person) test() {
person.Name = "jack"
fmt.Println("test() name=",person.Name) // 输出jack
}
func main() {
var person Person
person.Name = "tom"
person.test() // 输出的是jack,因为方法传入的person为副本(拷贝的)
fmt.Println("main() name=",person.Name) // 输出的为 tom
}
4)p这个名字,由程序员指定,不是固定,比如修改成person也是可以的
14.方法的快速入门
1)给Person结构体添加speak方法,输出 xxx是一个好人
2)给Person结构体添加jisuan方法,可以计算从 1+...+100的结果,说明方法体内可以像函数一样进行各种运算
3)给Person结构体jisuan2方法,该方法可以接收一个数n,计算从1+...+n的结果
4)给Person结构体添加getSum方法,可以计算两个数的和,并返回结果
package main
import (
"fmt"
)
type Person struct {
Name string
}
// 1)给Person结构体添加speak方法,输出 xxx是一个好人
func (p Person) speak() {
fmt.Println(p.Name, "是一个goodman")
}
// 2)给Person结构体添加jisuan方法,可以计算从1+...+100的结果,说明方法体可以像函数一样,进行各种运算
func (p Person) jisuan() {
res := 0
for i := 1; i <= 100; i++ {
res += i
}
fmt.Println(p.Name,"计算的结果是:",res)
}
// 3)给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)
}
// 4)给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)
res := p.getSum(10, 20)
fmt.Println("res=",res)
}
15. 方法的调用和传参机制原理:(重要!)
案例1:
画出前面getSum方法的执行过程+说明
说明:
1)在通过一个变量去调用方法时,其调用机制和函数一样
2)不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
案例2:
请编写一个程序,要求如下:
1)声明一个结构体Circle,字段为radius
2)声明一个方法area和Circle绑定,可以返回面积
3)提示:画出area执行过程+说明
package main
import (
"fmt"
)
// 案例2:
// 请编写一个程序,要求如下:
// 1)声明一个结构体Circle,字段为radius
// 2)声明一个方法area和Circle绑定,可以返回面积
// 3)提示:画出area执行过程+说明
type Circle struct {
radius float64
}
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)
}
16.方法的声明(定义)
17.方法的注意事项和细节
1)结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
2)如果程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
3)Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int、float32都可以有方法
package main
import (
"fmt"
)
// Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此
// 自定义类型,都可以有方法,而不仅仅是struct,比如int,float32等都可以有方法
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) 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
5)如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出
package main
import (
"fmt"
)
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)
}
18. 方法的课堂练习
1)编写结构体(MethodUtils),编写一个方法,方法不需要参数,在方法中打印一个
10*8的矩形,在main()方法中调用该方法
package main
import (
"fmt"
)
// 1)编写结构体(MethodUtils),编写一个方法,方法不需要参数,在方法中打印一个
// 10*8的矩形,在main()方法中调用该方法
type MethodUtils struct {
}
func (methodUtils MethodUtils) print() {
for i := 1; i <= 10; i++ {
for j :=1; j <=8; j++ {
fmt.Printf("*")
}
fmt.Println()
}
}
func main() {
var m = MethodUtils{}
m.print()
}
2) 编写一个方法,提供m和n两个参数,方法中打印一个矩形
package main
import (
"fmt"
)
// 2) 编写一个方法,提供m和n两个参数,方法中打印一个矩形
type MethodUtils struct {}
func (m MethodUtils) print(w, h) {
for i := 1; i <= w; i++ {
for j := 1; j <= h; j++ {
fmt.Printf("*")
}
fmt.Println()
}
}
func main() {
var m MethodUtils = MethodUtils{}
m.print(10,6)
}
3)编写一个方法算该矩形的面积(可以接收长len,宽width),将其作为方法返回值。在main方法中调用该方法,接收返回的面积并打印
package main
import (
"fmt"
)
// 3)编写一个方法算该矩形的面积(可以接收长len,宽width),将其作为方法返回值。
// 在main方法中调用该方法,接收返回的面积并打印
type MethodUtils struct {}
func (m MethodUtils) print(len float64, width float64) float64 {
return len * width
}
func main() {
var m MethodUtils = MethodUtils{}
res := m.print(10.0,20.0)
fmt.Println("area=",res)
}
4)编写方法:判断一个数是奇数还是偶数
package main
import (
"fmt"
)
// 4)编写方法:判断一个数是奇数还是偶数
type MethodUtils struct {}
func (m *MethodUtils) JudgeNum(num int) {
if num % 2 == 0 {
fmt.Println(num,"是偶数")
} else if num % 2 == 1 {
fmt.Println(num,"是奇数")
}
}
func main() {
var m MethodUtils = MethodUtils{}
(&m).JudgeNum(10)
}
5)根据行、列、字符打印 对应行数和列数的字符,比如: 行:3、列:2,字符*,则打印相应的效果
package main
import (
"fmt"
)
// 5)根据行、列、字符打印 对应行数和列数的字符
// 比如: 行:3、列:2,字符*,则打印相应的效果
type MethodUtils struct {}
func (mu *MethodUtils) print(n int, m int, key string) {
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
fmt.Printf(key)
}
fmt.Println()
}
}
func main() {
var m MethodUtils = MethodUtils{}
(&m).print(10,10,"*")
}
6)定义小小计算器结构体(Calculator),实现加减乘除四个功能
package main
import (
"fmt"
)
// 6)定义小小计算器结构体(Calculator),实现加减乘除四个功能
type Calculator struct {
Num1 float64
Num2 float64
}
func (c *Calculator) getRes(operator byte) float64 {
res := 0.0
switch operator {
case '+':
res = (*c).Num1 + (*c).Num2
case '-':
res = (*c).Num1 - (*c).Num2
case '*':
res = (*c).Num1 * (*c).Num2
case '/':
res = (*c).Num1 / (*c).Num2
}
return res
}
func main() {
var c Calculator
c.Num1 = 10.0
c.Num2 = 20.0
res1 := (&c).getRes('+')
res2 := (&c).getRes('-')
res3 := (&c).getRes('*')
res4 := (&c).getRes('/')
fmt.Println("+",res1)
fmt.Println("-",res2)
fmt.Println("*",res3)
fmt.Println("/",res4)
}