Golang语言结构体(struct)面向对象编程基础篇

简介: 这篇文章是关于Go语言中结构体(struct)面向对象编程的基础教程,详细介绍了面向对象编程在Go语言中的应用、结构体的定义与初始化、方法定义、跨包实例化结构体以及结构体方法和普通函数的区别。

                                              作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。

一.面向对象的引入

1.Golang语言面向对象编程

- 1.Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言,所以我们说Golang支持面向对象编程特性是比较准确的;

- 2.Golang中没有类(Class)的概念,Go语言的结构体(struct)和其他编程语言的类有同等的地位,你可以理解Golang是基于struct来实现OOP特性的;

- 3.Golang面向对象编程非常简洁,却掉了传统OOP语言的方法重载,构造函数和析构函数,隐藏的this指针等等;

- 4.Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其他OOP语言不一样,比如继承Golang没有类似于extends关键字,而是通过匿名字段实现;

- 5.Golang中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

- 6.Golang中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型。

- 7.Golang自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。

2.结构体的引入

package main

import "fmt"

func main() {

    var (
        // 描述《仙逆》男主
        dongman string   = "仙逆"
        name    string   = "王林"
        gender  bool     = true
        hobby   []string = []string{"修炼", "李慕婉", "极镜"}

        // 描述《凡人修仙传》男主
        dongman2 string   = "凡人修仙传"
        name2    string   = "韩立"
        gender2  bool     = true
        hobby2   []string = []string{"炼丹", "阵法", "修炼"}
    )

    /*
        如果描述一个对象一直使用变量来处理,则会存在以下缺点:
            - 1.不利于数据的管理和维护;
            - 2.每个动漫的很多熟悉属于一个对象,用变量管理太分散;

        综上所述,我们就不得不学习结构体(struct):
            - 1.Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了。
            - 2.Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

    */
    fmt.Printf("《%s》name: [%s], gender: [%t], hobby: %v\n", dongman, name, gender, hobby)
    fmt.Printf("《%s》name: [%s], gender: [%t], hobby: %v\n", dongman2, name2, gender2, hobby2)

}

3.结构体定义

package main

import "fmt"

// DongMan 定义动漫结构体,将动漫中各个属性放入一个结构体中管理
type DongMan struct {

    // 变量名称大写外界可以访问这个属性
    Name string

    Age int

    Gender bool

    Hobby []string
}

func main() {
    // 创建动漫结构体的实例,对象,变量

    // 描述《仙逆》男主
    var xianni DongMan
    xianni.Name = "王林"
    xianni.Gender = true
    xianni.Hobby = []string{"修炼", "李慕婉", "极镜"}
    xianni.Age =400

    // 描述《凡人修仙传》男主
    var xiuxian DongMan
    xiuxian.Name = "韩立"
    xiuxian.Gender = true
    xiuxian.Hobby = []string{"炼丹", "阵法", "修炼"}
    xiuxian.Age = 217

    fmt.Printf("《仙逆》: %v\n", xianni)
    fmt.Printf("《凡人修仙传》: %v\n", xiuxian)
}

4.结构体五种初始化方式

package main

import "fmt"

// DongMan 定义动漫结构体,将动漫中各个属性放入一个结构体中管理
type DongMan struct {

    // 变量名称大写外界可以访问这个属性
    Name string

    Age int

    Gender bool

    Hobby []string
}

func main() {
    // 实例化方式一: 先定义再赋值
    var xianni DongMan
    xianni.Name = "王林"
    xianni.Gender = true
    xianni.Age = 400
    xianni.Hobby = []string{"修炼", "李慕婉", "极镜"}

    fmt.Println(xianni)

    // 实例化方式二: 按照字段定义顺序初始化赋值,缺点: 必须按照顺序填写且各字段不可省略,有局限性。
    var xiuxian DongMan = DongMan{
        "韩立", 217, true, []string{"炼丹", "阵法", "修炼"},
    }

    // 实例化方式三: 按照字段名称初始化赋值,即使用键值对初始化,跟顺序无关。
    var yongSheng DongMan = DongMan{
        Hobby:  []string{"修炼", "耍酷"},
        Gender: true,
        Name:   "方寒",
        Age:    20,
    }

    // 实例化方式四: 通过指针变量赋值, 通过new方法返回初始化变量,得到指针变量
    var yiNianYongHeng *DongMan = new(DongMan)
    // "yiNianYongHeng"是指针,其实指向的就是地址,应该给这个地址指向的对象字段赋值,其中"*"的作用就是根据地址取值
    (*yiNianYongHeng).Name = "白小纯"
    (*yiNianYongHeng).Age = 50
    // 为了符合程序员的编程习惯,go提供了简化的赋值方式,go编译器底层对"yiNianYongHeng.Gender"转换为"(*yiNianYongHeng).Gender",这种简写的方式我们称之为"语法糖"
    yiNianYongHeng.Gender = true 
    yiNianYongHeng.Hobby = []string{"炼丹", "修炼", "整蛊"}

    // 实例化方式五: 取对象地址值返回指针对象,取结构体的地址实例化。用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作
    var tunShiXingKong *DongMan = &DongMan{}

    (*tunShiXingKong).Name = "罗峰"
    (*tunShiXingKong).Age = 25
    tunShiXingKong.Gender = true
    tunShiXingKong.Hobby = []string{"修炼", "宇宙级精神念师"}

    fmt.Printf("《仙逆》: %v\n", xianni)
    fmt.Printf("《凡人修仙传》: %v\n", xiuxian)
    fmt.Printf("《永生》: %v\n", yongSheng)
    fmt.Printf("《一念永恒》: %v\n", *yiNianYongHeng)
    fmt.Printf("《吞噬星空》: %v\n", *tunShiXingKong)

}

