Go 编程 | 连载 17 - 结构体方法

1,445 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

一、结构体方法

Go 不是面向对象编程的语言,没有类和对象的概念,结构体就类似于面向对象编程中的类,类有方法,结构体也有方法,但是结构体的方法是放在结构体外的。

结构体方法实现了面向对象中的 封装 特性,达到封装数据和封装方法的效果。

结构体方法的定义与普通函数的定义差别在于比普通函数多了一个 函数的接收者 的概念,也就是该方法要绑定的结构体。

// s 为函数接收者
func(s structName) funcName(para1, paraType) (retVal retType){
    // function body
}
func main() {

   tesla := Tesla{"Model 3", 298000.0}
   // 通过实例化的结构体调用方法
   tesla.printTeslaInfo()

}

type Tesla struct {
   Name string
   Price float64
}

// 定义结构体方法
func (t Tesla) printTeslaInfo(){
   fmt.Println("实例化 Tesla 结构体的 Name 属性值:", t.Name, ",价格为:", t.Price) 
}

执行上述代码,输出结果如下:

实例化 Tesla 结构体的 Name 属性值: Model 3 ,价格为: 298000

除了通过实例化结构体调用方法,也可以通过结构体直接调用方法,这种调用方式需要将实例化结构体作为参数

Tesla.printTeslaInfo(tesla)

再定义一个修改结构体属性的 set 方法

func main() {

   tesla := Tesla{"Model 3", 298000.0}
   
   // 调用 set 方法修改结构体价格
   tesla.setPrice(330000.0)
   fmt.Println(tesla) // {Model 3 298000}

}

func (t Tesla) setPrice(price float64) {
   // 修改结构体价格
   t.Price = price
}

根据输出结果可以确定 set 方法修改结构体的 Price 属性失败,这是因为结构体是值传递,作为函数参数是结构体的副本,并不是原始的结构体。

这时要将函数接收者改为结构体指针类型,才能真正实现修改结构体属性。修改 setPrice 方法为如下代码:

func (t *Tesla) setPrice(price float64) {
   // 修改结构体价格
   t.Price = price
}

使用结构体指针有两种情况:

  • 当你想改变结构体属性的时候
  • 当结构体非常大的时候

关于结构体方法需要注意的是:

  • 结构体和结构体方法必须在同一个包中
  • 内置的 int 类型不能添加结构体方法

内置的 int 绑定方法可以自定义一个 底层为 int 的数据类型,然后再绑定方法。

综上,结构体方法实现了面向对象的第一个特性 封装

二、结构体 继承

严格来说 Go 语言是不支持继承的,但是可以通过结构体 组合 或者 内嵌结构体 来实现继承特性。

首先定义 HumanStudent 两个结构体,代码如下:

type Human struct {
   Name string
   Age int
   Gender string
}

type Student struct {
   Human Human
   Grade string
   SchoolAddress string
}

接着分别给两个结构体绑定方法,输出实例化结构体的信息

func (h Human) HumanInfo(){
   fmt.Printf("Human Info, Name:%v, Age: %v, Gender: %v\n", h.Name, h.Age, h.Gender)
}

func (s Student) StudentInfo(){
   fmt.Printf("Student-Human Info, Name:%v, Age: %v, Gender: %v\n", s.Human.Name, s.Human.Age, s.Human.Gender)
   fmt.Printf("Student Info, Grade:%v, SchoolAddress: %v\n", s.Grade, s.SchoolAddress)

}

Student 结构体中嵌套了一个 Human 结构体,在输出 Student 结构体中的 Human 结构体属性的时候,通过 s.Human.Name 来输出,这种方式能不能成功输出?通过实例化结构体来调用方法验证一下。

func main() {

   h := Human{"tony", 12, "Male"}
   s := Student{h, "五年级", "NYC"}

   h.HumanInfo()
   s.StudentInfo()

}

执行上述代码,输出结果如下:

Human Info, Name:tony, Age: 12, Gender: Male
Student-Human Info, Name:tony, Age: 12, Gender: Male
Student Info, Grade:五年级, SchoolAddress: NYC

根据输出结果可以确定 s.Human.Name 这种形式是可以时候输出内嵌的结构体的信息的,但是其实还有一种匿名嵌套,既可以省略中间结构体的名字直接调用嵌套结构体的属性。

func main() {

   h := Human{"tony", 12, "Male"}
   s := Student{h, "五年级", "NYC"}

   h.HumanInfo()
   s.StudentInfo()

}

type Student struct {
   // 匿名嵌套
   Human
   Grade string
   SchoolAddress string
}

func (s Student) StudentInfo(){
   // 匿名嵌套,可以省略内嵌结构体的名称 
   fmt.Printf("Student-Human Info, Name:%v, Age: %v, Gender: %v\n", s.Name, s.Age, s.Gender)
   fmt.Printf("Student Info, Grade:%v, SchoolAddress: %v\n", s.Grade, s.SchoolAddress)

}

再次调用 main 方法,输出结果保持不变。

当匿名嵌套的结构体的属性名和当前结构体中的属性名有雷同的情况下,优先取当前结构体中的属性的值,为了区分同名属性,可以加上嵌套的结构体名。

三、结构体标签

结构体的字段除了名称和类型之外,还可以添加 标签 tag ,tag 是一个附属于结构体的字符串,使用反引号 `` 表示,是一个重要的标记。

以 JSON 序列化为例,将实例化的结构体序列化为 JSON 格式字符串时,需要将 JSON 字符串的 Key 改为小写,这时就需要用到 json 标签。

保持 Student 结构体不变,给 Human 结构体增加 json 标签

func main() {

   h := Human{"tony", 12, "Male"}
   s := Student{h, "五年级", "NYC"}

   // 序列化 Human 的实例化结构体
   hJson, _ := json.Marshal(h)
   fmt.Println(string(hJson))

   // 序列化 Student 的实例化结构体
   sJson, _ := json.Marshal(s)
   fmt.Println(string(sJson))

}

type Human struct {
   Name string `json:"name"`
   Age int `json:"age"`
   Gender string `json:"gender"`
}

执行上述代码,输出结果如下:

{"name":"tony","age":12,"gender":"Male"}
{"Human":{"name":"tony","age":12,"gender":"Male"},"Grade":"五年级","SchoolAddress":"NYC"}

通过输出结果可以确定,Human 结构体增加的 json 标签中的 name 标签值可以将结构体字段从 Name 变为 name

当然也有一些其他的标签比如 orm 标签,改标签可以限制结构体映射到数据库表时表字段的限制,比如 字段名、最大长度 max_length、最小长度 min_lengts、最大值 max 以及最小值 min 等。

通过 reflect 标准库识别结构体中每个字段上定义的 tag

func main() {

   h := Human{"tony", 12, "Male"}

   // 反射获取标签属性
   humanTag := reflect.TypeOf(h)

   for i := 0; i < humanTag.NumField(); i++ {
      field := humanTag.Field(i)
      jsonTag := field.Tag.Get("json")
      fmt.Printf("%d, %v, (%v), tag: %v\n", i+1, field.Name, field.Type.Name(), jsonTag)
   }
}

执行上述代码,输出结果如下:

1, Name, (string), tag: name
2, Age, (int), tag: age
3, Gender, (string), tag: gender