Go语言基础之结构体(面向对象编程上)

1 自定义类型和类型别名

1.1 自定义类型

Go语言中可以基于一些基本的数据类型,使用type关键字定义自定义类型的数据 。

自定义类型定义了一个全新的类型,该新类型具有基本数据类型的特性。自定义类型定义的方法如下:

type TypeName Type

//将 NewType定义为int 类型
type NewType int

NewType是一个新的类型,其具有int的特性。

1.2 类型别名

类型别名是Go1.9版本添加的新功能。利用类型别名的功能,可以给一些基本的数据类型定义一些让读者见名知意的名字,从而提高代码的可读性。类型别名定义的方法如下:

type TypeAlias = Type

Go语言中的runebyte就是类型别名,它们的定义如下:

type byte = uint8
type rune = int32

1.3 自定义类型和类型别名的区别

自定义类型:自定义类型定义了一个全新的类型,其继承了基本类型的所有特性,并且可以实现新类型的特有的一些方法。

类型别名:只存在代码编写的过程,代码编译以后根本不存在这个类型别名。其作用用来提高代码的可读性。

如下代码,体现了二者的区别:

//自定义类型
type NewInt int

//类型别名
type MyInt = int

func main(){
    var a NewInt
    var b MyInt
    var c int
    // c = a  //?  可以使用强制类型转换c = int(a)
    c = b  //c和b是同一类型
    
    fmt.Println("type of a:%T\n", a)  //type of a:main.NewInt
    fmt.Println("type of b:%T\n", b)  //type of b:int
}

2 结构体

在Go语言中可以使用基本数据类型表示一些事物的属性,但是如果想表达一个事物的全部或部分属性,比如说一个学生(学号、姓名、年龄、班级等),这时单一的基本数据类型就不能够满足需求。

Go语言中提供了一种自定义数据类型,可以将多个基本数据类型或引用类型封装在一起,这种数据类型叫struct结构体。Go语言也是通过struct来实现面向对象的。

2.1 Golang语言面向对象编程说明

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

2.2 快速入门

// 创建一个结构体类型的student
type student struct {
	name string
	age int
	gender string
	hobby []string
}

func main() {
	// 创建一个student实例
	var viktor = student{
		name:   "viktor",
		age:    24,
		gender: "男",
		hobby:  []string{"乒乓球", "羽毛球"},
	}

	fmt.Println(viktor)			//  {viktor 24 男 [乒乓球 羽毛球]}
    // 分别取出viktor实例中的每个字段
	fmt.Println(viktor.name)	// viktor
	fmt.Println(viktor.age)		// 24
	fmt.Println(viktor.gender)	// 男
	fmt.Println(viktor.hobby)	// [乒乓球 羽毛球]
}

2.3 如何声明结构体

基本语法:

type StructName struct {
    field1 type
    field2 type
}

示例,声明一个学生的结构体Student

type Student struct {
    Name string
    Age int
    Score float32
}

2.4 字段/属性

struct中封装了一些基本数据类型的变量,我们称之为结构体字段或者是该结构体的属性。

字段是结构体的一个组成部分,一般是基本数据类型、数组,也可以是引用类型,甚至是struct(嵌套结构体)等。

注意事项和细节说明:

  • 字段声明语法同变量;

  • 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值):布尔类型是false,数值是0,字符串是"",数组的默认值和它的元素类型相关,比如 score [3]int则为[0, 0, 0],指针、slice和map的零值都是nil,即还没有分配空间。

type Person struct {
    Name string 
    Age int
    Scores [5]float64
    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")
    }
    
    p1.slice = make([]int, 10)
    p1.slice[0] = 100
    
    p1.map1 = make(map[string]string)
    p1.map1["key1"] = "tom"
    fmt.Println(p1)
}
  • 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型

2.5 结构体的实例化

可以这样理解,声明一个结构体类似创造一个模具,如果想要真正的描述一个事物,那么就得使用这个模具来制造一个事物,这个制造事物的过程称为 创建结构体变量或者结构体实例化。结构体的实例化有四种方式:

  • 方式1:直接声明
type Person struct {
    Name string 
    Age int
}

func main() {
    //定义结构体变量
    var p1 Person
    fmt.Println(p1)
}
  • 方式2:{}
