概述
- Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
- Golang 没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct来实现 OOP特性的。
- Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this指针等等。
- Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承:Golang 没有 extends 关键字,继承是通过匿名字段来实现。
- Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。
结构体定义
package main
import "fmt"
// 定义 Person 结构体
type Person struct {
// 定义属性
Name string
Age int
Scores [5]float64
ptr *int // 指针
slice []int // 切片
mp map[string]string // Map
}
// 定义方法
func (p Person) test() {
fmt.Println(p.Name)
}
func main() {
a := 1
// 创建一个 Person 结构体变量
var p Person
p.Name = "aa"
p.Age = 18
p.ptr = &a
p.slice = make([]int, 10)
p.slice[0] = 100
p.mp = make(map[string]string)
p.mp["key"] = "val"
p.test() // aa
fmt.Println(p) // {aa 18 [0 0 0 0 0] 0xc0000ac058 [100 0 0 0 0 0 0 0 0 0] map[key:val]}
fmt.Println(p.Name) // aa
fmt.Println(p.Age) // 18
}
实例化结构体
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// 方式一
var p1 Person
p1.Name = "aa"
p1.Age = 18
fmt.Println(p1) // {aa 18}
// 方式二
p2 := Person{
Name: "bb",
Age: 20,
}
fmt.Println(p2) // {bb 20}
// 方式三
var p3 = new(Person)
p3.Name = "cc"
p3.Age = 22
fmt.Println(*p3) // {cc 22}
// 方式四
var p4 = &Person{}
p4.Name = "dd"
p4.Age = 24
fmt.Println(*p4) // {dd 24}
}
使用说明:
- 第三种和第四种方式返回的是结构体指针。
- 结构体指针访问字段的标准方式应该是:
(*结构体指针).字段名
,比如:(*p3).Name = "cc"
- 但 Go 做了一个简化,也支持
结构体指针.字段名
,比如:p3.Name = "cc"
。更加符合程序员使用的习惯,Go 编译器底层对p3.Name
做了转化(*p3).Name
。
使用细节 :
- 结构体的所有字段在内存中是连续的。
package main
import "fmt"
type Point struct {
x int
y int
}
type Rect struct {
leftUp, rightDown Point
}
type Rect2 struct {
leftUp, rightDown *Point
}
func main() {
r1 := Rect{Point{1, 2}, Point{3, 4}}
fmt.Println(r1) // {\{1 2} {3 4}}
// r1有四个int,在内存中是连续分布的
fmt.Printf("%p,%p,%p,%p", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)
// 输出结果:0xc0000aa080,0xc0000aa088,0xc0000aa090,0xc0000aa098
r2 := Rect2{&Point{10, 20}, &Point{30, 40}}
fmt.Println(r2) // {\{10 20} {30 40}}
// r2有两个*Point类型,这两个*Point类型本身地址是连续的,但是他们指向的地址不一定是连续
fmt.Printf("%p,%p,%p,%p", &r2.leftUp.x, &r2.leftUp.y, &r2.rightDown.x, &r2.rightDown.y)
// 输出结果:0xc000014100,0xc000014108,0xc000014110,0xc000014118
}
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)。
package main
import "fmt"
type A struct {
Num int
}
type B struct {
Num int
}
type C struct {
Age int
}
func main() {
var a A
var b B
a = A(b) // 结构体的字段一样,可以转换
fmt.Println(a, b) // {0} {0}
// var c C
// a = A(c) // 结构体的字段不一样,不可转换
}
- 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转。
package main
import "fmt"
type Student struct {
Name string
Age int
}
type Stu Student
func main() {
var stu1 Student
var stu2 Stu
// stu2 = stu1 // error
stu2 = Stu(stu1) // ok
fmt.Println(stu2) // { 0}
}
- struct 的每个字段上,可以写上一个 tag,该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `json:"name11"`
Age int `json:"age22"`
}
func main() {
stu := Student{"aa", 18}
fmt.Println(stu) // {aa 18}
jsonStr, err := json.Marshal(stu)
if err != nil {
fmt.Println("json 处理错误", err)
}
fmt.Println(string(jsonStr)) // {"name11":"aa","age22":18}
}
- 如果一个类型实现了 String() 这个方法,那么 fmt.Println 默认会调用这个变量的 String() 进行输出。
package main
import (
"fmt"
)
type Student struct {
Name string
Age int
}
func (stu *Student) String() string {
str := fmt.Sprintf("Name=[%v] Age=[%v]", stu.Name, stu.Age)
return str
}
func main() {
stu := Student{"aa", 18}
fmt.Println(&stu) // Name=[aa] Age=[18]
}
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
方法和函数区别:
- 调用方式不一样。
- 函数的调用方式: 函数名(实参列表)
- 方法的调用方式: 变量.方法名(实参列表)
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
package main
import "fmt"
type Person struct {
Name string
}
func test1(p Person) {
fmt.Println(p.Name)
}
func test2(p *Person) {
fmt.Println(p.Name)
}
func (p Person) test3() {
p.Name = "bb"
fmt.Println(p.Name)
}
func (p *Person) test4() {
p.Name = "cc"
fmt.Println(p.Name)
}
func main() {
p := Person{"aa"}
test1(p) // aa
test2(&p) // aa
p.test3() // bb
fmt.Println(p.Name) // aa
// 从形式上是传入地址,但是本质还是值拷贝
(&p).test3() // bb
fmt.Println(p.Name) // aa
// 从形式上是传入值类型,但是本质还是地址拷贝
p.test4() // cc
fmt.Println(p.Name) // cc
(&p).test4() // cc
fmt.Println(p.Name) // cc
}
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
- 如果是和值类型,比如 (p Person),则是值拷贝,如果和指针类型,比如是 (p *Person) 则是地址拷贝。
工厂模式
D:\GoWork\src\model\student.go
package model
type student struct {
Name string
score float64
}
// 结构体变量首字母小写,引入后不能直接使用,可以使用工厂模式解决
func NewStudent(n string, s float64) *student {
return &student{
Name: n,
score: s,
}
}
// score 属性首字母小写,其他包不可以直接访问,需要提供一个方法
func (s *student) GetScore() float64 {
return s.score
}
D:\GoWork\src\main\main.go
package main
import (
"fmt"
"model"
)
func main() {
var stu = model.NewStudent("aa", 88.8)
fmt.Println(*stu) // {aa 88.8}
fmt.Println(stu.Name, stu.GetScore()) // aa 88.8
}
抽象
定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。
package main
import "fmt"
// Account 定义一个结构体
type Account struct {
AccountNo string
Pwd string
Balance float64
}
// Deposit 存款
func (account *Account) Deposit(money float64, pwd string) {
// 判断密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
// 判断存储余额是否正确
if money <= 0 {
fmt.Println("你输入的金额不正确")
return
}
account.Balance += money
fmt.Println("存款成功")
}
// Withdraw 取款
func (account *Account) Withdraw(money float64, pwd string) {
// 判断密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
// 判断存储余额是否正确
if money <= 0 || money > account.Balance {
fmt.Println("你输入的金额不正确")
return
}
account.Balance -= money
fmt.Println("取款成功")
}
// Query 查询余额
func (account *Account) Query(pwd string) {
// 判断密码是否正确
if pwd != account.Pwd {
fmt.Println("你输入的密码不正确")
return
}
fmt.Printf("你的账号为:%v,余额为:%v\n", account.AccountNo, account.Balance)
}
func main() {
// 实例化一个账户
account1 := Account{
AccountNo: "6666",
Pwd: "123456",
Balance: 100.0,
}
account1.Query("123456") // 你的账号为:6666,余额为:100
account1.Deposit(200.0, "123456") // 存款成功
account1.Query("123456") // 你的账号为:6666,余额为:300
account1.Withdraw(150, "123456") // 取款成功
account1.Query("123456") // 你的账号为:6666,余额为:150
// 再实例化一个账户
account2 := Account{
AccountNo: "8888",
Pwd: "123456",
Balance: 200.0,
}
account2.Query("123456") // 你的账号为:888,余额为:200
account2.Deposit(300.0, "123456") // 存款成功
account2.Query("123456") // 你的账号为:888,余额为:500
account2.Withdraw(250, "123456") // 取款成功
account2.Query("123456") // 你的账号为:888,余额为:250
}
接口
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
- Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字。
package main
import "fmt"
// Usb 声明一个接口
type Usb interface {
Start()
Stop()
}
type Phone struct {
}
func (p Phone) Start() {
fmt.Println("手机开始工作...")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作...")
}
type Camera struct {
}
func (c Camera) Start() {
fmt.Println("相机开始工作...")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作...")
}
type Computer struct {
}
func (c Computer) Start() {
fmt.Println("计算机开始工作...")
}
func (c Computer) Stop() {
fmt.Println("计算机停止工作...")
}
// 编写一个方法 Working 方法,接收一个 Usb 接口类型变量
// 只要是实现了 Usb接口(所谓实现 Usb 接口,就是指实现了 Usb 接口声明所有方法)
func (c Computer) Working(usb Usb) { // usb变量会根据传入的实参,来判断到底是 Phone,还是 Camera
// 通过 Usb 接口变量来调用 Start 和 Stop 方法
usb.Start()
usb.Stop()
}
func main() {
// 测试
computer := Computer{}
phone := Phone{}
camera := Camera{}
// 关键点
computer.Working(phone)
computer.Working(camera)
}
interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
接口使用总结:
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)。
- 接口中所有的方法都没有方法体,即都是没有实现的方法。
- 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
- 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
- 一个自定义类型可以实现多个接口。
- Golang 接口中不能有任何变量。
- 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现。
- interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil。
- 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口。
接口最佳实现:
package main
import (
"fmt"
"math/rand"
"sort"
)
// 1. 声明 Hero 结构体
type Hero struct {
Name string
Age int
}
// 2. 声明一个 Hero 结构体切片类型
type HeroSlice []Hero
// 3. 实现 Interface 接口
func (hs HeroSlice) Len() int {
return len(hs)
}
// Less 方法就是决定你使用什么标准进行排序
// 1. 按 Hero 的年龄从小到大排序!!
func (hs HeroSlice) Less(i, j int) bool {
return hs[i].Age < hs[j].Age
// 修改成对 Name排序
// return hs[i].Name < hs[j].Name
}
func (hs HeroSlice) Swap(i, j int) {
// 交换
// temp := hs[i]
// hs[i] = hs[j]
// hs[j] = temp
// 下面的一句话等价于三句话
hs[i], hs[j] = hs[j], hs[i]
}
// 1. 声明 Student结构体
type Student struct {
Name string
Age int
Score float64
}
// 将 Student 的切片,按 Score 从大到小排序!!
func main() {
// 先定义一个数组/切片
var intSlice = []int{0, -1, 10, 7, 90}
// 要求对 intSlice切片进行排序
// 1.冒泡排序...
// 2.也可以使用系统提供的方法
sort.Ints(intSlice)
fmt.Println(intSlice)
// 请大家对结构体切片进行排序
// 1.冒泡排序...
// 2.也可以使用系统提供的方法
// 测试看看我们是否可以对结构体切片进行排序
var heroes HeroSlice
for i := 0; i < 10; i++ {
hero := Hero{
Name: fmt.Sprintf("英雄|%d", rand.Intn(100)),
Age: rand.Intn(100),
}
// 将 hero append到 heroes切片
heroes = append(heroes, hero)
}
// 看看排序前的顺序
for _, v := range heroes {
fmt.Println(v)
}
// 调用 sort.Sort
sort.Sort(heroes)
fmt.Println("-----------排序后------------")
// 看看排序后的顺序
for _, v := range heroes {
fmt.Println(v)
}
i := 10
j := 20
i, j = j, i
fmt.Println("i=", i, "j=", j) // i=20 j = 10
}
三大特征
封装
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。
封装的理解和好处:
- 隐藏实现细节。
- 提可以对数据进行验证,保证安全合理。
如何体现封装:
- 对结构体中的属性进行封装。
- 通过方法,包实现封装。
封装的实现步骤:
- 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)。
- 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数。
- 提供一个首字母大写的 Set方法(类似其它语言的public),用于对属性判断并赋值。
代码实现案例:
D:\GoWork\src\model\person.go
package model
type person struct {
Name string
age int
sal float64
}
// 写一个工厂模式的函数,相当于构造函数
func NewPerson(name string) *person {
return &person{
Name: name,
}
}
func (p *person) GetAge() int {
return p.age
}
func (p *person) SetAge(age int) {
p.age = age
}
func (p *person) GetSal() float64 {
return p.sal
}
func (p *person) SetSal(sal float64) {
p.sal = sal
}
D:\GoWork\src\main\main.go
package main
import (
"fmt"
"model"
)
func main() {
p := model.NewPerson("aa")
p.SetAge(18)
p.SetSal(3000)
fmt.Println(*p) // {aa 18 3000}
fmt.Println(p.Name, p.GetAge(), p.GetSal()) // aa 18 3000
}
继承
- 继承可以解决代码复用,扩展性和维护性提高了。
- 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。
- 其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个匿名结构体即可。
package main
import "fmt"
// Student 学生结构体
type Student struct {
Name string
Age int
Score int
}
func (stu *Student) ShowInfo() {
fmt.Println(stu.Name, stu.Age, stu.Score)
}
func (stu *Student) SetScore(score int) {
stu.Score = score
}
// Pupil 小学生结构体
type Pupil struct {
Student // 嵌入了 Student 匿名结构体
}
func (p *Pupil) testing() {
fmt.Println("小学生正在考试中...")
}
// Graduate 大学生结构体
type Graduate struct {
Student // 嵌入了 Student 匿名结构体
}
func (g *Graduate) testing() {
fmt.Println("大学生正在考试中...")
}
func main() {
pupil := &Pupil{}
pupil.Student.Name = "aa"
pupil.Student.Age = 8
pupil.testing() // 小学生正在考试中...
pupil.Student.SetScore(60)
pupil.Student.ShowInfo() // aa 8 60
graduate := &Graduate{}
graduate.Name = "bb"
graduate.Student.Age = 28
graduate.testing() // 大学生正在考试中...
graduate.Student.SetScore(80)
graduate.Student.ShowInfo() // bb 28 80
}
使用总结:
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法都可以使用。
- 匿名结构体字段访问可以简化,
graduate.Student.Name = "bb"
=>graduate.Name = "bb"
。 - 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分。
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法 (同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
- 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字。
多重继承:
type Goods struct {
Name string
Price float64
}
type Brand struct {
Name string
Address string
}
type LV struct {
Goods
Brand
}
- 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
- 为了保证代码的简洁性,建议大家尽量不使用多重继承。
多态
变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
package main
import "fmt"
// Usb 声明一个接口
type Usb interface {
Start()
Stop()
}
type Phone struct {
name string
}
func (p Phone) Start() {
fmt.Println("手机开始工作...")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作...")
}
type Camera struct {
name string
}
func (c Camera) Start() {
fmt.Println("相机开始工作...")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作...")
}
type Computer struct {
name string
}
func (c Computer) Start() {
fmt.Println("计算机开始工作...")
}
func (c Computer) Stop() {
fmt.Println("计算机停止工作...")
}
// 编写一个方法 Working 方法,接收一个 Usb 接口类型变量
// 只要是实现了 Usb接口(所谓实现 Usb 接口,就是指实现了 Usb 接口声明所有方法)
func (c Computer) Working(usb Usb) { // usb变量会根据传入的实参,来判断到底是 Phone,还是 Camera
// 通过 Usb 接口变量来调用 Start 和 Stop 方法
usb.Start()
usb.Stop()
}
func main() {
// 定义一个 Usb 接口数组,可以存放 Phone 和 Camera 的结构体变量
// 这里就体现出多态数组
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
fmt.Println(usbArr) // [{vivo} {小米} {尼康}]
}
类型断言
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,具体的如下:
var x interface{}
var b float32 = 1.1
x = b // 空接口,可以接收任意类型
// 类型断言
y := x.(float32)
fmt.Printf("y 的类型是 %T 值是 %v", y, y) // y 的类型是 float32 值是 1.1
在进行类型断言时,如果类型不匹配,就会报 panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型。
var x interface{}
var b float32 = 1.1
x = b // 空接口,可以接收任意类型
// 类型断言(带检测的)
if y, ok := x.(float32); ok {
fmt.Printf("y 的类型是 %T 值是 %v", y, y) // y 的类型是 float32 值是 1.1
} else {
fmt.Println("convert fail")
}
在前面的 Usb 接口案例做改进:给 Phone 结构体增加一个特有的方法 call(),当 Usb 接口接收的是 Phone 变量时,还需要调用 call()。
package main
import "fmt"
// Usb 声明一个接口
type Usb interface {
Start()
Stop()
}
type Phone struct {
name string
}
func (p Phone) Start() {
fmt.Println("手机开始工作...")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作...")
}
func (p Phone) Call() {
fmt.Println("手机在打电话..")
}
type Camera struct {
name string
}
func (c Camera) Start() {
fmt.Println("相机开始工作...")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作...")
}
type Computer struct {
name string
}
func (c Computer) Start() {
fmt.Println("计算机开始工作...")
}
func (c Computer) Stop() {
fmt.Println("计算机停止工作...")
}
// 编写一个方法 Working 方法,接收一个 Usb 接口类型变量
// 只要是实现了 Usb接口(所谓实现 Usb 接口,就是指实现了 Usb 接口声明所有方法)
func (c Computer) Working(usb Usb) { // usb变量会根据传入的实参,来判断到底是 Phone,还是 Camera
// 通过 Usb 接口变量来调用 Start 和 Stop 方法
usb.Start()
// 如果 Usb 是指向 Phone 结构体变量,则还需要调用 Call 方法
if phone, ok := usb.(Phone); ok {
phone.Call()
}
usb.Stop()
}
func main() {
// 定义一个 Usb 接口数组,可以存放 Phone 和 Camera 的结构体变量
// 这里就体现出多态数组
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
fmt.Println(usbArr) // [{vivo} {小米} {尼康}]
// 遍历 usbArr
var computer Computer
for _, v := range usbArr {
computer.Working(v)
fmt.Println()
}
/*输出结果:
手机开始工作...
手机在打电话..
手机停止工作...
手机开始工作...
手机在打电话..
手机停止工作...
相机开始工作...
相机停止工作...*/
}
类型断言的最佳实践:
写一函数,循环判断传入参数的类型。
package main
import "fmt"
func TypeJudge(items ...interface{}) {
for index, item := range items {
switch item.(type) {
case bool:
fmt.Printf("第%v个参数是 bool 类型,值是 %v\n", index, item)
case float32:
fmt.Printf("第%v个参数是 float32 类型,值是 %v\n", index, item)
case float64:
fmt.Printf("第%v个参数是 float64 类型,值是 %v\n", index, item)
case int, int32, int64:
fmt.Printf("第%v个参数是 int,int32,int64 类型,值是 %v\n", index, item)
case string:
fmt.Printf("第%v个参数是 string 类型,值是 %v\n", index, item)
default:
fmt.Printf("第%v个参数是 不确定 类型,值是 %v\n", index, item)
}
}
}
func main() {
var n1 float32 = 1.1
var n2 float64 = 2.2
var n3 int32 = 33
var name string = "aa"
address := "bb"
n4 := 444
TypeJudge(n1, n2, n3, name, address, n4)
/*输出结果:
第0个参数是 float32 类型,值是 1.1
第1个参数是 float64 类型,值是 2.2
第2个参数是 int,int32,int64 类型,值是 33
第3个参数是 string 类型,值是 aa
第4个参数是 string 类型,值是 bb
第5个参数是 int,int32,int64 类型,值是 444*/
}