75142913在线留言
GO语言面向对象1:结构体struct详解_Go语言_网络人

GO语言面向对象1:结构体struct详解

Kwok 发表于:2020-10-03 14:03:51 点击:50 评论: 0

GO语言的面向对象与传统的java、C#是不一样的。GO里面没有CLASS类的概念。GO语言里使用了结构体替代了class,使用首字母大写来公开对象与方法。GOlang支持面向对象的特性,但并不是纯粹的面向对象语言。GO去掉了传统OOP语言的继承(extends)、方法重载、构造和析构(destructor)函数、隐藏this指针等关键字

GO语言仍然有编程的继承、封闭和多态的特性,但GO使用了更优雅的接口方式来关联降低耦合性,使用更灵活,GO语言在面向接口编程是非常重要的特性!

一、结构体语法

结构体就是GO语言里的OOP(面向对象)的Object(对象),可以用于数据的统一管理,例如一个学生有不同的属性,姓名(string),年龄(int),学号(string/int),住址(string)等信息,每个学习都需要定义相同变量来管理的话会非常不方便,这里的学习就是对象,他们都有大部分相同的的属性。所以我们可以使用结构体来管理:

type Student struct {
	Name   string //学生对象的名字
	Age    int    //学生对象的年龄
	StuNum string //学生对象的学号
}
func main() {
	var jack Student //定义一个jack的学生对象
	jack.Name = "杰克"
	jack.Age = 18
	jack.StuNum = "202001007"
	fmt.Println(jack) //{杰克 18 202001007}
	//按字段顺序写入值
	marke := Student{"马克", 19, "202001008"}
	fmt.Println(marke) //{马克 19 202001008}
	//指定字段后可以不按顺序写
	marley := Student{Age: 19, StuNum: "202001009", Name: "马利"}
	fmt.Println(marley) //{马利 19 202001009}
}

如果有数据库编程经验的小伙伴可以看出来type Student struct就是建立了一张Student的数据表,里面有3个字段分别是名字(string),年龄(int),学号(string),下面的var jack Student就是把jack入库。

结构构是自定义的数据类型集合,代表某一类对象的属性,var jack Student是结构体变量(实例)是具体的、实际的、代表了一个具体的对象。

下面看一下结构体在内存里的布局方式便于我们更好的去理解结构体类型。

GO语言面向对象1结构体struct详解

结构体里面的数据类型可以是多种的:除了常用的string、int、float32/64,还可以是:数组、map、切片、其它结构体(继承)指针等。使用切片和map的时候需要遵循先make后使用原则。

 

二、结构体的几种定义形式:

除了上面定义的方式我们还可以使用以下几种来定义一个结构体:

type Student struct {
	Name, StuNum,Address string//批量定义3个string类型
}

接着上面的代码展示一下各种字段(属性)和继承:

type miniStudent struct {
	stu Student                      //继承Student 的所有属性
	Age        int               //年龄
	Scores     [2]float32        //数组保存语文和数学的考试成绩
	TopPtr     *int              //int类型的指针,用于排名
	SliceIndex []int             //int类型的切片,定义学生各项指数,使用的时候需要make
	Other      map[string]string //定义一个map用于保存其它额外信息,使用的时候需要make
}

func main() {
	var top = 21//班级排名
	var marley miniStudent
	marley.Name = "张三"//从Student继承的字段(属性)
	marley.Student.Address = "北京-海淀区-中关村-清华大学-门口保安室"//从Student继承并显式声明
	marley.StuNum = "202001006" //从Student继承,隐匿声明,由编译器自动补全
	marley.Age = 10 //年龄
	marley.Scores = [2]float32{89.5, 95} //语文、数学的成绩
	marley.TopPtr = &top //将排名的内存地址传给TopPtr
	marley.SliceIndex = []int{10,20,30,40}//切片可以自动make,也可以手动先make再使用
	marley.Other = map[string]string{"father":"张大牛","mather":"李金花"}//map可以自动make,也可以手动先make再使用
	fmt.Println(marley)//{{张三 202001006 北京-海淀区-中关村-清华大学-门口保安室} 10 [89.5 95] 0xc00002c008 [10 20 30 40] map[father:张大牛 mather:李金花]}

}

