Go语言基础 -- 面向对象编程
1. 面向对象编程
- 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中面向接口编程是非常重要的特性
2. 结构体
1. 创建/定义
1. 第一种方式:
type Cat struct {
Name string
Age int
Color string
course [3]int
}
var cat1 Cat // 实例化的时候,空间就已经分配了,有对应的默认值
cat1.Name = "小白"
cat1.Age = 3
cat1.Color = "白色"
2. 第二种方式:
type Cat struct {
Name string
Age int
Color string
course [3]int
}
cat1 := Cat{"小花",2,"花色"} //创建实例时赋值
3. 第三种方式
type Cat struct {
Name string
Age int
Color string
course [3]int
}
var cat *Cat = new(Cat) //得到一个Cat类型的指针
(*cat).Name = "小白" // 标准赋值方式,但是Go在底层自动加了优化了写法即标准写法,所以可以携程cat.Name="小白"
4. 第四种方式
type Cat struct {
Name string
Age int
Color string
course [3]int
}
var cat *Cat = &Cat{}
(*cat).Name = "小蓝"
var cat *Cat = &Cat{"小蓝"}
5. 创建结构体并赋值
package main
import "fmt"
type student struct {
name string
age int
}
func main() {
// 根据位置赋值
stu1 := student{"jack", 20}
// key-value 赋值,位置可以随意
stu2 := student{name: "jack", age: 20}
// 返回结构体指针
stu3 := &student{name: "jack", age: 20}
fmt.Println(stu1, stu2, stu3)
}
2. 字段访问
实例.字段
3. 结构体的内存布局
类似map的内存布局,实例指向内存空间,内存空间存储k-v结构的字段
4. 字段/属性细节
- 字段声明语法同变量
- 字段的类型可以为,基本数据类型,数组,引用类型
- 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值)
bool -- false, int -- 0, string -- "", 数组类型的默认值和它的元素类型相关,比如score [3]int 则为 [0,0,0], 指针,slice,和map的零值都是nil,即还没分配空间
- 不同结构体变量的字段是独立,互不影响的,一个结构体变量字段的更改,不影响另外一个
5. 结构体的细节
结构体的所有字段在内存中都是连续的,可以通过地址增加多少个字节 快速查找对应的数据
package main
import "fmt"
type Point struct {
x int
y int
}
type Rect struct {
leftUp, rigntDown Point
}
func main() {
r1 := Rect{Point{1, 2}, Point{3, 4}}
fmt.Printf("%p,%p\n", &r1.leftUp, &r1.rigntDown)
r2 := Rect{Point{1, 2}, Point{3, 4}}
fmt.Printf("%p,%p", &r2.leftUp, &r2.rigntDown)
}
// 运行结果,
// 同一时间定义的就是连续内存空间
// 十六进制,相差16字节,一个point类型是8字节,证明内存空间是连续的
// 如果不是同一时间定义,则不是如r1和r2是两个实例,不同时间定义
0xc00000e180,0xc00000e190
0xc00000e1c0,0xc00000e1d0
结构体是用户单独定义的类型,和其他类型进行转换时,需要有完全相同的字段(名字,个数和类型)
type A struct {
Num int
}
type B struct {
Num int
}
func main(){
var a A
var b B
a = b // 报错,
a = A(b) // 可以,因为两个结构体的字段类型,个数都一样,否则 不能转换
}
type Student1 struct {
Name string
Age int
}
type Student2 Student1 //Golang 认为用type定义的都是新的数据类型,不同类型之间不可以被赋值
func main(){
var stu1 Student1
var stu2 Student2
stu1 = stu2 // 报错,
stu2 = Student2(stu1) // 可以,因为两个结构体的字段类型,个数都一样,否则 不能转换
}
type integer int //Golang 认为用type定义的都是新的数据类型,不同类型之间不可以被赋值
func main(){
var i integer
var j int
j = i // 报错,
j = integer(j) // 可以,因为两个结构体的字段类型,个数都一样,否则 不能转换
}
struct 的每个字段上都可以写一个tag, 该tag 可以通过反射机制获取,常见的使用场景就是序列化与反序列化 由于每个字段的首字母必须大写才会被外部访问,但是前后端交互时,需要小写首字母的变量名
import {
"encoding/json"
}
type Monster struct {
Name string `json:"name"` //反引号 `json:"name"` 就是结构体tag
Age int `json:"age"`
Hobby string `json:"hobby"`
}
monster := Monster{"牛魔王",500,"狐狸精"}
res,err := json.Marshal(monster)
if err != nil{
错误
}
string(res) // 本身是返回[]byte,string强转成字符串
6. 面试题
package main
import "fmt"
type student struct {
name string
age int
}
func main() {
var stu1 student
stu1.name = "小明"
stu1.age = 18
var stu2 = stu1
fmt.Printf("stu1指针:%p, stu2指针:%p,stu1:%v stu2: %v\n", &stu1, &stu2, stu1, stu2)
stu2.name = "小红"
fmt.Printf("stu1指针:%p, stu2指针:%p,stu1:%v stu2: %v", &stu1, &stu2, stu1, stu2)
}
// 运行结果,var stu2 = stu1,值拷贝
stu1指针:0xc000004078, stu2指针:0xc000004090,stu1:{小明 18} stu2: {小明 18}
stu1指针:0xc000004078, stu2指针:0xc000004090,stu1:{小明 18} stu2: {小红 18}
通过一个实例改变另一个实例的值
package main
import "fmt"
type student struct {
name string
age int
}
func main() {
var stu1 student
stu1.name = "小明"
stu1.age = 18
var stu2 = &stu1 // 指向空间地址,所以一变两个都变
fmt.Printf("stu1指针:%p, stu2指针:%p,stu1:%v stu2: %v\n", &stu1, &stu2, stu1, stu2)
stu2.name = "小红"
fmt.Printf("stu1指针:%p, stu2指针:%p,stu1:%v stu2: %v", &stu1, &stu2, stu1, stu2)
}
// 运行结果
stu1指针:0xc000004078, stu2指针:0xc000006028,stu1:{小明 18} stu2: &{小明 18}
stu1指针:0xc000004078, stu2指针:0xc000006028,stu1:{小红 18} stu2: &{小红 18}
3. 方法
Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型都可以有方法,而不仅仅是struct
3.1 方法的声明和调用
type A struct {
Num int
}
// 声明
func (a A) test(){ // test这个方法只能被A调用 为了提高效率,一般会将(a *A)这样绑定
}
//调用
var a A
a.test()
//
1.test方法和A类型绑定,只能通过A类型的变量来调用
2.a 在调用test()时,会将a这个变量的值拷贝,传到方法的a变量中,在方法内部修改某些属性,并不会影响外部调用
3.参数传递时,如果传的是变量,会根据参数的数据类型来考虑是值传递还是引用传递,如果传的是地址,那就是引用传递
// func (recevier type) methodName(参数列表)(返回值列表){}
// 参数和返回值可以省略
package main
type Test struct{}
// 无参数、无返回值
func (t Test) method0() {
}
// 单参数、无返回值
func (t Test) method1(i int) {
}
// 多参数、无返回值
func (t Test) method2(x, y int) {
}
// 无参数、单返回值
func (t Test) method3() (i int) {
return
}
// 多参数、多返回值
func (t Test) method4(x, y int) (z int, err error) {
return
}
// 无参数、无返回值
func (t *Test) method5() {
}
// 单参数、无返回值
func (t *Test) method6(i int) {
}
// 多参数、无返回值
func (t *Test) method7(x, y int) {
}
// 无参数、单返回值
func (t *Test) method8() (i int) {
return
}
// 多参数、多返回值
func (t *Test) method9(x, y int) (z int, err error) {
return
}
func main() {}
下面定义一个结构体类型和该类型的一个方法:
package main
import (
"fmt"
)
//结构体
type User struct {
Name string
Email string
}
//方法
func (u User) Notify() {
fmt.Printf("%v : %v \n", u.Name, u.Email)
}
func main() {
// 值类型调用方法
u1 := User{"golang", "golang@golang.com"}
u1.Notify()
// 指针类型调用方法
u2 := User{"go", "go@go.com"}
u3 := &u2
u3.Notify()
}
输出结果:
golang : golang@golang.com
go : go@go.com
解释:
首先我们定义了一个叫做 User 的结构体类型,然后定义了一个该类型的方法叫做 Notify,该方法的接受者是一个 User 类型的值。要调用 Notify 方法我们需要一个 User 类型的值或者指针。
在这个例子中当我们使用指针时,Go 调整和解引用指针使得调用可以被执行。注意,当接收者不是一个指针时,该方法操作对应接受者的值的副本(意思就是即使你使用了指针调用函数,但是函数的接受者是值类型,所以函数内部操作还是对副本的操作,而不是指针操作。
我们修改 Notify 方法,让它的接受者使用指针类型:
package main
import (
"fmt"
)
//结构体
type User struct {
Name string
Email string
}
//方法
func (u *User) Notify() {
fmt.Printf("%v : %v \n", u.Name, u.Email)
}
func main() {
// 值类型调用方法
u1 := User{"golang", "golang@golang.com"}
u1.Notify()
// 指针类型调用方法
u2 := User{"go", "go@go.com"}
u3 := &u2
u3.Notify()
}
输出结果:
golang : golang@golang.com
go : go@go.com
注意:当接受者是指针时,即使用值类型调用那么函数内部也是对指针的操作。
方法不过是一种特殊的函数,只需将其还原,就知道 receiver T 和 *T
的差别。
package main
import "fmt"
type Data struct {
x int
}
func (self Data) ValueTest() { // func ValueTest(self Data);
fmt.Printf("Value: %p\n", &self)
}
func (self *Data) PointerTest() { // func PointerTest(self *Data);
fmt.Printf("Pointer: %p\n", self)
}
func main() {
d := Data{}
p := &d
fmt.Printf("Data: %p\n", p)
d.ValueTest() // ValueTest(d)
d.PointerTest() // PointerTest(&d)
p.ValueTest() // ValueTest(*p)
p.PointerTest() // PointerTest(p)
}
输出:
Data: 0xc42007c008
Value: 0xc42007c018
Pointer: 0xc42007c008
Value: 0xc42007c020
Pointer: 0xc42007c008
3.2 方法集
Golang方法集 :每个类型都有与之关联的方法集,这会影响到接口实现规则。
- 类型 T 方法集包含全部 receiver T 方法。
- 类型 *T 方法集包含全部 receiver T + *T 方法。
- 如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
- 如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
- 不管嵌入 T 或 T,S 方法集总是包含 T + *T 方法。
用实例 value 和 pointer 调用方法 (含匿名字段) 不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 实参。
Go 语言中内部类型方法集提升的规则:
类型 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()
}
输出结果:
t1 is : {1}
类型 T 方法集包含全部 receiver T 方法。
类型 *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()
}
输出结果:
t2 is : &{1}
类型 *T 方法集包含全部 receiver T 方法。
类型 *T 方法集包含全部 receiver *T 方法。
给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:
如类型 S 包含匿名字段 T,则 S 和 *S
方法集包含 T 方法。
这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为值类型的方法将被提升,可以被外部类型的值和指针调用。
package main
import (
"fmt"
)
type S struct {
T
}
type T struct {
int
}
func (t T) testT() {
fmt.Println("如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。")
}
func main() {
s1 := S{T{1}}
s2 := &s1
fmt.Printf("s1 is : %v\n", s1)
s1.testT()
fmt.Printf("s2 is : %v\n", s2)
s2.testT()
}
输出结果:
s1 is : {{1}}
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
s2 is : &{{1}}
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
如类型 S 包含匿名字段 *T
,则 S 和 *S
方法集包含 T + *T
方法。
这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接受者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。
package main
import (
"fmt"
)
type S struct {
T
}
type T struct {
int
}
func (t T) testT() {
fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法")
}
func (t *T) testP() {
fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法")
}
func main() {
s1 := S{T{1}}
s2 := &s1
fmt.Printf("s1 is : %v\n", s1)
s1.testT()
s1.testP()
fmt.Printf("s2 is : %v\n", s2)
s2.testT()
s2.testP()
}
输出结果:
s1 is : {{1}}
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法
s2 is : &{{1}}
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法
3.3 表达式
Golang 表达式 :根据调用者不同,方法分为两种表现形式:
instance.method(args...) // method value
<type>.func(instance, args...) //method expression
两者都可像普通函数那样赋值和传参,区别在于 method value 绑定实例,而 method expression 则须显式传参。
package main
import "fmt"
type User struct {
id int
name string
}
func (self *User) Test() {
fmt.Printf("%p, %v\n", self, self)
}
func main() {
u := User{1, "Tom"}
u.Test()
mValue := u.Test
mValue() // 隐式传递 receiver
mExpression := (*User).Test
mExpression(&u) // 显式传递 receiver
}
输出结果:
0xc42000a060, &{1 Tom}
0xc42000a060, &{1 Tom}
0xc42000a060, &{1 Tom}
需要注意,method value 会复制 receiver。
package main
import "fmt"
type User struct {
id int
name string
}
func (self User) Test() {
fmt.Println(self)
}
func main() {
u := User{1, "Tom"}
mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。
u.id, u.name = 2, "Jack"
u.Test()
mValue()
}
输出结果
{2 Jack}
{1 Tom}
在汇编层面,method value 和闭包的实现方式相同,实际返回 FuncVal 类型对象。
FuncVal { method_address, receiver_copy }
可依据方法集转换 method expression,注意 receiver 类型的差异。
package main
import "fmt"
type User struct {
id int
name string
}
func (self *User) TestPointer() {
fmt.Printf("TestPointer: %p, %v\n", self, self)
}
func (self User) TestValue() {
fmt.Printf("TestValue: %p, %v\n", &self, self)
}
func main() {
u := User{1, "Tom"}
fmt.Printf("User: %p, %v\n", &u, u)
mv := User.TestValue
mv(u)
mp := (*User).TestPointer
mp(&u)
mp2 := (*User).TestValue // *User 方法集包含 TestValue。签名变为 func TestValue(self *User)。实际依然是 receiver value copy。
mp2(&u)
}
输出:
User: 0xc42000a060, {1 Tom}
TestValue: 0xc42000a0a0, {1 Tom}
TestPointer: 0xc42000a060, &{1 Tom}
TestValue: 0xc42000a100, {1 Tom}
将方法 "还原" 成函数,就容易理解下面的代码了。
package main
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
func main() {
var p *Data = nil
p.TestPointer()
(*Data)(nil).TestPointer() // method value
(*Data).TestPointer(nil) // method expression
// p.TestValue() // invalid memory address or nil pointer dereference
// (Data)(nil).TestValue() // cannot convert nil to type Data
// Data.TestValue(nil) // cannot use nil as type Data in function argument
}
3.4 方法的注意细节
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如果程序员希望在方法中,修改结构体变量的值,可以通过传结构体指针的方式处理
type A struct {
Num int
}
// 声明
func (a *A) test(){
(*a).Num
}
//调用
var a A
(&a).test() // 编译器底层做了优化,可以通过a.test()调用
-
Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int,float32等都可以有方法
-
方法的访问范围控制的规则和函数一样,方法名首字母小写,只能在本报访问,方法首字母大写,可以在本包和其他包访问
-
如果一个变量实现了String()这个方法, fmt.Println默认会调用这个变量的String()进行输出
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{
Name : "tom",
Age : 20
}
fmt.Println(stu) // 这样只是会输出结构体
fmt.Println(&stu) // 自动执行String()方法,如果没有定义String()方法,则会输出stu的地址
}
3.5 方法和函数的区别
package main
//普通函数与方法的区别(在接收者分别为值类型和指针类型的时候)
import (
"fmt"
)
//1.普通函数
//接收值类型参数的函数
func valueIntTest(a int) int {
return a + 10
}
//接收指针类型参数的函数
func pointerIntTest(a *int) int {
return *a + 10
}
func structTestValue() {
a := 2
fmt.Println("valueIntTest:", valueIntTest(a))
//函数的参数为值类型,则不能直接将指针作为参数传递
//fmt.Println("valueIntTest:", valueIntTest(&a))
//compile error: cannot use &a (type *int) as type int in function argument
b := 5
fmt.Println("pointerIntTest:", pointerIntTest(&b))
//同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
//fmt.Println("pointerIntTest:", pointerIntTest(&b))
//compile error:cannot use b (type int) as type *int in function argument
}
//2.方法
type PersonD struct {
id int
name string
}
//接收者为值类型
func (p PersonD) valueShowName() {
fmt.Println(p.name)
}
//接收者为指针类型
func (p *PersonD) pointShowName() {
fmt.Println(p.name)
}
func structTestFunc() {
//值类型调用方法
personValue := PersonD{101, "hello world"}
personValue.valueShowName()
personValue.pointShowName()
//指针类型调用方法
personPointer := &PersonD{102, "hello golang"}
personPointer.valueShowName()
personPointer.pointShowName()
//与普通函数不同,接收者为指针类型和值类型的方法,指针类型和值类型的变量均可相互调用
}
func main() {
structTestValue()
structTestFunc()
}
-
对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
-
对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
- 调用方式不同,函数的调用方式: 函数名(实参列表) 方法的调用方式: 结构体变量.方法名(实参列表)
- 对于普通函数,接受者为值类型时,不能将指针烈性的数据直接传递,反之亦然
- 对于方法(如struct的方法),接受者为值类型时,可以直接用指针类型的变量调用,反之亦然,struct 默认是值拷贝,就算是用&结构体变量.方法名调用,也是值拷贝,除非把方法的形参改变成指针类型
不论调用方式如何真正决定是值拷贝还是地址拷贝,看这个方法是和哪种类型绑定,如果是和值类型(p Person),则是值拷贝,如果是和指针类型(p *Person) 则是地址拷贝
4. 工厂模式
1. 介绍
1. 使用背景
Go 语言中没有针对类的构造器方法定义统一的规范,倘若每次需要创建类的实例时,都需要在业务方法中事无俱细地执行实例初始化的细节,那么会存在缺陷的包括:
- 高耦合度: 业务方法和组件类之间产生高耦合度,需要了解到组件类的过多细节
- 低可维护性: 倘若组件类的定义发生变更,那么散落在各处业务方法中对类的构造流程都需要配合改动,
那么如何解决上述问题呢?在编程世界中,相当的一部分问题都可以通过增加一个中间层加以解决. 我们在此处遵循工厂模式的设计思路,在业务方法和类之间添加一个防腐中间层——工厂类,这样做能够带来的好处是:
- 解耦: 实现类和业务方法之间的解耦,如果类的构造过程发生变更,可以统一收口在工厂类中进行处理,从而对业务方法屏蔽相关细节
- 公共切面: 倘若有多个类都聚拢在工厂类中进行构造,这样各个类的构造流程中就天然形成了一个公共的切面,可以进行一些公共逻辑的执行
2. 工厂模式介绍
工厂顾名思义就是创建产品,根据产品是具体产品还是具体工厂可分为简单工厂模式(Simple Factory Pattern)和工厂方法模式(Factory Method Pattern),根据工厂的抽象程度可分为工厂方法模式和抽象工厂模式(Abstract Factory Pattern)。该模式用于封装和管理对象的创建,是一种创建型模式。
这样看我们工厂模式(Factory Pattern)可以分为三类,但其中简单工厂其实不是一个标准的的设计模式。GOF 23 种设计模式中只有「工厂方法模式」与「抽象工厂模式」。简单工厂模式可以看为工厂方法模式的一种特例
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
3. 工厂模式的优点
- 可以使代码结构清晰,有效地封装变化
- 对调用者屏蔽具体的产品类
- 降低耦合度
4. 适用场景
作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
其次,工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品需要增加依赖关系时,可以考虑使用工厂模式,将会大大降低对象之间的耦合度。再次,由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装
5. 直接创建实例的问题
耦合度高:如果业务逻辑直接依赖于具体的类实现,那么任何类实现的变更都可能影响到业务逻辑。
代码重复:在多个地方需要创建相同类型的对象时,代码重复不可避免,这增加了维护成本。
扩展性差:当需要引入新的类实现时,可能需要修改多处业务逻辑,这违反了开闭原则(对扩展开放,对修改封闭)。
6. 工厂模式的优势
降低耦合度:通过工厂模式,业务逻辑不需要知道具体的对象是如何创建的,只需要知道它们符合某个接口。
提高可维护性:对象的创建逻辑集中管理,当对象的创建方式需要改变时,只需修改工厂类,而不需要修改使用对象的代码。
易于扩展:添加新的类实现时,只需添加相应的工厂方法,无需修改现有代码。
2. 四种工厂模式
1. 简单工厂模式
1. 介绍
- 定义:是一种工厂模式,其中工厂类负责创建实例,但是它没有将责任委托给子类。所有实例化的工作都由一个类来完成。
- 适用场景:当创建逻辑不复杂,且类别不多时。
- 优点
- 直观简单:结构简单,没有复杂的继承关系,容易理解和使用。
- 易于使用:客户端代码只需知道传入正确的参数即可获取所需的产品。
- 缺点
- 扩展性差:当需要添加新的产品时,需要修改工厂类的决策逻辑,违反了开闭原则(对扩展开放,对修改封闭)。
- 违反单一职责原则:工厂类承担了所有产品的创建逻辑,一旦产品类型增多,工厂类将变得臃肿。
2. 需求
假设现在有这样一个编程场景:
- 水果 Fruit 是一个抽象的 interface,水果的共性是都可以食用水果,这里我们不纠结主被动的语义,赋以 Fruit 一个 Eat 方法
- 有三个具体的水果实现类,橘子 Orange、草莓 Strawberry、樱桃 cherry,分别实现了 Fruit 对应的 Eat 方法
- 有一个具体的水果工厂类 FruitFactory,专门用于水果的生产工作,对应的生产方法为 CreateFruit 方法,可以按照用户指定的水果类型,生产出对应的水果
上面聊到的几个类之间形成的 UML 类图如下所示:
3. 实现代码
水果 Fruit interface 实现
package main
import (
"fmt"
)
// Fruit 定义了水果的接口
type Fruit interface {
Eat()
}
// FruitFactory 简单工厂
type FruitFactory struct {}
func (f *FruitFactory) CreateFruit(t string) Fruit {
// 决策逻辑, 根据参数来选择创建什么样的产品
switch t {
case "Orange":
return &Orange{}
case "Strawberry":
return &Strawberry{}
case "Cherry":
return &Cherry{}
default:
panic("错误!")
}
}
// Orange 水果
type Orange struct {
name string
}
func (o *Orange) Eat() {
fmt.Println(o.name, "被吃掉了!")
}
// Strawberry 水果
type Strawberry struct {
name string
}
func (s *Strawberry) Eat() {
fmt.Println(s.name, "被吃掉了!")
}
// Cherry 水果
type Cherry struct {
name string
}
func (c *Cherry) Eat() {
fmt.Println(c.name, "被吃掉了!")
}
func main() {
f := FruitFactory{}
s := f.CreateFruit("Orange")
fmt.Println(s)
}
2. 工厂方法模式
1. 介绍
- 定义:定义了一个创建对象的接口,让子类决定实例化哪一个类。工厂方法让类的实例化推迟到子类。
- 适用场景:适用于当一个类不知道它必须创建的对象的类时。
- 优点
- 扩展性好:增加新的产品类时,不需要修改已有的工厂类,只需新增具体的工厂类。
- 遵循开闭原则:对扩展开放,对修改封闭。
- 缺点
- 代码冗余:每增加一个产品类,都需要增加一个对应的工厂类,可能导致工厂类数量急剧增加。
- 系统复杂度增加:随着工厂类数量的增加,系统结构变得更加复杂。
2. 实现代码
package main
import (
"fmt"
)
// Fruit 定义了水果的接口
type Fruit interface {
Eat()
}
// Orange 水果
type orange struct {
name string
}
func (o *orange) Eat() {
fmt.Println(o.name, "被吃掉了!")
}
// Strawberry 水果
type strawberry struct {
name string
}
func (s *strawberry) Eat() {
fmt.Println(s.name, "被吃掉了!")
}
// Cherry 水果
type cherry struct {
name string
}
func (c *cherry) Eat() {
fmt.Println(c.name, "被吃掉了!")
}
// Factory 定义了工厂接口
type Factory interface {
CreateFruit() Fruit
}
// Orange工厂, 用于创建 Orange
type OrangeFactory struct{}
func (c *OrangeFactory) CreateFruit() Fruit {
return &Orange{}
}
// Strawberry工厂, 用于创建 Strawberry
type StrawberryFactory struct{}
func (c *StrawberryFactory) CreateFruit() Fruit {
return &Strawberry{}
}
// Strawberry工厂, 用于创建 Strawberry
type CherryFactory struct{}
func (c *CherryFactory) CreateFruit() Fruit {
return &Cherry{}
}
func main() {
// 根据需要来通过对应工厂创建对应产品
orangeFactoryA := &OrangeFactory{}
orangeA := orangeFactoryA.CreateFruit()
fmt.Println(orangeA)
cherryFactoryA := &CherryFactory{}
cherryA := cherryFactoryA.CreateFruit()
fmt.Println(cherryA)
}
3. 抽象工厂模式
1. 介绍
-
定义:创建相关或依赖对象的家族,而不需明确指定具体类。提供一个接口以创建一族相关或依赖的对象,而不需要构造它们的具体类。
-
适用场景:当需要生成多个产品族时。
-
产品族和产品等级的概念
- 产品等级:指的是同一类产品,它们在抽象层上具有共同的特征,但在具体实现上可能有所不同。
- 产品族:指的是在同一个工厂中创建的一组产品,这些产品通常具有某种关联性。
-
优点
-
易于扩展产品族:增加新的产品族时,不需要修改已有的工厂类,只需新增具体的工厂类。
-
解耦具体类:客户端不需要知道具体的产品类是谁创建的,只需要知道是哪一个工厂创建的。
-
-
缺点
-
扩展产品等级成本高:增加新的产品等级时,需要修改抽象工厂接口及其所有子类的实现,这可能涉及到大量代码的改动。
-
增加了系统的复杂度:需要定义和维护多个接口和类。
-
2. 实现代码
package main
import (
"fmt"
)
// Fruit 定义了水果的接口
type Fruit interface {
Eat()
}
// Orange 水果
type orange struct {
name string
}
func (o *orange) Eat() {
fmt.Println(o.name, "被吃掉了!")
}
// Strawberry 水果
type strawberry struct {
name string
}
func (s *strawberry) Eat() {
fmt.Println(s.name, "被吃掉了!")
}
// Cherry 水果
type cherry struct {
name string
}
func (c *cherry) Eat() {
fmt.Println(c.name, "被吃掉了!")
}
// AbstractFactory 定义了抽象工厂接口
type AbstractFactory interface {
CreateOrange() orange
CreateStrawberry() strawberry
CreateCherry() cherry
}
// 水果加工厂
type FruitFactory struct{}
func (f *FruitFactory) CreateOrange() Fruit {
return &orange{}
}
func (f *FruitFactory) CreateStrawberry() Fruit {
return &strawberry{}
}
func (f *FruitFactory) CreateCherry() Fruit {
return &cherry{}
}
func main() {
// 通过具体工厂来创建一系列相关产品
f := &FruitFactory{}
o1 := f.CreateOrange()
s1 := f.CreateStrawberry()
c1 := f.CreateCherry()
fmt.Println(o1)
fmt.Println(s1)
fmt.Println(c1)
}
4. 容器工厂模式
1. 介绍
-
定义:依赖于依赖注入框架,工厂作为一个容器,通过注册和解析的方式管理对象的创建和生命周期。
-
适用场景:适用于大型项目,需要管理大量对象的依赖关系。
-
优点
- 灵活性:容器工厂模式提供了一个灵活的组件注册和获取机制,可以很容易地在运行时注入和替换组件。
- 解耦:组件的创建和使用完全解耦,有助于降低系统的耦合度。
- 易于测试:由于组件之间的低耦合性,可以更容易地进行单元测试。
-
缺点:可能引入外部依赖,增加了系统的复杂性。
2. 实现代码
1. 下载模块
go get go.uber.org/dig
2. 代码实现
package main
import (
"fmt"
"go.uber.org/dig"
)
// Fruit 定义了水果的接口
type Fruit interface {
Eat()
}
// Orange 水果
type orange struct {
name string
}
func (o *orange) Eat() {
fmt.Println(o.name, "被吃掉了!")
}
// Strawberry 水果
type strawberry struct {
name string
}
func (s *strawberry) Eat() {
fmt.Println(s.name, "被吃掉了!")
}
// Cherry 水果
type cherry struct {
name string
}
func (c *cherry) Eat() {
fmt.Println(c.name, "被吃掉了!")
}
// Factory 定义了工厂接口, 使用依赖注入容器来创建产品
type Factory struct {
Container *dig.Container
}
// Factory 的构造函数
func NewFactory(c *dig.Container) *Factory {
return &Factory{Container: c}
}
// 注册 Orange 产品到容器中
func (f *Factory) ProvideOrange() {
f.Container.Provide(NewOrange)
}
// 构造具体的 Orange 产品
func NewOrange() Fruit {
return &orange{}
}
func main() {
// 创建依赖注入容器
container := dig.New()
// 创建工厂并注入容器
f := NewFactory(container)
// 注册 Orange 产品到容器
f.ProvideOrange()
// 解析并获取产品实例
if err := container.Invoke(func(f Fruit) {
fmt.Println("获得实例: ",f)
}); err != nil{
fmt.Println("Error: ",err)
}
}
图文解释
在这个概念图中,容器是依赖注入的核心,它负责提供依赖关系。Factory 通过容器来注册和解析产品。客户端代码通过容器来获取产品实例,而不需要知道具体的创建细节。
容器工厂模式特别适用于大型项目,其中组件众多且依赖关系复杂。通过使用这种模式,可以有效地管理这些依赖关系,提高代码的可维护性和可测试性。
5. 如何选择工厂模式
3. 工厂模式解决外界实例化
student.go
package student
type student struct {
name string // 变量名小写, 表示其是私有变量, 外部不可访问
age int
}
// NewStudent 构造函数 首字母大写, 确保外部访问
func NewStudent(name string, age int) *student {
return &student{name, age}
}
main.go
package main
import (
"fmt"
"student"
)
func main() {
stu1 := student.NewStudent("小明", 18)
// 访问student私有变量时会报错
fmt.Println(stu1.name, stu1.age)
}
4. 外部访问私有字段
student.go
package student
type student struct {
name string
age int
}
func NewStudent(name string, age int) *student {
return &student{name, age}
}
// 通过新增一个方法来实现外部访问私有字段
func (s *student) GetName() string {
return s.name
}
main.go
package main
import (
"fmt"
"student"
)
func main() {
stu1 := student.NewStudent("小明", 18)
fmt.Println(stu1.GetName())
stu2 := student.NewStudent("小红", 18)
fmt.Println(stu2.GetName())
}
5. 抽象
我们在前面定义一个结构体的目的,实际上就是将一类事物的共有属性(字段)和行为(方法)提取出来,形成一个物理模型(模板/结构体),这种研究问题的方法称之为抽象
6. 封装(encapsulation)
封装就是将抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其他包只能通过被授权的操作(方法),才能对字段进行操作
1. 封装的好处
- 隐藏实现细节
- 可以对数据进行验证,保证安全合理
2. 如何理解封装?
- 对结构体中的属性进行封装
- 通过方法,包 进行封装
3. 封装如何实现
- 将结构体,字段(属性)的首字母小写(不能导出了,其他包不能使用类似其他语言的private)
- 给结构体所在包提供一个工厂模式的函数,首字母大写,类似一个构造函数
- 提供一个首字母大写的Set方法(类似其他语言的public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表){
// 加入数据验证的业务逻辑
var.字段 = 参数
}
- 提供一个首字母大写的Get方法(类似其他语言的public),用于获取属性的值
func (var 结构体类型名) GetXxx(){
return var.age
}
注意: 在Golang开发中并没有特别强调封装,这点并不想java,Golang本身对面向对象的特性进行了简化
4. 示例
不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证 model/person.go
package model
type person struct {
name string
age int
sal float
}
// 工厂模式函数(构造函数)
func NewPerson(name string) *person {
return &person{
Name : name,
}
}
// 为了设置 和访问 age,sal 的数据信息,编写一个Set和Get的方法
//设置年龄
func (person *person) SetAge(age int){
if age < 0 || age > 150{
fmt.Println("年龄范围不正确")
}
person.age = age
}
// 获取年龄
func (person *person) GetAge() int {
return person.age
}
// 设置薪水
func (person *person) SetSal(sal float64){
person.sal = sal
}
// 获取薪水
func (person *person) GetSal() float64{
return person.sal
}
main/main.go
package main
import "xxx/xxx/model"
func main(){
// 创建一个tom的类
person := model.NewPerson("tom")
person.SetAge(18)
}
7. 继承
1. 代码复用
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法,其他的结构体不需要重新定义这些属性和方法,只需嵌套一个Student匿名结构体即可,在Golang中,如果一个struct嵌套了另一个匿名结构体,n那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性.
2. 匿名字段(实现继承的基础)
1. 基本使用
Golang匿名字段 :可以像字段成员那样访问匿名字段方法,编译器负责查找。
package main
import "fmt"
type User struct {
id int
name string
}
type Manager struct {
User
}
func (self *User) ToString() string { // receiver = &(Manager.User)
return fmt.Sprintf("User: %p, %v", self, self)
}
func main() {
m := Manager{User{1, "Tom"}}
fmt.Printf("Manager: %p\n", &m)
fmt.Println(m.ToString())
}
输出结果:
Manager: 0xc42000a060
User: 0xc42000a060, &{1 Tom}
通过匿名字段,可获得和继承类似的复用能力。依据编译器查找次序,只需在外层定义同名方法,就可以实现 "override"。
package main
import "fmt"
type User struct {
id int
name string
}
type Manager struct {
User
title string
}
func (self *User) ToString() string {
return fmt.Sprintf("User: %p, %v", self, self)
}
func (self *Manager) ToString() string {
return fmt.Sprintf("Manager: %p, %v", self, self)
}
func main() {
m := Manager{User{1, "Tom"}, "Administrator"}
fmt.Println(m.ToString())
fmt.Println(m.User.ToString())
}
// 运行结果
Manager: 0xc420074180, &{{1 Tom} Administrator}
User: 0xc420074180, &{1 Tom}
package main
import "fmt"
//人
type Person struct {
name string
sex string
age int
}
// 学生
type Student struct {
*Person
id int
addr string
}
func main() {
s1 := Student{&Person{"5lmh", "man", 18}, 1, "bj"}
fmt.Println(s1)
fmt.Println(s1.name)
fmt.Println(s1.Person.name)
}
// 运行结果
{0xc00005c360 1 bj}
zs
zs
go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段
package main
import "fmt"
// go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段
//人
type Person struct {
name string
sex string
age int
}
type Student struct {
Person
id int
addr string
}
func main() {
// 初始化
s1 := Student{Person{"5lmh", "man", 20}, 1, "bj"}
fmt.Println(s1)
s2 := Student{Person: Person{"5lmh", "man", 20}}
fmt.Println(s2)
s3 := Student{Person: Person{name: "5lmh"}}
fmt.Println(s3)
}
// 运行结果
{{5lmh man 20} 1 bj}
{{5lmh man 20} 0 }
{{5lmh 0} 0 }
2. 同名字段的情况
package main
import "fmt"
//人
type Person struct {
name string
sex string
age int
}
type Student struct {
Person
id int
addr string
//同名字段
name string
}
func main() {
var s Student
// 给自己字段赋值了
s.name = "5lmh"
fmt.Println(s)
// 若给父类同名字段赋值,如下
s.Person.name = "枯藤"
fmt.Println(s)
}
// 运行结果
{{ 0} 0 5lmh}
{{枯藤 0} 0 5lmh}
3. 内置类型和自定义类型当做匿名字段使用
package main
import "fmt"
//人
type Person struct {
name string
sex string
age int
}
// 自定义类型
type mystr string
// 学生
type Student struct {
Person
int
mystr
}
func main() {
s1 := Student{Person{"5lmh", "man", 18}, 1, "bj"}
fmt.Println(s1)
}
// 运行结果
{{5lmh man 18} 1 bj}
4. 指针类型匿名字段
package main
import "fmt"
//人
type Person struct {
name string
sex string
age int
}
// 学生
type Student struct {
*Person
id int
addr string
}
func main() {
s1 := Student{&Person{"5lmh", "man", 18}, 1, "bj"}
fmt.Println(s1)
fmt.Println(s1.name)
fmt.Println(s1.Person.name)
}
// 运行结果
{0xc00005c360 1 bj}
zs
zs
3. 示例
type Goods struct {
Name string
Price int
}
func (goods *Goods) Set(){ // 绑定共有方法
}
type Book struct {
Goods // 这里就是嵌套匿名结构体 Goods
Writer string
}
//调用
var book = Book{}
book.Writer = "鲁迅"
book.Goods.Name = "一本书"
book.Goods.Price = 15.0
book.Goods.Set()
4. 继承说明
-
结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段,方法都可以使用
-
匿名结构体字段访问可以简化
如:book.Goods.Name ---> book.Name -
挡结构体和匿名结构体有相同的字段或方法时,编译器采用的就近访问原则,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
-
结构体内嵌入了两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字
-
如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
type Goods struct {
Name string
Price int
}
type Book struct {
good Goods // 这里就是有名结构体(组合结构体)
}
book := Book
book.good.Name
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
// 值传递方式
// 位置赋值
tv := Tv{
Good{
"电视机",
500.88,
},
Brand{
"海尔",
"山东",
},
}
// key-
tv := Tv{
Good{
Name : "电视机",
Price : 500.88,
},
Brand{
Name : "海尔",
Address : "山东",
},
}
//指针传递
package main
import "fmt"
type Tv struct {
*Good
*Brand
}
type Good struct {
name string
price float64
}
type Brand struct {
title string
address string
}
func main() {
tv := Tv{
&Good{"电视机", 500.88},
&Brand{"海尔", "山东"},
}
tv2 := Tv{
&Good{name: "电视机", price: 500.88},
&Brand{title: "TCL", address: "山东"},
}
// *的运算顺序在后,所以必须加括号
fmt.Println((*tv.Good).name, (*tv2.Brand).title)
}
- 对于基本数据类型嵌入结构体
type Tv struct{
*message1
int
n int // 如果有多个(同样基本数据类型)int类型字段,就必须要是有名结构体了
}
tv := Tv
// 赋值或调用
tv.int = 80
5. 多重继承
type Message1 struct{
}
type Message2 struct{
}
type Tv struct{
*Message1
*Message2
}
8. 多态
变量(实例)具有多种形态,在Go语言中,多态特征是通过接口实现的,可以按照统一的接口来调用不同的实现,这时接口变量就呈现不同的形态
1. 多态参数
根据传入的参数的不同,调用自己的方法
2. 多态数组
// 普通数组中是不可以放不同的结构体的,但是可以通过接口来实现
// 利用接口的多态特点来实现多态数组
var usbArr [3]Usb
usbArr[0] = Phone{} // 结构体
usbArr[1] = Phone{}
usbArr[2] = Camera{}
9. 类型断言
假如其中一个结构体中实现的方法不止接口定义的那些方法,再想通过,传入某个变量来实现某些方法,则需要类型断言
package main
import "fmt"
type Point struct {
x int
y int
}
func main() {
var a interface{}
var point Point = Point{1, 2}
a = point
var b = point
// cannot use a (variable of type interface{}) as type Point in assignment:
b = a
// 上面一行代码会报错,此时需要类型断言
var b Point
b = a.(Point)
fmt.Println(b)
}
1. 断言报错
类型断言:由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用到类型断言,类型断言会出错,如果解决?
if phone, ok := usb.(Phone); ok == true {
phone.Call()
}
2. 断言到对应类型,执行对应操作
package main
// 定义一个接口
type Usb interface {
Start()
Working(usb Usb)
Stop()
}
type Phone struct {
name string
}
func (p Phone) Call() {
println(p.name, "打电话了")
}
func (p Phone) Start() {
println("手机开启!")
}
func (p Phone) Stop() {
println("手机关闭")
}
func (p Phone) Working(usb Usb) {
usb.Start()
if phone, ok := usb.(Phone); ok == true {
phone.Call()
}
usb.Stop()
}
type Camera struct {
name string
}
func (ca Camera) Call() {
println(ca.name, "相机拍摄")
}
func (ca Camera) Start() {
println("照相开启")
}
func (ca Camera) Stop() {
println("照相关闭")
}
func (ca Camera) Working(usb Usb) {
usb.Start()
if phone, ok := usb.(Phone); ok == true {
phone.Call()
}
usb.Stop()
}
type Computer struct {
name string
}
func (c Computer) Call() {
println(c.name, "电脑call")
}
func (c Computer) Start() {
println("computer开始")
}
func (c Computer) Stop() {
println("电脑关闭")
}
func (c Computer) Working(usb Usb) {
usb.Start()
// 如果是Phone类型,还要执行phone的Call()方法
if phone, ok := usb.(Phone); ok == true {
phone.Call()
}
usb.Stop()
}
func main() {
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Computer{"小米"}
usbArr[2] = Camera{"康佳"}
for _, v := range usbArr {
v.Working(v)
}
}
3. 根据传入的参数,判断其类型
package main
import "fmt"
type student struct {
name string
}
func TypeJudge(item ...interface{}) {
for i, v := range item {
switch v.(type) {
case bool:
fmt.Printf("第%d个参数是 bool 类型,值是%v\n", i, v)
case float32:
fmt.Printf("第%d个参数是 float32 类型,值是%v\n", i, v)
case float64:
fmt.Printf("第%d个参数是 float64 类型,值是%v\n", i, v)
case int, int32, int64:
fmt.Printf("第%d个参数是 int/int32/int64 类型,值是%v\n", i, v)
case string:
fmt.Printf("第%d个参数是 string 类型,值是%v\n", i, v)
case student:
fmt.Printf("第%d个参数是 student 类型,值是%v\n", i, v)
case *student:
fmt.Printf("第%d个参数是 *student 类型,值是%v\n", i, v)
default:
fmt.Printf("第%d个参数是 不确定 类型,值是%v\n", i, v)
}
}
}
func main() {
TypeJudge(
true,
3.2,
3.55555555555555555555555555555555555,
1546,
"nihao",
student{"xiaoming"},
&student{"xiaohong"},
[]int{1, 2, 3, 4},
)
}
10. 方法定义
Golang 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。
- 只能为当前包内命名类型定义方法。
- 参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
- 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
- 不支持方法重载,receiver 只是参数签名的组成部分。
- 可用实例 value 或 pointer 调用全部方法,编译器自动转换。
一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
所有给定类型的方法属于该类型的方法集。
11. 抛异常和处理异常
1. 系统报错
package main
import "fmt"
// 系统抛
func test01() {
a := [5]int{0, 1, 2, 3, 4}
a[1] = 123
fmt.Println(a)
//a[10] = 11
index := 10
a[index] = 10
fmt.Println(a)
}
func getCircleArea(radius float32) (area float32) {
if radius < 0 {
// 自己抛
panic("半径不能为负")
}
return 3.14 * radius * radius
}
func test02() {
getCircleArea(-5)
}
//
func test03() {
// 延时执行匿名函数
// 延时到何时?(1)程序正常结束 (2)发生异常时
defer func() {
// recover() 复活 恢复
// 会返回程序为什么挂了
if err := recover(); err != nil {
fmt.Println(err)
}
}()
getCircleArea(-5)
fmt.Println("这里有没有执行")
}
func test04() {
test03()
fmt.Println("test04")
}
func main() {
test04()
}
// 运行结果
半径不能为负
test04
2. 自定义异常
package main
import (
"errors"
"fmt"
)
func getCircleArea(radius float32) (area float32, err error) {
if radius < 0 {
// 构建个异常对象
err = errors.New("半径不能为负")
return
}
area = 3.14 * radius * radius
return
}
func main() {
area, err := getCircleArea(-5)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(area)
}
}
3. 自定义error
package main
import (
"fmt"
"os"
"time"
)
type PathError struct {
path string
op string
createTime string
message string
}
func (p *PathError) Error() string {
return fmt.Sprintf("path=%s \nop=%s \ncreateTime=%s \nmessage=%s", p.path,
p.op, p.createTime, p.message)
}
func Open(filename string) error {
file, err := os.Open(filename)
if err != nil {
return &PathError{
path: filename,
op: "read",
message: err.Error(),
createTime: fmt.Sprintf("%v", time.Now()),
}
}
defer file.Close()
return nil
}
func main() {
err := Open("/Users/5lmh/Desktop/go/src/test.txt")
switch v := err.(type) {
case *PathError:
fmt.Println("get path error,", v)
default:
}
}
输出结果:
get path error, path=/Users/pprof/Desktop/go/src/test.txt
op=read
createTime=2018-04-05 11:25:17.331915 +0800 CST m=+0.000441790
message=open /Users/pprof/Desktop/go/src/test.txt: no such file or directory
12. 反射机制
1. 需求
package main
import (
"encoding/json"
"fmt"
)
type Monster struct {
Name string `json:"name"`
Age int `json:"age"`
Sal float64 `json:"sal"`
Sex string `json:"sex"`
}
func main() {
m := Monster{
"玉兔精",
20,
888.89,
"female",
}
data, _ := json.Marshal(m)
fmt.Println(string(data))
}
// 打印结果
{"name":"玉兔精","age":20,"sal":888.89,"sex":"female"}
为什么序列化后,key-val 的key值是结构体Tag的值,而不是字段的名称? 如: 不是Name 而是:name
使用反射机制,编写函数的适配器(桥连接)
2. 基本介绍
- 反射可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind)
- 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段,方法)
- 通过反射,可以修改变量的值,可以调用关联的方法
- 使用反射,要import ("reflect")
- 变量, interface{} 和 reflect.Value 是可以相互转换的
3. 获取Type/Value 接口的实例
reflect 包实现了运行时反射,允许程序操作任意类型的对象,典型用法是用静态类型interface 保存一个值,通过调用TypeOf获取其动态类型信息,该函数返回一个Type类型值,调用ValueOf函数返回一个Value类型值,该值代表运行时的数据,Zero接收一个Type类型参数并返回一个代表该类型零值的Value类型值
res := reflect.TypeOf(a) // 获取变量的类型,返回Type类型的接口,定义了一堆方法
res1 := reflect.ValueOf(a) // 获取变量的值,返回Value类型的结构体,定义了一堆方法
4. 反射基本使用流程示意图
5. 反射的应用场景
1. 不知道接口调用哪个函数,根据传入参数在运行时确定调用的具体接口,这种需要对函数或方法反射,
例如下面这种桥接模式:
func biinge(funcPtr interface{},args ...interface{}) {}
// 第一个参数funcPtr 以接口的形式传入函数指针,函数参数args以可变参数的形式传入,
// biinge函数中可以用反射来动态执行funcPtr函数
2. 对结构体序列化时,如果结构体有指定Tag,也会使用到反射生成对应的字符串
6. 案例
案例一
演示对(基本数据类型 --> interface{} --> reflect.Value)进行反射的基本操作
案例二
演示对(结构体类型,interface{},relect.Value)进行反射的基本操作
package main
import (
"fmt"
"reflect"
)
func reflectTest(serdata interface{}){
// 形参传递的过程中,int 类型被转成interface 类型
// 获取a的类型:type 类别:kind ,值
typ := reflect.TypeOf(serdata)
kin := typ.Kind()
res := reflect.ValueOf(serdata)
kin2 := res.Kind()
intVal := res.Int()
fmt.Printf("变量num 的类型为 %v,类别为 %v 值为 %v,值类别为 %v\n",typ,kin,intVal,kin2)
fmt.Printf("typ 的类型为 %T,res 的类型为%T\n",typ,res)
// 将rVale 转成 interface{}类型
ival := res.Interface()
fmt.Printf("ival 的类型为 %T\n",ival)
// 将interface{} 通过断言转成int类型
num := ival.(int)
fmt.Printf("num 的类型为 %T,值为 %v",num,num)
// 严谨一些是需要使用swith语句进行转换
switch ival.(type) {
case int,int8,int16,int32,int64:
fmt.Println()
case bool:
fmt.Println()
case string:
fmt.Println()
case float32,float64:
fmt.Println()
case Student:
fmt.Println()
}
}
func main() {
var a int = 1
reflectTest(a)
}
// 变量num 的类型为 int,类别为 int 值为 1,值类别为 int
// typ 的类型为 *reflect.rtype,res 的类型为reflect.Value
// ival 的类型为 int
// num 的类型为 int,值为 1
7. 反射的注意事项和细节
-
reflect.Value.Kind 获取变量的类别,返回的是一个常量
-
Type是类型,Kind是类别,Type和Kind可能是相同的,也可能是不同的
比如: var num int = 10 num 的type是int, Kind 也是int
比如: var stu Student struct 的Type 是包名.Student,kind 是struct -
通过反射可以让变量在 interface{} 和reflect.Value之间相互转换
-
使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如x 是int, 那么就应该使用reflect.Value(x).Int(),而不能使用其他,否则会报panic
-
通过反射来修改变量,注意当使用Setxxx方法来设置需要通过对应的指针类型来完成,这样才能改变传入的变量的值,同时需要使用到reflect.Value.Elem()方法
func reflect01(b interface{}){
rVal := reflect.Valueof(b) // 这里传入的是一个指针类型
fmt.Printf("rVal kind=%v",rVal.Kind())
// 需要调用Elem()方法获取到指针指向的值才能修改原值
rVal.Elem().SetInt(20)
}
func main(){
var num int = 10
reflect01(&num) // 修改值类型,需要传地址,否则是值拷贝,无法修改原值
}
- reflect.Value.Elem()如何理解? 就是取到指针指向的值,可以通过改变空间中的值,改变原值
8. 反射的最佳实践
1. 使用反射遍历结构体的字段,调用结构体的方法,并获取结构体标签的值
1. 定义结构体
package main
import (
"fmt"
"reflect"
)
type Monster struct {
Name string `json:"name"`
Age int `json:"age"`
Score float32 `json:"score"`
Sex string `json:"sex"`
}
func (m Monster) Print() {
fmt.Println("---start---")
fmt.Println(m)
fmt.Println("---end---")
}
func (m Monster) GetSum(n1, n2 int) int {
return n1 + n2
}
func (m Monster) Set(name string, age int, score float32, sex string) {
m.Name = name
m.Age = age
m.Score = score
m.Sex = sex
}
2. 判断参数类型
对传入的参数做类别判断,如果不是struct 就退出程序
func CheckStruct(m Monster) bool {
val := reflect.ValueOf(m) // 获取传入参数的值结构体
kd := val.Kind() // 获取传入参数的类型
if kd != reflect.Struct {
fmt.Println("error type")
return false
}
return true
}
3. 通过反射获取结构体的所有标签名
func GetAllTags(m Monster) {
typ := reflect.TypeOf(m) // 获取传入参数的类型
val := reflect.ValueOf(m) // 获取传入参数的值结构体
num := val.NumField()
for i := 0; i < num; i++ {
tagVal := typ.Field(i).Tag.Get("json")
if tagVal != "" {
fmt.Printf("field %d : tag为 %v\n", i, tagVal)
}
}
}
4. 通过反射调用结构体的方法
func CallMethod(m Monster) {
typ := reflect.TypeOf(m) // 获取传入参数的值结构体
val := reflect.ValueOf(m) // 获取传入参数的类型
num := val.NumMethod()
fmt.Printf("结构体 %v, 有 %d 个方法", typ, num)
// 1. 根据索引调用结构体方法,没有参数,必须要传入nil,否则报错,方法排序为方法名称的ASCII码从小到大排序
val.Method(1).Call(nil)
// 2. 根据方法名调用结构体的方法,没有参数,必须要传入nil,否则报错,
val.MethodByName("Print").Call(nil)
}
5. 调用GetSum()
方法,得到结果
func SetValueAndGetSum(m Monster) {
val := reflect.ValueOf(m) // 获取传入参数的值结构体
var params []reflect.Value
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(40))
res := val.Method(0).Call(params) // 调用GetSum() 传切片参数
fmt.Println(res[0].Int()) // 转成int
}
6. 依次调用方法
func TestStruct(m Monster) {
if !CheckStruct(m) {
return
}
GetAllTags(m)
CallMethod(m)
SetValueAndGetSum(m)
}
func main() {
var a = Monster{
Name: "黄鼠狼",
Age: 400,
Score: 30.9,
}
TestStruct(a)
}
2. 修改结构体的字段
1.定义结构体
package main
type Monster struct {
Name string `json:"name"`
Age int `json:"age"`
Score float32 `json:"score"`
Sex string `json:"sex"`
}
func (m Monster) Print() {
fmt.Println("---start---")
fmt.Println(m)
fmt.Println("---end---")
}
func (m Monster) GetSum(n1, n2 int) int {
return n1 + n2
}
func (m Monster) Set(name string, age int, score float32, sex string) {
m.Name = name
m.Age = age
m.Score = score
m.Sex = sex
}
2. 修改字段的值
func UpdateValue(m Monster) {
val := reflect.ValueOf(&m) // 修改值必须传指针,否则报错
num := val.Elem().NumField()
fmt.Printf("struct has %d fields\n", num)
fmt.Println("修改前: ", m)
val.Elem().Field(0).SetString("白象")
fmt.Println("修改后: ", m)
}
3. 使用反射操作任意结构体类型
type user struct{
Userid string
Name string
}
func TestReflectStruct(t *testring.T){
var (
model *user
sv reflect.Value
)
model = &user{}
sv = reflect.ValueOf(model) // 修改值,必须传指针,否则报错
sv = sv.Elem()
sv.FieldByName("Userid").SetString("123456")
sv.FieldByName("Name").SetString("nickname")
}
package main
import (
"fmt"
"reflect"
)
type User struct {
UserId string
Name string
}
func TestReflectStructPtr(u User) {
var (
model *User
st reflect.Type
elem reflect.Value
)
st = reflect.TypeOf(model)
fmt.Println(st)
st = st.Elem() // 获取st的类型值
elem = reflect.New(st) // 返回一个Value类型值,该值持有一个指向类型为type的新申请的零值指针
model = elem.Interface().(*User)
elem = elem.Elem()
elem.FieldByName("UserId").SetString("123")
elem.FieldByName("Name").SetString("小白")
fmt.Println(model, model.Name)
}
func main() {
var u = User{
Name: "小明",
UserId: "1",
}
TestReflectStructPtr(u)
}