关于 Go 语言的面向对象
Go 语言不原生支持面向对象。
但基本可以实现面向对象的三大基本特征(封装、继承、多态),所以可以实现面向对象。
面向对象效率低于面向过程,所以 go 语言不想支持面向对象。
Go 中可以通过结构体来实现封装和继承,通过接口实现多态。
基本使用
定义结构体
定义结构体,必须用type。结构体中只能有数据。数据一行一个,不能写在同一行里,不能写逗号。
type Student struct {id intname stringage 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: 8name string // size: 16age 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 intname stringage 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 float64func (i myFloat) toString() {// float to string}
结构体嵌套
结构体不能继承,但可以组合:
type Teacher struct {name string}func (t *Teacher) getName() string {return t.name}//---------------------------------------------------------------------type Course struct {name stringteacher 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 stringTeacher // 直接这样写更像继承。这样写的话,直接通过 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'
