关于 Go 语言的面向对象

Go 语言不原生支持面向对象。
但基本可以实现面向对象的三大基本特征(封装、继承、多态),所以可以实现面向对象。

面向对象效率低于面向过程,所以 go 语言不想支持面向对象。

Go 中可以通过结构体来实现封装和继承,通过接口实现多态。

基本使用

定义结构体

定义结构体,必须用type。结构体中只能有数据。数据一行一个,不能写在同一行里,不能写逗号。

  1. type Student struct {
  2. id int
  3. name string
  4. age int
  5. }

实例化、初始化、访问

  1. st1 := Student{1, "xiaoming", 18} // 实例化结构体。注意是大括号不是小括号。多个值可以写在一行。如果没写属性名,默认是按位置一一对应
  2. st2 := Student{name: "xiaoming", id: 2, age: 18} // 这种实例化方法类似 Python 的关键字参数,可以不用按位置一一对应
  3. st3 := Student{ // 当然也可以这样写
  4. age: 18,
  5. name: "xiaoming",
  6. id: 3,
  7. }
  8. fmt.Println(st1.name) // 通过 . 来访问结构体的属性
  9. fmt.Println(st2.age)
  10. fmt.Println(st3.id)

用指针访问结构体

  1. var st1 *Student = &Student{} // 创建结构体并将其地址赋值给指针
  2. st1 = new(Student) // 也可以用 new 的方式
  3. fmt.Println((*st1).id) // Go 中没有 ->,用指针访问结构体属性时要用这种方式
  4. fmt.Println(st1.id) // 这样也可以,这是前一种方式的语法糖,Go 内部会将 st1.id 转换为 (*st1).id (Go 语言中的指针是受限的,所以 Go 语言敢于这样自动转换,而 C/C++ 不敢)

结构体的默认值

  1. var st2 Student = Student{} // 初始化结构体时如果没有给属性赋值,每个属性的值会是其对应的数据类型的默认值
  2. var st3 Student // 这样创建,默认值同上
  3. var st4 *Student = new(Student) // 用 new 创建时,默认值同上
  4. var st5 *Student // 注意这样只是创建了一个指针(默认值 nil),没有实际创建结构体
  5. fmt.Println(st2.id)
  6. fmt.Println(st3.id)
  7. fmt.Println(st4.id)
  8. fmt.Println(st5.id) // 空指针异常

结构体是值类型,传参时是值传递

  1. func changeId(s Student) {
  2. s.id = 10
  3. }
  4. func main() {
  5. st6 := Student{}
  6. changeId(st6)
  7. fmt.Println(st6.id) // 结果:0
  8. }

结构体占用的内存大小

使用unsafe.Sizeof()可以计算一个变量或常量占用的字节数。

一般结构体

  1. import (
  2. "fmt"
  3. "unsafe"
  4. )
  5. type Student struct {
  6. id int // size: 8
  7. name string // size: 16
  8. age int // size: 8
  9. }
  10. func main() {
  11. s := Student{}
  12. fmt.Println(unsafe.Sizeof(s)) // 32
  13. }

字符串

字符串在 Go 中实际上是个结构体,这是其大致定义:

  1. type string struct {
  2. Data uintptr // 指针占 8 字节
  3. Len int // int 占 8 字节(64位系统)
  4. }

因此字符串无论多长,都是占 16 字节,因为unsafe.Sizeof()只是计算了该结构体的大小。

  1. fmt.Println(unsafe.Sizeof("hello world")) // 16

切片

切片也是结构体,这是其大致定义:

  1. type slice struct {
  2. array unsafe.Pointer // 底层数组的地址
  3. len int // 长度
  4. cap int // 容量
  5. }

因此,无论切片中有多少数据,其 size 都是 24。

  1. slc := []int{0, 1, 2, 3, 4, 5, 6}
  2. fmt.Println(unsafe.Sizeof(slc)) // 24

给结构体绑定方法

