关于 Go 语言的面向对象
Go 语言不原生支持面向对象。
但基本可以实现面向对象的三大基本特征(封装、继承、多态),所以可以实现面向对象。
面向对象效率低于面向过程,所以 go 语言不想支持面向对象。
Go 中可以通过结构体来实现封装和继承,通过接口实现多态。
基本使用
定义结构体
定义结构体,必须用type
。结构体中只能有数据。数据一行一个,不能写在同一行里,不能写逗号。
type Student struct {
id int
name string
age int
}
实例化、初始化、访问
st1 := Student{1, "xiaoming", 18} // 实例化结构体。注意是大括号不是小括号。多个值可以写在一行。如果没写属性名,默认是按位置一一对应
st2 := Student{name: "xiaoming", id: 2, age: 18} // 这种实例化方法类似 Python 的关键字参数,可以不用按位置一一对应
st3 := Student{ // 当然也可以这样写
age: 18,
name: "xiaoming",
id: 3,
}
fmt.Println(st1.name) // 通过 . 来访问结构体的属性
fmt.Println(st2.age)
fmt.Println(st3.id)
用指针访问结构体
var st1 *Student = &Student{} // 创建结构体并将其地址赋值给指针
st1 = new(Student) // 也可以用 new 的方式
fmt.Println((*st1).id) // Go 中没有 ->,用指针访问结构体属性时要用这种方式
fmt.Println(st1.id) // 这样也可以,这是前一种方式的语法糖,Go 内部会将 st1.id 转换为 (*st1).id (Go 语言中的指针是受限的,所以 Go 语言敢于这样自动转换,而 C/C++ 不敢)
结构体的默认值
var st2 Student = Student{} // 初始化结构体时如果没有给属性赋值,每个属性的值会是其对应的数据类型的默认值
var st3 Student // 这样创建,默认值同上
var st4 *Student = new(Student) // 用 new 创建时,默认值同上
var st5 *Student // 注意这样只是创建了一个指针(默认值 nil),没有实际创建结构体
fmt.Println(st2.id)
fmt.Println(st3.id)
fmt.Println(st4.id)
fmt.Println(st5.id) // 空指针异常
结构体是值类型,传参时是值传递
func changeId(s Student) {
s.id = 10
}
func main() {
st6 := Student{}
changeId(st6)
fmt.Println(st6.id) // 结果:0
}
结构体占用的内存大小
使用unsafe.Sizeof()
可以计算一个变量或常量占用的字节数。
一般结构体
import (
"fmt"
"unsafe"
)
type Student struct {
id int // size: 8
name string // size: 16
age int // size: 8
}
func main() {
s := Student{}
fmt.Println(unsafe.Sizeof(s)) // 32
}
字符串
字符串在 Go 中实际上是个结构体,这是其大致定义:
type string struct {
Data uintptr // 指针占 8 字节
Len int // int 占 8 字节(64位系统)
}
因此字符串无论多长,都是占 16 字节,因为unsafe.Sizeof()
只是计算了该结构体的大小。
fmt.Println(unsafe.Sizeof("hello world")) // 16
切片
切片也是结构体,这是其大致定义:
type slice struct {
array unsafe.Pointer // 底层数组的地址
len int // 长度
cap int // 容量
}
因此,无论切片中有多少数据,其 size 都是 24。
slc := []int{0, 1, 2, 3, 4, 5, 6}
fmt.Println(unsafe.Sizeof(slc)) // 24
给结构体绑定方法
Go 语言中结构体能绑定方法,让结构体具有行为。
type Student struct {
id int
name string
age int
}
// 为结构体绑定方法,s 相当于 Python 中的 self,可随意命名,一般约定为结构体名首字母的小写形式
func (s Student) getAge() int {
return s.age
}
func main() {
s := Student{}
fmt.Println(s.getAge()) // 调用结构体的方法
fmt.Println(Student.getAge(s)) // 上面那种写法实际上是这种写法的语法糖
}
结构体是值传递,所以 get 方法可以用上面这种方式写,但 set 方法就不行了:
func (s Student) setAge(age int) {
s.age = age
}
func main() {
s := Student{}
s.setAge(18)
fmt.Println(s.getAge()) // 仍然是 0
}
结构体是值传递,所以调用s.setAge(10)
并不会改变s
的age
,而只是改变了setAge()
函数内部的s
的age
。
所以要这样写:
func (s *Student) setAge2(age int) { // s 是个 Student 类型的指针
s.age = age
}
func main() {
s := Student{}
s.setAge2(18)
fmt.Println(s.getAge()) // 18
}
注意:结构体的方法定义只能和结构体的定义放在同一个包中(可以是不同文件)。
如果想给不同包中的类型加方法怎么办?可以用type
自己定义一个类型:
type myFloat float64
func (i myFloat) toString() {
// float to string
}
结构体嵌套
结构体不能继承,但可以组合:
type Teacher struct {
name string
}
func (t *Teacher) getName() string {
return t.name
}
//---------------------------------------------------------------------
type Course struct {
name string
teacher Teacher
}
func (c *Course) getInfo() string {
return fmt.Sprintf("课程名:%s, 老师姓名:%s", c.name, c.teacher.getName())
}
//---------------------------------------------------------------------
func main() {
c := Course{
name: "Golang",
teacher: Teacher{
name: "zhangsan",
},
}
fmt.Println(c.getInfo())
}
语法糖:
type Teacher struct {
name string
}
func (t *Teacher) getName() string {
return t.name
}
//---------------------------------------------------------------------
type Course struct {
name string
Teacher // 直接这样写更像继承。这样写的话,直接通过 c.xxx 就可以调用 Teacher 的属性和方法(c 是 Course 的实例)
}
func (c *Course) getInfo() string {
str := fmt.Sprintf("课程名:%s, 老师姓名:%s", c.name, c.name)
// 直接通过 c.name 就可以调用 Teacher 结构体的属性和方法
// 但是由于 Teacher 和 Course 中有重名属性 name(Course 中的 name 覆盖了 Teacher 中的 name),无法调用到 Teacher 的 name
// 此时可以通过 c.Teacher.name 调用
str = fmt.Sprintf("课程名:%s, 老师姓名:%s", c.name, c.Teacher.name)
return str
}
//---------------------------------------------------------------------
func main() {
c := Course{
name: "Golang",
Teacher: Teacher{
name: "zhangsan",
},
}
fmt.Println(c.getInfo())
}
结构体标签
是什么
结构体的字段除了名字和类型外,还可以有一个可选的标签(tag)。
它是一个附属于字段的字符串,包含一些重要标记。
例子:
type Student struct {
Id int `json:"id,omitempty"` // 最后用反引号引起来的字符串就是结构体标签
Name string `json:"name,omitempty"`
Age int `json:"age,omitempty"`
}
有什么用
总的来说就是,结构体中字段如果只有类型和字段名,能表达的信息是很少的,这在很多情况下是不够用的,因此使用标签来给字段添加额外的信息。
例一:orm 框架
使用 orm 框架时,一般一个结构体就对应数据库中一张表,需要声明结构体的每个字段在数据库中对应的字段的名字、数据类型、是否主键、是否唯一、默认值等等信息。此时就需要结构体标签来表达这些信息。
例如:
type SysUser struct {
UserId int `json:"userId" gorm:"primaryKey;autoIncrement;comment:编码"`
Username string `json:"username" gorm:"size:64;comment:用户名"`
Password string `json:"-" gorm:"size:128;comment:密码"`
NickName string `json:"nickName" gorm:"size:128;comment:昵称"`
Phone string `json:"phone" gorm:"size:11;comment:手机号"`
RoleId int `json:"roleId" gorm:"size:20;comment:角色ID"`
Salt string `json:"-" gorm:"size:255;comment:加盐"`
Avatar string `json:"avatar" gorm:"size:255;comment:头像"`
Sex string `json:"sex" gorm:"size:255;comment:性别"`
Email string `json:"email" gorm:"size:128;comment:邮箱"`
DeptId int `json:"deptId" gorm:"size:20;comment:部门"`
PostId int `json:"postId" gorm:"size:20;comment:岗位"`
Remark string `json:"remark" gorm:"size:255;comment:备注"`
Status string `json:"status" gorm:"size:4;comment:状态"`
}
在 Python 的 orm 框架中,一张表是这样定义的:
Python 可以通过元类编程反向获取到这些字段信息。
而在 Java 中,只能通过配置文件实现,没有 Go 和 Python 那么优雅。
例二:表单校验
和 orm 框架类似,表单校验也需要结构体标签。一个表单一般就是一个结构体,那么每个字段的校验规则都需要结构体标签来表达。
例如:
type User struct {
FirstName string `validate:"required"`
LastName string `validate:"required"`
Age uint8 `validate:"gte=0,lte=130"`
Email string `validate:"required,email"`
FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}
例三:json 序列化行为控制
使用 Go 语言内置的encoding/json
包将结构体序列化成字符串时,可以在结构体标签中定义一些序列化规则。
例如:
type Student struct {
Id int `json:"id,omitempty"`
Name string `json:"student_name,omitempty"`
Age int `json:"age"`
}
func main() {
s := Student{
Name: "zhangsan",
}
res, _ := json.Marshal(s)
fmt.Println(string(res)) // {"student_name":"zhangsan","age":0}
}
上例中,Age
字段的标签中没有omitempty
,而Id
字段的标签中有omitempty
,所以在Id
和Age
都没有初始化的情况下,最后序列化后的结果中不包含id
字段,包含age
字段(零值)。Name
字段序列化后的名字变为student_name
。
如何自己提取
可以使用反射提取结构体标签。
例子:
import (
"fmt"
"reflect"
)
type Student struct {
Id int `tag1:"id,omitempty" tag2:"type=int"`
Name string `tag1:"student_name,omitempty" tag2:"min_length=0,max_length=20"`
Age int `tag1:"age" tag2:"min=0,max=180"`
}
func main() {
s := Student{}
tp := reflect.TypeOf(s)
for i := 0; i < tp.NumField(); i++ {
field := tp.Field(i) // 获取结构体的每一个字段
tag1 := field.Tag.Get("tag1")
tag2 := field.Tag.Get("tag2")
fmt.Printf("%d. %v (%v), tag1: '%v', tag2: '%v'\n", i + 1, field.Name, field.Type.Name(), tag1, tag2)
}
}
输出结果:
1. Id (int), tag1: 'id,omitempty', tag2: 'type=int'
2. Name (string), tag1: 'student_name,omitempty', tag2: 'min_length=0,max_length=20'
3. Age (int), tag1: 'age', tag2: 'min=0,max=180'