结构体创建实体的更多方式集合:

type Vertex struct {
	X, Y int
}
func main() {
	var a Vertex //标准方式定义一个结构体实体
	a.X = 1 //标准方式给结构体X赋int值
	a.Y = 2 //标准方式给结构体Y赋int值
	fmt.Println("a的值:", a) //a的值: {1 2}

	var b = Vertex{3, 4}   //定义同时赋值X=3,Y=4
	fmt.Println("b的值:", b) //b的值: {3 4}

	var b2 = Vertex{Y: 3, X: 4} //不按X/Y的顺序
	fmt.Println("b2的值:", b2)    //b2的值: {4 3}

	c := Vertex{5, 6}      //使用推导方式创建结构体
	fmt.Println("c的值:", c) //c的值: {5 6}

	c2 := Vertex{Y:5, X:6}      //不按X/Y顺序使用推导方式创建结构体
	fmt.Println("c2的值:", c2) //c2的值: {6 5}

	//指针传递的标准写法
	var p *Vertex = new(Vertex) //引用地址传递。v := new(T) 和 v := &T{}
	(*p).X = 7
	(*p).Y = 8
	fmt.Println("p的值:", p) //p的值: &{7 8}

	//指针定义项目中实际写法(简化处理)
	p.X = 70
	p.Y = 80
	fmt.Println("*p的值:", *p) //*p的值: {70 80}
	
	//省略new关键字
	var p2 *Vertex = &Vertex{90,100}//也是指针引用传值,编译器会自动加上(*p2)
	fmt.Println("*p2的值:", *p2) //*p2的值: {90 100}

}

使用别名继承:

type miniStudent struct {
	stu Student           //继承Student 的所有属性并命名为stu
}

func main() {	
	var marley miniStudent
	marley.stu.Name = "张三" //使用了别名继承,就必须要完整输入stu.属性
	marley.Address = "北京-海淀区-中关村-清华大学-门口保安室"//这样定义会报错:marley.Address undefined (type miniStudent has no field or method Address)
	marley.Student.StuNum = "202001006" //marley.Student undefined (type miniStudent has no field or method Student)
}

 结构体指针:

type Vertex struct {
	X int
	Y int
}
func main() {
	v := Vertex{1, 2}
	p := &v//将v的内存地址交给p,所以v的值 应该是*P
	p.X = 111//编译器解析为:(*p).X = 111
	fmt.Println(v)//{111,2}
}

 实际开发中我们经常会使用指针来传递数据,这样效率更高,内存占用更少,GO语言支持隐式间接引用,直接写 p.X 就可以,但我们要理解为(*p).X。 

type Vertex struct {
	X, Y int
}