5.结构体的互相转换

package main

import "fmt"

type Cat struct {
    Name string
    Age  uint8
}

type Dog struct {
    Name string
    Age  uint8
}

type BaGeQuan Dog

func main() {
    var (
        b BaGeQuan = BaGeQuan{"雨花剑传人神医", 19}
        c Cat      = Cat{"虹猫", 18}
        d Dog      = Dog{"逗逗", 17}
    )
    fmt.Printf("转换前: b = %v\n", b)
    fmt.Printf("转换前: c = %v\n", c)
    fmt.Printf("转换前: d = %v\n", d)

    // 结构体是用户单独定义的类型,和其他类型进行转换时需要有完全相同的字段(名字,个数和类型)
    c = Cat(d)

    // 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是互相间可以强转。
    d = Dog(b)

    fmt.Println("----- 分割线 -----")
    fmt.Printf("转换后: b = %v\n", b)
    fmt.Printf("转换后: c = %v\n", c)
    fmt.Printf("转换后: d = %v\n", d)
}

6.匿名结构体

package main

import (
    "fmt"
)

func main() {

    // 在定义一些临时数据结构等场景下还可以使用匿名结构体。
    var user struct {
        Name string
        Age  int
    }

    // 使用匿名结构体进行赋值
    user.Name = "Jason Yin"
    user.Age = 18

    fmt.Printf("%#v\n", user)
}

二.结构体方法定义

1.方法概述

Go语言中的方法(Method)是一种作用于特定类型变量的函数,这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者self。

方法的定义格式如下:
    func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
        函数体
    }

  相关参数说明:
    接收者变量:
      接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。
      例如,Person类型的接收者变量应该命名为p。

    接收者类型:
      接收者类型和参数类似,可以是指针类型和非指针类型。

    方法名、参数列表、返回参数:
      具体格式与函数定义相同。


方法使用注意事项:
    - 1.结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式;
    - 2.如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来传递;
    - 3.Golang中的方法作用在指定的数据类型上的,和指定的数据类型绑定,因此自定义类型,都可以有方法,而不仅仅是struct,比如int,float32等都可以有方法;
    - 4.方法的访问控制的规则,和函数一样,方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其他包访问;
    - 5.如果一个类型实现了String()这个方法,那么"fmt.Println"默认会调用这个变量的"String()"方法进行输出;


函数和方法的区别如下:
        - 1.绑定指定类型
        方法需要绑定指定数据类型,而函数不需要绑定数据类型。

        - 2.调用方式不一样
            方法的调用方式为: "对象.方法名称(实参列表)",函数的调用方式为: "函数名(实参列表)"。

        - 3.传递参数方式不一样
            对于方法来说,接受者为值类型,可以传入指针类型,接受值为指针类型,可以传入值类型。
            对于函数来说,参数类型对应是什么就要传入对应的类型。

2.方法是值拷贝传递方式

package main

import "fmt"

// DongMan 定义动漫结构体
type DongMan struct {
    Name   string
    Leader string
    Age    uint16
}

// 给DongMan结构体绑定SayHi方法,方法名称可以随意起
func (d DongMan) SayHi() {
    d.Age = 500
    fmt.Printf("大家好,我是《%s》的男主[%s],[%d]岁达到元婴期境界~\n", d.Name, d.Leader, d.Age)
}

func main() {
    var xianNi DongMan

    // 结构体对象传入SayHi方法中是值传递,和函数参数传递效果一致
    xianNi.Name = "仙逆"
    xianNi.Leader = "王林"
    xianNi.Age = 407

    // 结构体DongMan和SayHi方法绑定,调用SayHi方法必须靠指定类型DongMan,如果其他类型调用SayHi一定会报错。
    xianNi.SayHi()

    fmt.Printf("xianNi.Age =  %d\n", xianNi.Age)
}