//使用值列表初始化
p2 := Person{"tom", 20}
fmt.Println(p2)
  • 方式3:使用new关键字

struct是值类型,那么就可以使用new关键字定义一个结构体指针:

var p3 *Person = new(Person)
(*p3).Name = "smith"
p3.Name = "john"

(*p3).Age = 20
p3.Age = 30
fmt.Println(p3)
  • 方式4:使用&,定义一个结构体指针
//使用键值对初始化
var person *Person = &Person{
    Name : "tom", 
    Age : 19,
}

//也可以通过字段访问的形式进行赋值
(*p3).Name = "scott"
p3.Name = "scott~"

(*p3).Age = 20
p3.Age = 30
fmt.Println(p3)

说明:

  • 第三种和第四种方式返回的是结构体指针;
  • 在结构体初始化是,必须初始化结构体的所有字段;初始值的填充顺序必须与字段在结构体中的声明顺序一致;值列表初始化方式和键值初始化方式不能混用;
  • 结构体指针访问字段的标准方式是:(*结构体指针).字段名
  • 由于Go语言中的指针不支持偏移和运算,语句go编译器底层对person.Name做了转化(*person).Name

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

  • 结构体的所有字段在内存中是连续的
type Point struct {
    x, 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", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)
    
    //r2有两个*Point类型,这两个*Point类型的本身地址也是连续的,但是其指向的地址不一定是连续的
    r2 := Rect(&Point{10,20}, Point{30,40})
    
    fmt.Printf("r2.leftUp 本身地址=%p r2.rightDown 本身地址=%p", &r2.leftUp, &r2.rightDown)
    
    fmt.Printf("r2.leftUp 指向地址=%p r2.rightDown 指向地址=%p", r2.leftUp, r2.rightDown)
}
  • 结构体是用户单独定义的类型,和其他类型进行转换时需要有完全相同的字段(名字、个数和类型)
  • struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景:序列化和反序列化。
type Monster struct {
    Name string `json:"name"`
    Age int `json:"age"`
    Skill string `json:"skill"`
}

func main() {
    //创建一个Monster实例
    monster := Monster{"蜘蛛精", 200, "吐丝"}
    
    //将monster序列化
    jsonStr, err := json.Marshal(monster)
    if err != nil {
        fmt.Println("json 处理错误", err)
    }
    fmt.Println("jsonStr", string(jsonStr))
}

2.7 面试题

下面代码的执行结果?

type student struct {
	name string
	age  int
}

func main() {
	m := make(map[string]*student)
	stus := []student{
		{name: "李四", age: 18},
		{name: "张三", age: 23},
		{name: "李明", age: 9000},
	}

	for _, stu := range stus {
		m[stu.name] = &stu
	}
	for k, v := range m {
		fmt.Println(k, "=>", v.name)
	}
}

3 方法

方法是什么?在声明了一个结构体后,比如说Person结构体,那么这个人都有哪些功能,或者说都有什么能力,这些功能或者能力就是一个结构体的方法。

Golang中的方法是作用在指定的数据类型上(即,和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。如下示例:

//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
	fmt.Println("Hello, 我是一个int。")
}
func main() {
	var m1 MyInt
	m1.SayHello() //Hello, 我是一个int。
	m1 = 100
	fmt.Printf("%#v  %T\n", m1, m1) //100  main.MyInt
}

3.1 方法的声明和调用

方法的声明语法:

//声明一个自定义类型struct
type A struct {
    Num int
}

//声明A类型的方法
func (a A) test() {		//其中的(a A)表示test方法和A类型绑定
    fmt.Println(a.Num)
}

举例说明:

type Person struct {
    Name string
}

func (p Person) test() {
    fmt.Println("test() name=", p.Name)
}

func main() {
    var p Person	//实例化
    p.Name = "tom"
    p.test()	//调用方法
}
  • test方法和Person类型绑定;

  • test方法只能通过Person类型的变量来调用,不能直接调用,也不能使用其它类型变量来调用;

  • func (p Person) test() {...},其中p表示哪个Person变量调用,这个p就是它的副本,代表接收者。这点和函数传参非常相似,并且p可以有程序员任意指定;

3.2 方法的传参机制

在3.1中 提到,方法要和指定自定义类型的变量绑定,那个这个绑定方法的变量被称为接收者,而方法的传参机制被这个接收者的类型不同分为值类型的接收者和指针类型的接收者,下面分别来看这这两方式的传参机制:

3.2.1 值类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针特性,调用方法时修改接收者的任意成员变量,在方法接收后,修改都是有效的。例如为Person结构体添加一个SetAge方法,来修改实例中的年龄:

//Person 结构体
type Person struct {
	name string
	age  int8
}

// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
	p.age = newAge
}

func main() {
	p1 := NewPerson("小王子", 25)
	fmt.Println(p1.age) // 25
	p1.SetAge(30)
	fmt.Println(p1.age) // 30
}

3.2.2 指针类型的接收者

当方法和值类型接收者绑定是,Go语言会在代码运行时将接收者的值复制一份 。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
	p.age = newAge
}

func main() {
	p1 := NewPerson("张三", 25)
	p1.Dream()
	fmt.Println(p1.age) // 25
	p1.SetAge2(30) // (*p1).SetAge2(30)
	fmt.Println(p1.age) // 25
}

3.2.3 使用指针类型传参的时机

  • 需要修改接收者中的值;
  • 传参时拷贝代表比较大的大实例;
  • 保证形参实例和实参实例的一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

3.3 方法和函数区别

  • 声明方式不一样

    • 函数的声明方式:func 函数名(形参列表) (返回值列表) {...}
    • 方法的声明方式:func (变量 自定义类型) 函数名(形参列表) (返回值列表) {...}
  • 调用方式不一样

    • 函数的调用方式:函数名(实参列表)
    • 方法的调用方式:变量.方法名(实参列表)
  • 对于普通函数,接收者为值类型时,不能讲指针类型的数据直接传递,反之亦然;

type Person struct {
    Name string
}

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

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

func main() {
    var p = Person{"tom"}
    test01(p)
    test02(&p)
}
  • 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反之亦然;
func (p Person) test03() {
    p.Name = "jack"
    fmt.Println(p.Name)		//jack
}

func (p *Person) test03() {
    p.Name = "jerry"
    fmt.Println(p.Name)		//jerry
}

func main() {
    p := Person{"viktor"}
    p.test03()
    fmt.Println(p.Name)		// viktor
    
    (&p).test03()	//从形式上传入地址,但是本质扔然是值拷贝
    fmt.Println(p.Name)		// viktor
    
    (&p).test04()
    fmt.Println(p.Name)		// jerry
    p.test04()	//等价于(&p).test04(),从形式上是传入值类型,但是本事仍然是地址拷贝
}

对于方法来说,不管调用形式如何,真正决定是之拷贝还是地址拷贝,看这个方法是和哪种类型绑定,也就是接收者的类型是值类型还是指针类型。

4 工厂模式

Golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。

4.1 为何需要工厂模式

首选,在Golang语言中公有和私有变量这一说法。如果说一个包中的变量的首字母为小写,在其他包如果引入这个包,就不能访问这个变量;如果这个变量的变量名为大写字母,那么可以直接访问。

同样对于自定义的struct类型也是,而工厂模式,就是为了解决变量的首字母为小写的结构体能够被其它包引用的问题。

4.2 工厂模式的使用

//student.go属于model包
package model

//定义一个结构体
type student struct {
    Name string
    Score float64
}

func NewStudent(n string, s float64) *stuent {
    return &student{
        Name : n,
        Score : s,
    }
}

//mian.go中声明一个student实例,并初始化
func main() {
    var stu = model.NewStudent("viktor", 86.6)
    
    fmt.Println(*stu)
    fmt.Println("name=", stu.Name, "score=", stu.Score)
}

另外,如果结构体中的某个字段的首字母也为小写该如何访问?

//student.go属于model包
package model

//定义一个结构体
type student struct {
    Name string
    score float64
}

func NewStudent(n string, s float64) *stuent {
    return &student{
        Name : n,
        Score : s,
    }
}

func (s *student) GetScore() float64 {
    return s.score
}

//mian.go中声明一个student实例,并初始化
func main() {
    var stu = model.NewStudent("viktor", 86.8)
    
    fmt.Println(*stu)
    fmt.Println("name=", stu.Name, "score=", stu.GetScore())
}
posted @ 2020-02-25 15:31  Dabric  阅读(1004)  评论(0编辑  收藏  举报
TOP