Go 语言中结构体能绑定方法,让结构体具有行为。

  1. type Student struct {
  2. id int
  3. name string
  4. age int
  5. }
  6. // 为结构体绑定方法,s 相当于 Python 中的 self,可随意命名,一般约定为结构体名首字母的小写形式
  7. func (s Student) getAge() int {
  8. return s.age
  9. }
  10. func main() {
  11. s := Student{}
  12. fmt.Println(s.getAge()) // 调用结构体的方法
  13. fmt.Println(Student.getAge(s)) // 上面那种写法实际上是这种写法的语法糖
  14. }

结构体是值传递,所以 get 方法可以用上面这种方式写,但 set 方法就不行了:

  1. func (s Student) setAge(age int) {
  2. s.age = age
  3. }
  4. func main() {
  5. s := Student{}
  6. s.setAge(18)
  7. fmt.Println(s.getAge()) // 仍然是 0
  8. }

结构体是值传递,所以调用s.setAge(10)并不会改变sage,而只是改变了setAge()函数内部的sage

所以要这样写:

  1. func (s *Student) setAge2(age int) { // s 是个 Student 类型的指针
  2. s.age = age
  3. }
  4. func main() {
  5. s := Student{}
  6. s.setAge2(18)
  7. fmt.Println(s.getAge()) // 18
  8. }

注意:结构体的方法定义只能和结构体的定义放在同一个包中(可以是不同文件)。
如果想给不同包中的类型加方法怎么办?可以用type自己定义一个类型:

  1. type myFloat float64
  2. func (i myFloat) toString() {
  3. // float to string
  4. }

结构体嵌套

结构体不能继承,但可以组合:

  1. type Teacher struct {
  2. name string
  3. }
  4. func (t *Teacher) getName() string {
  5. return t.name
  6. }
  7. //---------------------------------------------------------------------
  8. type Course struct {
  9. name string
  10. teacher Teacher
  11. }
  12. func (c *Course) getInfo() string {
  13. return fmt.Sprintf("课程名:%s, 老师姓名:%s", c.name, c.teacher.getName())
  14. }
  15. //---------------------------------------------------------------------
  16. func main() {
  17. c := Course{
  18. name: "Golang",
  19. teacher: Teacher{
  20. name: "zhangsan",
  21. },
  22. }
  23. fmt.Println(c.getInfo())
  24. }

语法糖:

  1. type Teacher struct {
  2. name string
  3. }
  4. func (t *Teacher) getName() string {
  5. return t.name
  6. }
  7. //---------------------------------------------------------------------
  8. type Course struct {
  9. name string
  10. Teacher // 直接这样写更像继承。这样写的话,直接通过 c.xxx 就可以调用 Teacher 的属性和方法(c 是 Course 的实例)
  11. }
  12. func (c *Course) getInfo() string {
  13. str := fmt.Sprintf("课程名:%s, 老师姓名:%s", c.name, c.name)
  14. // 直接通过 c.name 就可以调用 Teacher 结构体的属性和方法
  15. // 但是由于 Teacher 和 Course 中有重名属性 name(Course 中的 name 覆盖了 Teacher 中的 name),无法调用到 Teacher 的 name
  16. // 此时可以通过 c.Teacher.name 调用
  17. str = fmt.Sprintf("课程名:%s, 老师姓名:%s", c.name, c.Teacher.name)
  18. return str
  19. }
  20. //---------------------------------------------------------------------
  21. func main() {
  22. c := Course{
  23. name: "Golang",
  24. Teacher: Teacher{
  25. name: "zhangsan",
  26. },
  27. }
  28. fmt.Println(c.getInfo())
  29. }

结构体标签

是什么

结构体的字段除了名字和类型外,还可以有一个可选的标签(tag)。
它是一个附属于字段的字符串,包含一些重要标记。

例子:

  1. type Student struct {
  2. Id int `json:"id,omitempty"` // 最后用反引号引起来的字符串就是结构体标签
  3. Name string `json:"name,omitempty"`
  4. Age int `json:"age,omitempty"`
  5. }

有什么用

总的来说就是,结构体中字段如果只有类型和字段名,能表达的信息是很少的,这在很多情况下是不够用的,因此使用标签来给字段添加额外的信息。

例一:orm 框架

