10. golang之面向对象编程(上)

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)
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值