结构体与面向对象
0. 课程内容
1. 结构体
1.1. 结构体的定义
1.2. 指针与结构体
1.3. 初始化结构体
1.4. 类型别名和自定义类型
1.4.1. 自定义类型
1.4.2. 类型别名
1.4.3. 类型定义和类型别名的区别
1.5. 结构体的匿名字段
1.6. 百花缭乱的嵌套
1.7. 结构体与JSON序列化
1.8. 结构体标签(
tag)(了解)
2. 面向对象-结构体与接口
2.1. 构造函数
2.2. 方法
2.2.1. 方法定义
2.2.2. 方法与指针
2.2.3. 嵌套结构体下(继承)
2.2.4. 方法规则
2.3. 接口
2.3.1. 接口体验
2.3.2. 接口运用
1. 结构体
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个
基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。
Go语言中通过struct来实现面向对象。
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
1.1. 结构体的定义
使用type和struct关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
其中:
1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
3.字段类型:表示结构体字段的具体类型。
例子
type go_class struct {
class_name string
class_stage int
}
func main() {
fmt.Println(go_class{"go class", 1})
var g go_class
g.class_name = "shineyork go class"
g.class_stage = 1
fmt.Println("课程名: ", g.class_name)
fmt.Println("课程阶段: ", g.class_stage)
}
我们还可以定义匿名结构体
package main
import (
"fmt"
)
func main() {
var user struct{Name string; Age int}
user.Name = "pprof.cn"
user.Age = 18
fmt.Printf("%#v\n", user)
d := struct {
a int
}{
a: 100,
}
fmt.Println(d.a)
}
1.2. 指针与结构体
可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
var gc = new(go_class)
fmt.Printf("%T\n", gc) // *main.go_class
fmt.Printf("gc=%#v\n", gc) //
从打印的结果中我们可以看出gc是一个结构体指针。
使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作
gc := &go_class{}
fmt.Printf("%T\n", gc) // *main.go_class
fmt.Printf("gc=%#v\n", gc) // gc=&main.go_class{class_name:"", class_stage:0}
gc.class_name = "shineyork"
gc.class_stage = 1
fmt.Printf("gc=%#v\n", gc)
效果:
C:\Users\shineyork>go run shineyork/03
*main.go_class
gc=&main.go_class{class_name:"", class_stage:0}
gc=&main.go_class{class_name:"shineyork", class_stage:1}
需要注意go的结构体中的每一个属性的内存地址
package main
import "fmt"
type go_class struct {
class_name string
class_stage int
}
func main() {
gckv := go_class{
class_name: "shineyork",
class_stage: 1,
}
ff("gckv %p", &gckv) // gckv 0xc00003a1f0
ff("gckv.class_name %p", &gckv.class_name) // gckv.class_name 0xc00003a200
ff("gckv.class_stage %p", &gckv.class_stage) // gckv.class_stage 0xc00003a210
}
func ff(desc string, args ...interface{}) {
fmt.Printf(desc+" \n", args)
}
1.3. 初始化结构体
package main
import "fmt"
type go_class struct {
class_name string
class_stage int
}
func main() {
// 1. 普通方式
var gc go_class
fmt.Printf("gc=%#v\n", gc) // gc=main.go_class{class_name:"", class_stage:0}
// 2. 键值对初始化
gckv := go_class{
class_name: "shineyork",
class_stage: 1,
}
fmt.Printf("gckv=%#v\n", gckv) // gckv=main.go_class{class_name:"shineyork", class_stage:1}
gckv1 := &go_class{
class_name: "shineyork",
}
fmt.Printf("gckv1=%#v\n", gckv1) // gckv1=&main.go_class{class_name:"shineyork", class_stage:0}
// 3. 使用值的列表初始化
gcv := &go_class{
"shineyork",
1,
}
fmt.Printf("gcv=%#v\n", gcv) // gcv=&main.go_class{class_name:"shineyork", class_stage:1}
}
一共有如上三种方式
1. 普通方式
2. 键值对初始化
3. 使用值的列表初始化
注意:
1. 必须初始化结构体的所有字段。
2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3. 该方式不能和键值初始化方式混用。
1.4. 类型别名和自定义类型
1.4.1. 自定义类型
在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
//将MyInt定义为int类型
type MyInt int
通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性
1.4.2. 类型别名
类型别名是Go1.9版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
我们之前见过的rune和byte就是类型别名,他们的定义如下:
type byte = uint8
type rune = int32
1.4.3. 类型定义和类型别名的区别
类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
1.5. 结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段
type go_class struct {
string
int
}
func main() {
gc := go_class{
"shineyork",
12,
}
fmt.Println(gc.string, gc.int)
}
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个
1.6. 百花缭乱的嵌套
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
//address 地址结构体
type address struct {
city string
}
//User 用户结构体
type user struct {
name string
sex string
addr address //匿名结构体
}
func main() {
user1 := user{
name: "shineyork",
sex: "男",
addr: address{
city: "长沙",
},
}
fmt.Println(user1)
var user2 user
user2.name = "shineyork"
user2.sex = "男"
user2.addr.city = "长沙"
fmt.Println(user2)
}
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
1.7. 结构体与JSON序列化
JSON 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多 个键值之间使用英文,分隔。
//address 地址结构体
type Address struct {
City string
}
//User 用户结构体
type User struct {
Name string
Sex string
Addr Address //匿名结构体
}
func main() {
user1 := &User{
Name: "shineyork",
Sex: "男",
Addr: Address{
City: "长沙",
},
}
// JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(user1)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
// JSON反序列化:JSON格式的字符串-->结构体
str := `{"Name":"shineyork","Sex":"男","Addr":{"City":"长沙"}}`
user2 := &User{}
err = json.Unmarshal([]byte(str), user2)
fmt.Println(user2)
}
1.8. 结构体标签(tag)(了解)
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很
差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student结构体的每个字段定义json序列化时使用的Tag:
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "女",
name: "pprof",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
}
2. 面向对象-结构体与接口
2.1. 构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指 针类型。
func main() {
gc := newGoClass("shineyork", 1)
fmt.Println(gc)
fmt.Printf("%p", gc)
}
func newGoClass(class_name string, class_stage int) *go_class {
return &go_class{
class_name: class_name,
class_stage: class_stage,
}
}
效果
C:\Users\shineyork>go run shineyork/03
&{shineyork 1}
0xc000004480
2.2. 方法
2.2.1. 方法定义
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
1. 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命
名为c等。
2. 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
3. 方法名、参数列表、返回参数:具体格式与函数定义相同。
例子
//User 用户结构体
type User struct {
Name string
Sex string
}
func main() {
u1 := newUser("shineyork", "男")
u1.Demo()
}
func newUser(name string, sex string) *User {
return &User{
Name: name,
Sex: sex,
}
}
func (u User) Demo() {
fmt.Printf("我是 %s ", u.Name)
}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
Golang 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。
只能为当前包内命名类型定义方法。
参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
不支持方法重载,receiver 只是参数签名的组成部分。
可用实例 value 或 pointer 调用全部方法,编译器自动转换
一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
所有给定类型的方法属于该类型的方法集。
// 无参数、单返回值
func (t Test) method3() (i int) {
return
}
// 多参数、多返回值
func (t Test) method4(x, y int) (z int, err error) {
return
}
// 无参数、单返回值
func (t *Test) method8() (i int) {
return
}
// 多参数、多返回值
func (t *Test) method9(x, y int) (z int, err error) {
return
}
2.2.2. 方法与指针
func main() {
u1 := User{"shineyork","1",}
u1.Demo()
u2 := User{"shineyork","2",}
u3 := &u2
u3.Demo()
}
func (u User) Demo() {
fmt.Printf("我是 %s sex = %s \n", u.Name, u.Sex)
}
解释: 首先我们定义了一个叫做 User 的结构体类型,然后定义了一个该类型的方法叫做 Demo,该方法的接受者是一个 User 类型的值。要调用 Demo 方法我们需要一个 User 类型的值或者指针。
在这个例子中当我们使用指针时,Go 调整和解引用指针使得调用可以被执行。注意,当接受者不是一个指针时,该方法操作对应接受者的值的副本(意思就是即使你使用了指针调用函数,但是函数的接受者是值类型,
所以函数内部操作还是对副本的操作,而不是指针操作。
func main() {
u1 := User{"shineyork", "1"}
u1.test()
u2 := User{"shineyork", "2"}
u3 := &u2
u3.test()
}
func (u *User) test() {
fmt.Printf("test name = %s sex = %s \n", u.Name, u.Sex)
}
注意:当接受者是指针时,即使用值类型调用那么函数内部也是对指针的操作。
2.2.3. 嵌套结构体(继承)
//User 用户结构体
type User struct {
Name string
Sex string
}
type Teacher struct {
user User
}
type Manger struct {
User
}
func main() {
t := Teacher{user: User{"s", "1"}}
t.user.Demo()
m := Manger{User{"m", "1"}}
m.Demo()
}
Golang匿名字段 :可以像字段成员那样访问匿名字段方法,编译器负责查找。通过匿名字段,可获得和继承类似的复用能力。依据编译器查找次序,只需在外层定义同名方法,就可以实现 "override"。
//User 用户结构体
type User struct {
Name string
Sex string
}
type Manger struct {
User
}
func main() {
m := Manger{User{"m", "1"}}
m.User.Demo()
m.Demo()
}
func (u User) Demo() {
fmt.Printf("我是 %s sex = %s \n", u.Name, u.Sex)
}
func (m Manger) Demo() {
fmt.Printf("m.User.Name %s m.User.Sex %s \n", m.User.Name, m.User.Sex)
}
2.2.4. 方法规则
类型 T 方法集包含全部 receiver T 方法。
类型 *T 方法集包含全部 receiver T + *T 方法。
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
不管嵌入 T 或 *T,*S 方法集总是包含 T + *T 方法。
类型 T 方法集包含全部 receiver T 方法。
package main
import (
"fmt"
)
type T struct {
int
}
func (t T) test() {
fmt.Println("类型 T 方法集包含全部 receiver T 方法。")
}
func main() {
t1 := T{1}
fmt.Printf("t1 is : %v\n", t1)
t1.test()
}
类型 *T 方法集包含全部 receiver T + *T 方法。
package main
import (
"fmt"
)
type T struct {
int
}
func (t T) testT() {
fmt.Println("类型 *T 方法集包含全部 receiver T 方法。")
}
func (t *T) testP() {
fmt.Println("类型 *T 方法集包含全部 receiver *T 方法。")
}
func main() {
t1 := T{1}
t2 := &t1
fmt.Printf("t2 is : %v\n", t2)
t2.testT()
t2.testP()
}
2.3. 接口
2.3.1. 接口
体验 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方
法)。
为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。
type Animal interface {
getAge() int
getType() string
}
type dog struct {
age int
type1 string
}
type cat struct {
age int
type1 string
}
func (c *cat) getType() string {
return c.type1
}
func (c *cat) getAge() int {
return c.age
}
func main() {
var animal Animal = &cat{age: 12, type1: "cat"}
fmt.Println(animal.getAge(), animal.getType())
}
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
2.3.2. 接口运用
接 interface作为通用类型
package main
import (
"fmt"
"reflect"
)
type User struct {
Id int
Name string
Amount float64
}
func main() {
var i interface{}
i = "string"
fmt.Println(i)
i = 1
fmt.Println(i)
i = User{Id: 2}
//i.(User).Id = 15 //运行此处会报错,在函数中修改interface表示的结构体的成员变量的值,编译时遇到这个编译错误,cannot assign to i.(User).Id
fmt.Println(i.(User).Id)
}
接口类型查询
只能对interface{}类型的变量使用类型查询
package main
import "fmt"
type Animal interface {
GetAge() int32
GetType() string
}
type AnimalB interface {
GetAge() int32
}
type Dog struct {
Age int32
Type string
}
func (a *Dog) GetAge() int32 {
return a.Age
}
func (a *Dog) GetType() string {
return a.Type
}
func main() {
var i interface{}
//i = "ok"
//方法一
val, ok := i.(Animal)
if !ok {
fmt.Println("no")
} else {
fmt.Println(val.GetAge())
}
// 方法二
switch val := i.(type) {
case string:
fmt.Println(val)
case int:
fmt.Println(val)
default:
fmt.Println(val)
}
// 方法三 通过反射
typename := reflect.TypeOf(i)
fmt.Println(typename)
}