使用 orm 框架时,一般一个结构体就对应数据库中一张表,需要声明结构体的每个字段在数据库中对应的字段的名字、数据类型、是否主键、是否唯一、默认值等等信息。此时就需要结构体标签来表达这些信息。
例如:

  1. type SysUser struct {
  2. UserId int `json:"userId" gorm:"primaryKey;autoIncrement;comment:编码"`
  3. Username string `json:"username" gorm:"size:64;comment:用户名"`
  4. Password string `json:"-" gorm:"size:128;comment:密码"`
  5. NickName string `json:"nickName" gorm:"size:128;comment:昵称"`
  6. Phone string `json:"phone" gorm:"size:11;comment:手机号"`
  7. RoleId int `json:"roleId" gorm:"size:20;comment:角色ID"`
  8. Salt string `json:"-" gorm:"size:255;comment:加盐"`
  9. Avatar string `json:"avatar" gorm:"size:255;comment:头像"`
  10. Sex string `json:"sex" gorm:"size:255;comment:性别"`
  11. Email string `json:"email" gorm:"size:128;comment:邮箱"`
  12. DeptId int `json:"deptId" gorm:"size:20;comment:部门"`
  13. PostId int `json:"postId" gorm:"size:20;comment:岗位"`
  14. Remark string `json:"remark" gorm:"size:255;comment:备注"`
  15. Status string `json:"status" gorm:"size:4;comment:状态"`
  16. }

在 Python 的 orm 框架中,一张表是这样定义的:
image.png
Python 可以通过元类编程反向获取到这些字段信息。

而在 Java 中,只能通过配置文件实现,没有 Go 和 Python 那么优雅。

例二:表单校验

和 orm 框架类似,表单校验也需要结构体标签。一个表单一般就是一个结构体,那么每个字段的校验规则都需要结构体标签来表达。

例如:

  1. type User struct {
  2. FirstName string `validate:"required"`
  3. LastName string `validate:"required"`
  4. Age uint8 `validate:"gte=0,lte=130"`
  5. Email string `validate:"required,email"`
  6. FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
  7. Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
  8. }

例三:json 序列化行为控制

使用 Go 语言内置的encoding/json包将结构体序列化成字符串时,可以在结构体标签中定义一些序列化规则。
例如:

  1. type Student struct {
  2. Id int `json:"id,omitempty"`
  3. Name string `json:"student_name,omitempty"`
  4. Age int `json:"age"`
  5. }
  6. func main() {
  7. s := Student{
  8. Name: "zhangsan",
  9. }
  10. res, _ := json.Marshal(s)
  11. fmt.Println(string(res)) // {"student_name":"zhangsan","age":0}
  12. }

上例中,Age字段的标签中没有omitempty,而Id字段的标签中有omitempty,所以在IdAge都没有初始化的情况下,最后序列化后的结果中不包含id字段,包含age字段(零值)。
Name字段序列化后的名字变为student_name

如何自己提取

可以使用反射提取结构体标签。

例子:

  1. import (
  2. "fmt"
  3. "reflect"
  4. )
  5. type Student struct {
  6. Id int `tag1:"id,omitempty" tag2:"type=int"`
  7. Name string `tag1:"student_name,omitempty" tag2:"min_length=0,max_length=20"`
  8. Age int `tag1:"age" tag2:"min=0,max=180"`
  9. }
  10. func main() {
  11. s := Student{}
  12. tp := reflect.TypeOf(s)
  13. for i := 0; i < tp.NumField(); i++ {
  14. field := tp.Field(i) // 获取结构体的每一个字段
  15. tag1 := field.Tag.Get("tag1")
  16. tag2 := field.Tag.Get("tag2")
  17. fmt.Printf("%d. %v (%v), tag1: '%v', tag2: '%v'\n", i + 1, field.Name, field.Type.Name(), tag1, tag2)
  18. }
  19. }

输出结果:

  1. 1. Id (int), tag1: 'id,omitempty', tag2: 'type=int'
  2. 2. Name (string), tag1: 'student_name,omitempty', tag2: 'min_length=0,max_length=20'
  3. 3. Age (int), tag1: 'age', tag2: 'min=0,max=180'