var (
	v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
	v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
	v3 = Vertex{}      // X:0 Y:0
	p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

func main() {
	fmt.Println(v1, p, v2, v3)  //{1 2} &{1 2} {1 0} {0 0}
	fmt.Println(v1, *p, v2, v3) //{1 2} {1 2} {1 0} {0 0}
}

结构体是一个值类型,默认按值拷贝传递,除非使用结构体对象指针传递才是引用类型。

var a = Vertex{3, 4}   //定义同时赋值
var b Vertex = a       //将a的值拷贝一份给b
fmt.Println("b的值:", b) //b的值: {3 4}
b.X = 100              //这里只能修改b的值a的值不会变。
fmt.Println("b的值:", b) //b的值: {100 4}
fmt.Println("a的值:", a) //a的值: {3 4}

//结构体引用传递
var c *Vertex = &a     //将c的指针值指向a的内存地址
(*c).Y = 90            //通过指针(*c)修改a的Y值
c.X = 99               //通过指针c修改a的X值,编译器将把c处理成(*c)
fmt.Println("a的值:", a) //a的值: {99 90}

上面代码里(*c).Y不能写为*c.Y,因为.的运算符要高于*,这样写会报错~结构体里的值在内存里是连续分布的,所以在查找运算中只需要加8个字节就能找到下一个,运算速度会非常快,但是结构体里的值是指针的情况下内存地址将指向实际地址,所以指针的值不会连续。

结构体是自定义的类型,和其它类型进行转换时需要有完全相同的字段(属性一样,名字、个数和类型)才可以。

由于结构体定义的包,经常会被其它包引用,属性首字母一般都是大写的,在接口开发的时候,比如json传递的时候前端要求改为全小写,我们可以使用结构体的tag标签:

import (
	"encoding/json"
	"fmt"
)

//定义一个Student和结构体。里面有姓名和年龄2个字段
type Student struct {
	Name string `json:"myname"`
	Age  int    `json:"myage"`
}

func main() {
	lishi := Student{"李四", 20}
	v, err := json.Marshal(lishi) // json.Marshal方法,json序列化,返回值和报错信息
	if err != nil {
		fmt.Println(err) //发生错误才会输出
	}
	fmt.Println(string(v)) // []byte转string, json
}

三、结构体绑定方法

Go 没有类。不过你可以为结构体类型定义方法。定义绑定方法和函数极为相似,方法比函数多一个绑定参数:

//定义一个Student和结构体。里面有姓名和年龄2个字段
type Student struct {
	Name string
	Age  int
}

//把Info()方法绑定给Student结构体
func (v Student) Info() {
	fmt.Println(v.Name, "的年龄是:", v.Age, "岁") //当调用这个方法的时候打印这句话
}
//下面定义一个函数,接收结构体参数
func Info(v Student) {
	fmt.Println(v.Name, "的年龄是:", v.Age, "岁") //当调用这个方法的时候打印这句话
}
func main() {
	a := Student{"张三", 15}
	a.Info() //通过Student结构体方法调用结果:张三的年龄是:15岁
	b := Student{"李四", 18}
	Info(b) //使用Info函数调用结果:李四 的年龄是: 18 岁
}

通过上面的代码可以看到,定义函数和方法区别在于方法多一个绑定,其它的实现是一样的。方法的传参也是值拷贝,在方法里修改并不会改变对象的值,除非传指针进行。在实际开发中,我们更多会使用传指针进去速度更快!

四、使用工厂模式实现构造方法

GO语文没有构造函数,通过可以使用工厂模式来解决这个问题。假设结构体声明是以小写字母开头,在其它包里就无法访问到这个结构体里的字段。工厂模式就可以实现跨包创建结构体实例。

type student struct {
	Name string
	Age  int
}

//把要初始化的参数放入NewStudent这个函数里,然后返回一个*student的指针值。
func NewStudent(n string, a int) *student {
	return &student{
		Name: n,
		Age:  a,
	}
}

//给student绑定一个私有方法
func (s student) info() {
	fmt.Println(s.Name, "的年龄是:", s.Age) //打印出对象的信息

}

//绑定私有方法,可修改名字和年龄
func (s *student) changeName(n string, a int) {
	*s = student{n, a} //这是标准写法,*s可以简写为s
}
func main() {
	var stu = NewStudent("张三", 10)  //使用工厂模式的函数定义student结构体。
	fmt.Println(stu)                //打印:&{张三 10}
	fmt.Println(*stu)               //打印:{张三 10}
	fmt.Printf("stu的类型是:%Tn", stu) //打印:stu的类型是:*main.student
	stu.info()                      //打印:张三 的年龄是: 10
	stu.changeName("李四", 18)        //调用方法改名字
	stu.info()                      //再次打印结果为:李四 的年龄是: 18
}

关于抽象的理解:这是一种分析方法,我们在定义一个结构体的时候,实际上就是把一类事物的共有属性(字段)和行为(方法)提取出来,开成一个物理模型(结构体)。这种研究问题的方法称为抽象。我们可以把生活中的是事物代码化的思考过程就是抽象,如:定义一个“人”的结构体,属性(字段)有姓名、年龄、身高.....行为(方法)有走路、吃饭、说话、工作、学习等....刚才我就以抽象出来1个的人结构体^_^

五、面向对象的三大特性

GO语言对字段的封装:将结构体里的字段(属性)首字母小写(不能导出,其它包就不能直接使用,类似private),然后像上面代码一样使用工厂模式的函数(首字母大写)类似构造函数对属性判断和赋值操作。GO语言中并没有特别强调封装,因为本身就对面向对象做了简化。

type Student struct {
	Name string
	age  int //设为不公开
}

//使用SetAge封闭起来,满足条件才会把变量赋值给age
func (s *Student) SetAge(a int) {
	if a >= 6 && a <= 22 {
		s.age = a
	} else {
		fmt.Println("你的年龄不适合上学哦~")
	}

}
//使用GetAge里的条件判断才能读取关于age的信息
func (s *Student) GetAge() {
	if s.age < 6 {
		fmt.Println(s.Name, "你才", s.age, "岁,快乐的玩耍吧~")
	} else if s.age > 22 {
		fmt.Println(s.Name, "好好上班吧~你都", s.age, "岁了")
	} else {
		fmt.Println(s.Name, "好好学习,天天向上,你", s.age, "岁了")
	}
}
func main() {
	var stu Student
	stu.Name = "张三"
	stu.SetAge(20)
	stu.GetAge()	//张三 好好学习,天天向上,你 20 岁了
}

 GO语言的继承,主要可以解决代码复用,当多个结构存存在相同的属性(字段)和方法时,可以把有共同特性的结构分离出来单独定义后给其它结构体使用,本文上部的“使用别名继承”已做为了属性的继承,下面讲讲实际使用中的方法继承和使用。

假设,学生可以分为小学生、中学生、大学生,他们都有部分共同的属性,如:年龄、身高、姓名等,我们可以定义一个共同属性的结构体,然后再定义3个不时期的学生结构来继承这这共同属性和方法。

//定义一个共同属性的结构体
type Student struct {
	Name string
	Age  int
}

//小学生特有属性
type MiniStudent struct {
	Student        //从Student继承所有属性
	StuPinyin bool //有没有学会拼音
}

//中学生特有属性
type MidStudent struct {
	Student       //从Student继承所有属性
	InSchool bool //是否住校
}

//大学生特有属性
type ColStudent struct {
	Student      //从Student继承所有属性
	Inlove  bool //是否在谈恋爱
}

 结构体可以使用嵌套匿名结构体所有的字段和方法,首字母大写、小写的字段和方法都可以使用。匿名结构体字段访问可以简化,当结构体和匿名结构体有相同字段或者方法时,编译器采用就近访问原则,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分,下面的代码演示:

//接上面的代码
var stu MiniStudent
stu.Name = "张三"      //编辑器会解析为:stu.Student.Name,因为这个字段继承于Student,这里演示简写
stu.Student.Age = 16 //这里是正常写法,假设MiniStudent也定义了Age字段,这里指定的是Student里的字段值。
stu.StuPinyin = true
fmt.Println(stu) //{{张三 16} true}

如果继承的2或者多个结构体(多重继承),有相同的字段或者方法(同时当前结构体无此方法或者字段),在访问时就必须明确指定继承的结构体名字不能使用简写的方式,否则编译时就会报错。因为编译不知道应该使用哪个字段或者方法。为了保证代码的简洁性,建议在实际项目开发中,尽量不用或者少用多重继承。

在GO语言中基本数据类型(float32、int、string等)也可以做为结构体的字段使用,但不能重复哦~

type Student struct {
	string //我是基本数据string
	int    //我是基本数据int
}

func main() {
	var stu Student
	stu.string = "张三" //这里传入string值
	stu.int = 16      //根据数据类型传值
	fmt.Println(stu)  //{张三 16}
}

 六、匿名结构体

匿名结构体没有类型名称,只有字段和类型定义,无须通过type关键字定义就可以直接使用。匿名结构体的初始化写法由结构体定义和键值对初始化两部分组成。注意,匿名结构体,必须要同时初始化,不能仅仅定义匿名结构体。 当需要使用一个临时结构体类型时,可以使用匿名结构体。

//定义一个匿名结构体
var stu = struct {
		Name string
		Age  int
	}{
		Name: "张三",
		Age:  16,
	}
fmt.Println(stu) //{张三 16}
除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/78
标签:GO面向对象结构体Kwok最后编辑于:2020-10-04 17:04:41
0
感谢打赏!

《GO语言面向对象1:结构体struct详解》的网友评论(0)

本站推荐阅读

热门点击文章