3.指针类型的接收者

package main

import "fmt"

type DongMan struct {
    Name   string
    Leader string
    Age    uint16
}

func (d DongMan) SayHi() {
    fmt.Printf("in SayHi ... d的地址: %p, d存储的数据:%p\n", &d, &d)

    fmt.Printf("大家好,我是《%s》的男主[%s],[%d]岁达到元婴期境界~\n", d.Name, d.Leader, d.Age)
}

func (d *DongMan) SetAge(age uint16) {
    (*d).Age = age // 可简写为"d.Age = age"

    fmt.Printf("in SetAge ... d的地址: %p, d存储的数据:%p\n", &d, d)
}

func main() {
    var xianNi DongMan

    xianNi.Name = "仙逆"
    xianNi.Leader = "王林"
    xianNi.Age = 407

    fmt.Printf("in main ... xianNi的地址: %p\n", &xianNi)
    (&xianNi).SetAge(500) // 可简写为“xianNi.SetAge(500)”

    xianNi.SayHi()

    fmt.Printf("xianNi.Age =  %d\n", xianNi.Age)
}

4.为内置数据类型绑定方法

package main

import "fmt"

// 定义一个Integer类型,让其作为int的别名,之后就可以为其添加方法
type Integer int

func (i Integer) SayHi() {
    fmt.Printf(" i = %d\n", i)
}

func (i *Integer) SetInt(number int) {
    *i = Integer(number)
}

func main() {

    var x Integer = 100

    x.SayHi()
    x.SetInt(200)
    x.SayHi()
}

5.结构体的String()方法

package main

import "fmt"

type DongMan struct {
    Name   string
    Leader string
    Age    int
    Hobby  []string
}

// 建议大家定义String()作为输出结构体信息的方法,在会"fmt.Println"时自动调用哟~
func (d DongMan) String() string {

    return fmt.Sprintf("[%s]的男主是[%s],在[%d]岁时修炼到元婴", d.Name, d.Leader, d.Age)
}

func main() {

    var xiuxian DongMan
    xiuxian.Name = "《凡人修仙传》"
    xiuxian.Leader = "韩立"
    xiuxian.Hobby = []string{"炼丹", "阵法", "修炼"}
    xiuxian.Age = 217

    fmt.Println(xiuxian)

}

6.函数和方法的区别

package main

import "fmt"

type DongMan struct {
    Name string
}

// SetName 方法
func (d *DongMan) SetName(name string) {
    d.Name = name
}

// getName 方法
func (d DongMan) getName() {
    fmt.Printf("in method ... d.Name = %s\n", d.Name)
}

// getName 函数
func getName(d DongMan) {
    fmt.Printf("in function ... d.Name = %s\n", d.Name)
}

func main() {
    var yongSheng DongMan

    // 调用方法
    yongSheng.SetName("永生") // 但是不使用指针方式传递依旧可行,
    // (&yongSheng).SetName("一念永恒") // 正常应该使用指针方式传递

    // yongSheng.getName()
    (&yongSheng).getName()    // 虽然用指针类型调用,但是传递还是按照值传递的形式

    // 调用函数
    getName(yongSheng)
    // getName(&yongSheng) // 错误,函数不支持指针变量传递


    /*
    综上所述,我们总结函数和方法的区别如下:
        - 1.绑定指定类型
        方法需要绑定指定数据类型,而函数不需要绑定数据类型。

        - 2.调用方式不一样
            方法的调用方式为: "对象.方法名称(实参列表)",函数的调用方式为: "函数名(实参列表)"。

        - 3.传递参数方式不一样
            对于方法来说,接受者为值类型,可以传入指针类型,接受值为指针类型,可以传入值类型。
            对于函数来说,参数类型对应是什么就要传入对应的类型。
    */
}

7.什么时候应该使用指针类型接受者

- 1.需要修改接收者中的值;

- 2.接收者是拷贝代价比较大的大对象

- 3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

三.跨包创建结构体实例

1 首字母大写可以支持跨包

1.1 项目测试结构

如上图所示,本案例仅需要让main包倒入dongman包进行测试即可。

1.2 创建go.mod文件

yinzhengjie@bogon 10-struct-package % go mod init yinzhengjie-dongman
go: creating new go.mod: module yinzhengjie-dongman
yinzhengjie@bogon 10-struct-package % 
yinzhengjie@bogon 10-struct-package % ls
go.mod
yinzhengjie@bogon 10-struct-package % 
yinzhengjie@bogon 10-struct-package % cat go.mod 
module yinzhengjie-dongman

