前言
emmm为什么要写前言呢,因为这以及不知道第几次开始学面向对象了,哎,面向了这么多次还是没对象怎么办()))))
概述
Go也支持面向对象编程,但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言,和Java C++相比做了很多简化,所以我们说Golang支持面向对象编程特性是比较准确的。
Golang里面没有类,Go语言的结构体和其他语言的类有同等的地位,可以理解为Golang是基于struct来实现OOP特性
Golang面向对象编程非常简洁,去掉了传统OOP语言的方法重载、构造函数和析构函数,隐藏的this指针等等
Golang仍然具有面向对象编程的封装、继承、多态 的特性,只是实现方式和其他语言不同,比如:继承中,Golang没有extends关键是,而是通过匿名字段来实现的。
Golang面向对象很优雅,OOP本身就是语言系统的一部分,通过接口关联,耦合性低,也非常灵活,也就是说在Golang中面向接口编程是一个很重要的特性。
结构体
结构体是一个“模板”,用于定义一类事物的属性(字段)和行为(方法);而结构体变量(实例)是根据这个模板创建的具体对象(实例)。
快速入门
我们先来个案例看一看,比如说《三角洲行动》中的枪械众多,我们要做一个枪械数据整理系统,里面要存储ttdr爱用的一些枪械,当ttdr输入枪械名称后,能够查看枪械的基础属性。
package main
import "fmt"
type Guns struct {
Name string
Dps int
RoF int
}
func main() {
//var M7 Guns
//M7.Name = "M7"
//M7.Dps = 433
//M7.RoF = 649
M7 := Guns{Name: "M7", Dps: 433, RoF: 649}
fmt.Printf("枪械信息如下:\n枪械名称:%v\n枪械Dps:%v\n"+
"枪械RoF:%v\n", M7.Name, M7.Dps, M7.RoF)
}

结构体变量在内存中的布局
首先我们要明白一个点是,结构体式值类型而不是引用类型,M7这个结构体变量指向的直接是这个结构体的值,而不是地址。
地址(偏移) 内存内容(字段) 说明
──────────────────────────────────────────────────────
0x00 ~ 0x0F │ [ Name: string ] │ 16 字节:ptr + len
│ (8B ptr → "M7") │
│ (8B len = 2) │
──────────────────────────────────────────────────────
0x10 ~ 0x17 │ [ Dps: int = 433 ] │ 8 字节整数
──────────────────────────────────────────────────────
0x18 ~ 0x1F │ [ RoF: int = 649 ] │ 8 字节整数
──────────────────────────────────────────────────────
总大小:32 字节(0x00 ~ 0x1F)
fmt.Printf("结构体大小: %d 字节\n", unsafe.Sizeof(M7))
fmt.Printf("Name 偏移: %d\n", unsafe.Offsetof(M7.Name))
fmt.Printf("Dps 偏移: %d\n", unsafe.Offsetof(M7.Dps))
fmt.Printf("RoF 偏移: %d\n", unsafe.Offsetof(M7.RoF))

如何声明以及使用陷阱
我们上面已经用过了,还是话不多说,直接上基本语法
type 结构体名称 struct {
字段名 类型
}
字段也可以叫属性,是结构体的一个组成部分,一般是基本数据类型、数组,也可以是引用类型。
在创建结构体变量后,如果没有给字段赋值,都对应一个零值;创建的不同的结构体变量的字段是独立的,互不影响。
注意:如果有slice还要map类型,使用的时候一定要先make
创建结构体变量和访问结构体字段
1.直接创建
var gun Guns
2.方式2
var gun Guns = Guns{}
//或者
As := Guns{
Name:"Asval",
Dps:470,
RoF: 972,
}
fmt.Printf("枪械信息如下:\n枪械名称:%v\n枪械Dps:%v\n"+
"枪械RoF:%v\n", As.Name, As.Dps, As.RoF)

3.方式三:用new返回一个指针
var gun *Guns = new(Gun)
//或者
K4 := new(Guns)
(*K4).Name = "K416"
(*K4).Dps = 454
(*K4).RoF = 880
fmt.Printf("枪械信息如下:\n枪械名称:%v\n枪械Dps:%v\n"+
"枪械RoF:%v\n", K4.Name, K4.Dps, K4.RoF)

虽然但是,感觉上面写的好麻烦,当然为了简洁,我们也可以直接写成K4.Name,但这样似乎又不太对劲,K4这里就是个指针,为什么可以直接这么写,因为Go的设计者为了程序猿使用方便,在底层做了优化,可以直接用。
4.方法四:用取地址符号返回指针
var Aug *Guns = &Guns{
Name: "Aug",
Dps: 328,
RoF: 679,
}
fmt.Printf("枪械信息如下:\n枪械名称:%v\n枪械Dps:%v\n"+
"枪械RoF:%v\n", Aug.Name, Aug.Dps, Aug.RoF)

注意事项和使用细节
1.结构体是用户单独定义的类型,和其他类型及逆行转换的时候要有完全相同的字段(名字、个数、类型)
/*注意这里进行转换的时候,要加一个强转,不能直接互相赋值*/
type A struct{
Sum int
}
type B struct{
Sumint
}
fun main(){
var a A
var b B
a = A(b)
}
2.结构体可以重新定义,(相当于取别名),Golang认为是新的数据类型,但是互相之间可以强转
type guns Guns
3.struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的场景就是序列化(变量序列化成字串,现在这个字串常用的格式是json)和反序列化
我们来看一个场景,来深入理解下这个问题,json处理后的字段也是首字母大写,但是如果将json后的字串返回给其他程序,比如jquery,php那么可能他们不习惯这个命名方式,那我们就可以把字段的首字母改成小写,但是这样别的包(或者是jquery,php)就不能返回这个结构体,这样显然是行不通的,所以就引入了tag标签来解决这个问题。
我们先来学会序列化一个结构体,用json.Marshal方法
jsonM7, err := json.Marshal(M7)
if err != nil {
fmt.Println("json处理错误", err)
}
fmt.Println(jsonM7)

