结构体&方法

前言

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

结构体变量在内存中的布局

首先我们要明白一个点是,结构体式值类型而不是引用类型,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))
image-20251108224310096

如何声明以及使用陷阱

我们上面已经用过了,还是话不多说,直接上基本语法

 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)
image-20251110213218070

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)
image-20251110212319075

虽然但是,感觉上面写的好麻烦,当然为了简洁,我们也可以直接写成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)
image-20251110213348735

注意事项和使用细节

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后的字串返回给其他程序,比如jqueryphp那么可能他们不习惯这个命名方式,那我们就可以把字段的首字母改成小写,但是这样别的包(或者是jqueryphp)就不能返回这个结构体,这样显然是行不通的,所以就引入了tag标签来解决这个问题。

我们先来学会序列化一个结构体,用json.Marshal方法

     jsonM7, err := json.Marshal(M7)
  if err != nil {
  fmt.Println("json处理错误", err)
  }
  fmt.Println(jsonM7)
image-20251110220501128

我们这里获得的是一个[]byte,我们再给他转成字符串

     fmt.Println(string(jsonM7))
image-20251110220546386

那么我们来学会加tag,这里先不讲原理,我们后面在讲,这里知道使用的反射机制即可

 type Guns struct {
  Name string `json:"name"`
  Dps  int    `json:"dps"`
  RoF  int    `json:"roF"`
 }

我们再来打印一下啊

image-20251110220746404

方法

结构体出了有一些字段外,结构体还可以进行一些行为,比如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)
 }
image-20251111182836420

方法的调用和传参机制

方法的调用和传参机制和函数基本一致,不一样的地方是,方法调用时会将调用方法的变量当作参数传给方法,可以获得结构体变量的所有字段,上面的案例我们也能够体会到。

注意事项和细节讨论

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)
 }
 ​
image-20251111213502349

说白了就是公有方法调用私有字段。。。。

正规点说就是,不要直接暴露 struct 的创建,而是提供一个 NewXXX() 函数。

暂无评论

发送评论 编辑评论


				
上一篇
下一篇