一、go与面向对象
go是一门面向对象的语言吗? 是,也不是
严格来说在go中没有面向对象(oop)的说法,它不像其它面向对象编程语言一样,有类、继承、对象、构造函数、析构函数这些概念,但是可以使用结构体(struct)来实现面向对象的特性和功能,它类似于其它编程语言中的类,不过和传统的面向对象有很大的区别.
我们知道面向对象三大特性:封装、继承、多态,都可以通过结构体来实现
-
封装: 把数据存储到对象的内部,隐藏内部细节,对外可以可见或不可见,按标识符规则大写开头表示外部可见,小写只在内部可见
-
继承: 把公共的字段和方法提取出来复用,通过在结构体嵌入匿名字段(结构体)方式实现,这种方式也叫组合
-
多态: 鸭子类型,通过接口实现,接口是方法的类型,只要实现接口中的方法即可
接口是go中的重要的特性,通过接口(interface)关联,耦合性低,非常灵活,所以也叫面向接口编程
在go中实现面向对象比较简洁、优雅,就拿继承来说是通过组合实现的,接口也非常灵活,从设计上就是为了避免传统面向对象那些比较复杂实现方式.
二、结构体
每种数据结构都有自己的类型,用于不同的场景, 像int、map、slice等都只能保存单一、相同的数据类型,在表达复杂数据时候无法满足要求,因此我们可以使用结构体来表达复杂的数据结构。
结构体是任意类型字段的集合,从而可以组成复杂的数据结构,定义方式如下
type 标识符 struct {
字段1 类型
字段2 类型
字段3 类型
字段n 类型
}
要点:
-
通过type、结构体名、struct关键字定义一个结构体类型,也可以为结构体添加方法
-
结构体内部可以有若干字段以及对应的类型
-
结构体是值类型
-
如果多个字段的类型一致,可以合并参数的类型,写在一行
-
结构体名与字段名同样遵守大小写可见性规则
示例:
type Cat struct {
Name string
Age int
Color string
}
表示定义了一个Cat结构体,它拥有Name、Age、Color三个字段,类型分别是 string、int、string。 这个结构体就可以当成某种类型使用
要点:
- 字段也叫属性或成员变量,意思都差不多
- 结构体是值类型
- 和变量的规则一样,大写名字对外可见
三、结构体初始化与赋值
定义了结构体就可以初始化与赋值, 有下面几种方式
方式1: 通过字段赋值
// 声明一个Cat类型的结构体cat1
var cat1 Cat
//打印cat1,没有赋值所以为各个字段的零值
fmt.Println(cat1) // { 0 }
// 指定字段赋值
cat1.Name = "小白"
cat1.Age = 20
cat1.Color = "灰色"
// 打印cat1的详细信息(访问字段)
fmt.Printf("cat1的详细: Name=%v, Age=%v, Color=%v\n", cat1.Name, cat1.Age, cat1.Color) // cat1的信息: Name=小白, Age=20, Color=灰色
注:
cat1可以看成是Cat结构体(类)的一实例,或者说叫结构体变量。
不能指定一个没有在结构体中定义字段赋值,比如cat1.xxx = ooo, 因为xxx字段没有在结构体中定义(和动态语言不一样)
方式2: 按照字面量赋值
// 按照顺序个字段赋值,特点是一一对应,而且不能少也不能多
var cat1 Cat = Cat{"小白", 10, "灰色"}
cat2 := Cat{"小黑", 20, "红色"}
fmt.Printf("cat1: Name:%v, Age:%v, Color:%v\n", cat1.Name, cat1.Age, cat1.Color) // cat1: Name:小白, Age:10, Color:灰色
fmt.Printf("cat2: Name:%v, Age:%v, Color:%v\n", cat2.Name, cat2.Age, cat2.Color) // cat2: Name:小黑, Age:20, Color:红色
cat3 := Cat{"小红", 45} // 报错
// 指定字段赋值,类似于key:value方式,特点是不用考虑顺序,而且可选赋值
package main
import "fmt"
type Person struct {
Name string
Age int
}
cat1 := Person{
Name: "zhang",
Age: 10,
}
cat2 := Person{
Age: 20,
Name: "li",
}
// age不赋值
cat3 := Person{
Name: "alice",
}
fmt.Println(cat1, cat2, cat3)
方式3: 通过结构体指针
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// cat1是一个类型为*Person的指针
var cat1 *Person = &Person{}
// 打印指针的值
fmt.Printf("%p\n", cat1) // 0xc00004a420
// 指针解引用
fmt.Printf("%v\n", *cat1) // { 0}
// 通过指针给字段赋值
(*cat1).Name = "zhang"
cat1.Age = 20 // 这种方式也赋值,语法糖,与(cat1*).Age = 20等价 //Nmae:zhang, Age:20
fmt.Printf("Nmae: %v, Age: %v\n", cat1.Name, (*cat1).Age)
// cat2是一个类型为*Person的指针
cat2 := &Person{"li", 10}
fmt.Printf("Nmae: %v, Age: %v\n", cat2.Name, (*cat2).Age)
// cat3是一类型为*Person的指针(通过new函数)
cat3 := new(Person)
fmt.Printf("%p\n", cat3) //0xc00004a480
fmt.Printf("%v\n", *cat3) //{ 0}
(*cat3).Name = "bob"
cat3.Age = 45
fmt.Printf("Nmae: %v, Age: %v\n", cat3.Name, (*cat3).Age) // Nmae: bob, Age: 45
}
注: 如果是结构体指针变量,可以通过(*变量).字段引用或设置字段值,也可以通过变量.字段方式,前者是标准的写法,后者是语法糖
四、结构体使用细节
1. 如果结构体的字段包含了引用类型,需要初始化才能使用
type Person struct {
Name string
Slice []int // int切片类型字段
Family map[int]string // map类型字段
}
func main() {
var p Person
p.Name = "zhang"
p.Slice[0] = 100 // 报错, 切片没有初始化不能使用
p.Family[1] = 200 // 报错, map没有初始化不能使用
}
Person结构体包含了引用字段,不能直接使用, 应该先初始化在使用
func main() {
var p Person
p.Name = "zhang"
// 初始化字段
p.Slice = make([]int, 10)
p.Family = make(map[int]string, 20)
//字段赋值
p.Slice[0] = 100
p.Family[1] = "xxoo"
fmt.Println(p.Slice, p.Family) // 输出: [100 0 0 0 0 0 0 0 0 0] map[1:xxoo]
}
2. 不同的结构体互不影响
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
var p Person
p.Name = "zhang"
p.Age = 10
fmt.Printf("p.Name=%v, p.Age=%v\n", p.Name, p.Age) // p.Name=zhang, p.Age=10
p2 := p
p2.Name = "alice"
p2.Age = 20
fmt.Printf("p2.Name=%v, p2.Age=%v\n", p2.Name, p2.Age) // p.Name=zhang, p.Age=10
}
在这里例子中p2是结构体p的拷贝,p2的Name和Age属性和p是不一样的,因为结构体是值类型,赋值给另一个变量得到的是拷贝(新的内存空间)。
3. 只有相同个数的字段并且类型一致的结构才能转换
package main
import "fmt"
type A struct {
Name string
Age int
}
type B struct {
Name string
Age int
}
func main() {
var a A
var b B
a = b // 错误,不能隐式的转换
c := B(a) // 正确
fmt.Println(c, b)
}
4. 结构体是值类型,内存分配是连续的
package main
import (
"fmt"
)
type Person struct {
Id int
Score int
Gender int
}
func main() {
// 实例化出对象p
p := Person{1, 2, 3}
fmt.Println(&p.Id, &p.Score, &p.Gender) // 0xc00005a140 0xc00005a148 0xc00005a150 # 连续地址,后面在前面加8
}
5. 给字段打tag,用于序列化或反序列
可以给结构体的字段打tag,可以用于序列化和反序列化,程序间的交互
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"` // 给字段添加json格式的tag
Age int `json:"age"`
City string `json:"city"`
}
func main() {
// 实例化出对象p
p := Person{"Bob", 20, "Beijing"}
// 通过json包的Marshal格式化,返回一个字节切片和错误类型
jsonstr, _ := json.Marshal(p)
// 转成字符串并打印
fmt.Println(string(jsonstr)) // {"name":"Bob","age":20,"city":"Beijing"}
}
五、匿名结构体
匿名结构体,也就是没有名字的结构体,类似于匿名函数, 可以当做一个字段集合来用
package main
import "fmt"
func main() {
// 声明了一个匿名结构体并直接初始化
x := struct {
Name string
age int
}{"huangwiemin", 20}
// 打印x
fmt.Println(x)
// 查看类型
fmt.Printf("%T\n", x) // struct { Name string; age int }
// 引用字段
fmt.Println(x.Name, x.age) // huangwiemin 20
}