我们这里获得的是一个[]byte,我们再给他转成字符串
fmt.Println(string(jsonM7))

那么我们来学会加tag,这里先不讲原理,我们后面在讲,这里知道使用的反射机制即可
type Guns struct {
Name string `json:"name"`
Dps int `json:"dps"`
RoF int `json:"roF"`
}
我们再来打印一下啊

方法
结构体出了有一些字段外,结构体还可以进行一些行为,比如Guns可以射击,射击后减少血量,这个时候就要用方法才能完成。
Golang中的方法是作用在指定的数据类型上的,因此自定义类型都可以有方法,不仅仅是struct。
我们先来个例子尝尝咸淡
type Guns struct {
Name string `json:"name"`
Dps int `json:"dps"`
RoF int `json:"roF"`
}
func(gun Guns) test{
fmt.Println(gun.Name)
}
//结构体Guns有一个方法,方法名为test
/*(gun Guns)表示test方法和Guns结构体绑定*/
func main {
var M7 Guns
M7.test()
}
这里的M7.test相当于传了一个M7的副本进函数,如果在函数内对结构体变量进行操作,不会更改原本结构体变量的值。
我们来验证一下
type Guns struct {
Name string `json:"name"`
Dps int `json:"dps"`
RoF int `json:"roF"`
}
func (gun Guns) change() {
gun.Name = "AKM"
fmt.Println("change中:",gun.Name)
}
//结构体Guns有一个方法,方法名为test
/*(gun Guns)表示test方法和Guns结构体绑定*/
func main {
var M7 Guns
M7.Name = "M7"
M7.change()
fmt.Println("change运行后:", M7.Name)
}

方法的调用和传参机制
方法的调用和传参机制和函数基本一致,不一样的地方是,方法调用时会将调用方法的变量当作参数传给方法,可以获得结构体变量的所有字段,上面的案例我们也能够体会到。
注意事项和细节讨论
1.如果希望在方法中修改结构体变量的值,可以通过结构体指针的方式来处理
2.方法的访问范围控制和规则和函数一样,方法名首字母小写,只能在本包访问,方法首字母大写可以在本包和其他包访问
3.如果一个类型实现了String()这个放啊,那么fmt.Println默认会调用这个变量String()进行输出
type Guns struct {
Name string `json:"name"`
Dps int `json:"dps"`
RoF int `json:"roF"`
}
func (gun Guns) String() string {
return fmt.Sprintf("枪械: %s, Dps: %d, RoF: %d", gun.Name, gun.Dps, gun.RoF)
}
func main {
var M7 Guns
M7.Name = "M7"
M7.Dps = 433
M7.RoF = 649
fmt.Println(" fmt.Println 自动调用String输出:")
fmt.Println(M7)
}
练习
要练习吗?跟函数大差不差
结构体与函数的区别
上面讲了一些这里不做赘述,讲点不一样的。
对于普通函数,接收者为值类型时,不能将指针类型的变量直接传递,反之亦然。
对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。
func main() {
person := Person{Name: "李四", Age: 18}
ptr := &person
/*值类型调用方法(自动转为指针)*/
person.Introduce()
/*指针类型调用方法(自动解引用)*/
ptr.Introduce()
/*修改数据,必须用指针接收者*/
ptr.GrowOld()
/* person.GrowOld() 编译错误:不能修改值副本*/
/*但是,如果方法是值接收者,可以被指针调用*/
ptr.Introduce() // 即使是值接收者,也能用指针调用
}
| 方法接收者 | 是否可由值调用 | 是否可由指针调用 | 是否能修改原数据 |
|---|---|---|---|
Person(值) | ✅ 是 | ✅ 是(自动解引用) | ❌ 否(操作的是副本) |
*Person(指针) | ❌ 否 | ✅ 是 | ✅ 是(修改原对象) |
工厂模式
Golang中没有构造函数,通常可以使用工厂模式来解决这个问题
需求:
我们在model包中声明了一个结构体,但是这个结构体首字母是小写的,这样我不能在其他包里面创建这个实例,我该怎么办?有点脱裤子放屁?那我们换个说法:
在实际开发中,我们想隐藏实现细节,又不想让别人直接 new?比如你想:
- 控制对象的创建流程(比如初始化某些字段)
- 做一些校验(如年龄不能小于0)
- 或者将来扩展更多逻辑(比如日志、序列化等)
我们用工厂模式来实现跨包创建结构体实例。
package model
type character struct {
Name string
Star int
}
func NewCharacter(n string, s int) *character {
return &character{
Name: n,
Star: s,
}
}
package main
import (
"fmt"
"go_code/project01/pointerdemo/factory/model"
)
func main() {
//var chara = model.character{
// Name: "STELLE",
// Star: 5,
//}
/*通过工厂模式来接解决*/
var chara = model.NewCharacter("STELLE", 5)
fmt.Println(*chara)
}

说白了就是公有方法调用私有字段。。。。
正规点说就是,不要直接暴露 struct 的创建,而是提供一个 NewXXX() 函数。