go 1.22.4
yinzhengjie@bogon 10-struct-package %

1.3 dongman.go

package dongman

import "fmt"

type DongMan struct {
    Name   string
    Leader string
    Age    int
    Hobby  []string
}

func (d DongMan) String() string {

    return fmt.Sprintf("[%s]的男主是[%s],爱好是: %v,在[%d]岁时修炼到元婴哟~", d.Name, d.Leader, d.Hobby, d.Age)
}

1.4 main.go

package main

import (
    "fmt"
    "yinzhengjie-dongman/dongman"
)

func main() {

    var xiuxian dongman.DongMan
    xiuxian.Name = "《凡人修仙传》"
    xiuxian.Leader = "韩立"
    xiuxian.Hobby = []string{"炼丹", "阵法", "修炼"}
    xiuxian.Age = 217

    fmt.Println(xiuxian)

}

1.5 测试代码

yinzhengjie@bogon 10-struct-package % go run main/main.go
[《凡人修仙传》]的男主是[韩立],爱好是: [炼丹 阵法 修炼],在[217]岁时修炼到元婴哟~
yinzhengjie@bogon 10-struct-package %

2 首字母小写基于工厂模式实现跨包

2.1 项目测试结构

如上图所示,本案例仅需要让main包倒入dongman包进行测试即可。

2.2 创建go.mod文件

yinzhengjie@bogon 11-struct-package-2 % source /etc/profile 
yinzhengjie@bogon 11-struct-package-2 % 
yinzhengjie@bogon 11-struct-package-2 % ls       
yinzhengjie@bogon 11-struct-package-2 % 
yinzhengjie@bogon 11-struct-package-2 % go mod init yinzhengjie-dongman
go: creating new go.mod: module yinzhengjie-dongman
yinzhengjie@bogon 11-struct-package-2 % 
yinzhengjie@bogon 11-struct-package-2 % ls
go.mod
yinzhengjie@bogon 11-struct-package-2 % 
yinzhengjie@bogon 11-struct-package-2 % cat go.mod 
module yinzhengjie-dongman

go 1.22.4
yinzhengjie@bogon 11-struct-package-2 %

2.3 dongman.go

package dongman

import "fmt"

type dongMan struct {
    Name   string
    Leader string
    Age    int
    Hobby  []string
}

func (d dongMan) String() string {

    return fmt.Sprintf("[%s]的男主是[%s],爱好是: %v,在[%d]岁时修炼到元婴哟~", d.Name, d.Leader, d.Hobby, d.Age)
}


// 工厂模式,调用该方法就会返回"dongMan"的指针类型对象哟~有点类似其他编程语言的"构造方法"
func NewDongMan(name,leader string,age int,hobby []string) *dongMan {
    return &dongMan{
        Name: name,
        Leader: leader,
        Age: age,
        Hobby: hobby,
    }
}

2.4 main.go

package main

import (
    "fmt"
    "yinzhengjie-dongman/dongman"
)

func main() {

    xianNi := dongman.NewDongMan("《仙逆》", "王林", 400, []string{"极镜", "阵法", "修炼"})

    fmt.Println(xianNi)

}

2.5 测试代码

yinzhengjie@bogon 11-struct-package-2 % go run main/main.go
[《仙逆》]的男主是[王林],爱好是: [极镜 阵法 修炼],在[400]岁时修炼到元婴哟~
yinzhengjie@bogon 11-struct-package-2 %
目录
相关文章
|
12天前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
43 4
Golang语言之管道channel快速入门篇
|
12天前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
40 3
Golang语言之gRPC程序设计示例
|
12天前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
36 4
|
12天前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
20 3
Golang语言之Prometheus的日志模块使用案例
|
12天前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
36 4
Golang语言文件操作快速入门篇
|
12天前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
35 3
|
12天前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
23 4
Golang语言goroutine协程篇
|
12天前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
16 3
Golang语言之函数(func)进阶篇
|
12天前
|
Go
Golang语言之函数(func)基础篇
这篇文章深入讲解了Golang语言中函数的定义和使用,包括函数的引入原因、使用细节、定义语法,并通过多个案例展示了如何定义不返回任何参数、返回一个或多个参数、返回值命名、可变参数的函数,同时探讨了函数默认值传递、指针传递、函数作为变量和参数、自定义数据类型以及返回值为切片类型的函数。
17 2
Golang语言之函数(func)基础篇
|
12天前
|
Go
Golang语言之映射(map)快速入门篇
这篇文章是关于Go语言中映射(map)的快速入门教程,涵盖了map的定义、创建方式、基本操作如增删改查、遍历、嵌套map的使用以及相关练习题。
21 5