Go语言基础 -- 面向对象编程

1. 面向对象编程

  1. Golang也支持面向对象编程(OOP),但是和传统的面向对象有区别,并不是纯粹的面向对象语言,所以Golang支持面向对象编程特性更准确
  2. Golang没有类(class),Go语言的结构体(struct)和其他编程语言的类(class)有同等地位,可以理解成Golang是基于struct来实现OOP特性
  3. Golang面向对象编程非常简洁,去掉了传统OOP语言的集成,方法重构,构造函数和析构函数,隐藏的this指针等等
  4. Golang仍然有面向对象编程的集成,封装和多态的特性,只是实现的方式和其他OOP语言不一样,比如集成:Golang没有extends关键字,继承时通过匿名字段来实现的
  5. 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. 字段/属性细节

  1. 字段声明语法同变量
  2. 字段的类型可以为,基本数据类型,数组,引用类型
  3. 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值)
    bool -- false, int -- 0, string -- "", 数组类型的默认值和它的元素类型相关,比如score [3]int 则为 [0,0,0], 指针,slice,和map的零值都是nil,即还没分配空间
  4. 不同结构体变量的字段是独立,互不影响的,一个结构体变量字段的更改,不影响另外一个

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方法集 :每个类型都有与之关联的方法集,这会影响到接口实现规则。

  1. 类型 T 方法集包含全部 receiver T 方法。
  2. 类型 *T 方法集包含全部 receiver T + *T 方法。
  3. 如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
  4. 如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
  5. 不管嵌入 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 方法的注意细节

  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  2. 如果程序员希望在方法中,修改结构体变量的值,可以通过传结构体指针的方式处理
type A struct {
    Num int
}
// 声明
func (a *A) test(){    
    (*a).Num
}
//调用
var a A
(&a).test()  // 编译器底层做了优化,可以通过a.test()调用
  1. Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int,float32等都可以有方法

  2. 方法的访问范围控制的规则和函数一样,方法名首字母小写,只能在本报访问,方法首字母大写,可以在本包和其他包访问

  3. 如果一个变量实现了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()
}
  1. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。

  2. 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以

    1. 调用方式不同,函数的调用方式: 函数名(实参列表) 方法的调用方式: 结构体变量.方法名(实参列表)
    2. 对于普通函数,接受者为值类型时,不能将指针烈性的数据直接传递,反之亦然
    3. 对于方法(如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. 工厂模式的优点

  1. 可以使代码结构清晰,有效地封装变化
  2. 对调用者屏蔽具体的产品类
  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. 介绍

  • 定义:依赖于依赖注入框架,工厂作为一个容器,通过注册和解析的方式管理对象的创建和生命周期。

  • 适用场景:适用于大型项目,需要管理大量对象的依赖关系。

  • 优点

    1. 灵活性:容器工厂模式提供了一个灵活的组件注册和获取机制,可以很容易地在运行时注入和替换组件。
    2. 解耦:组件的创建和使用完全解耦,有助于降低系统的耦合度。
    3. 易于测试:由于组件之间的低耦合性,可以更容易地进行单元测试。
  • 缺点:可能引入外部依赖,增加了系统的复杂性。

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. 封装的好处

  1. 隐藏实现细节
  2. 可以对数据进行验证,保证安全合理

2. 如何理解封装?

  1. 对结构体中的属性进行封装
  2. 通过方法,包 进行封装

3. 封装如何实现

  1. 将结构体,字段(属性)的首字母小写(不能导出了,其他包不能使用类似其他语言的private)
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写,类似一个构造函数
  3. 提供一个首字母大写的Set方法(类似其他语言的public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表){
    // 加入数据验证的业务逻辑
    var.字段 = 参数  
}
  1. 提供一个首字母大写的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. 继承说明

  1. 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段,方法都可以使用

  2. 匿名结构体字段访问可以简化
    如:book.Goods.Name ---> book.Name

  3. 挡结构体和匿名结构体有相同的字段或方法时,编译器采用的就近访问原则,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分

  4. 结构体内嵌入了两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字

  5. 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

type Goods struct {
    Name string
    Price int  
}

type Book struct {
    good Goods   // 这里就是有名结构体(组合结构体)
}

book := Book
book.good.Name
  1. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
// 值传递方式
// 位置赋值
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)
}
  1. 对于基本数据类型嵌入结构体
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)。

  1. 只能为当前包内命名类型定义方法。
  2. 参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
  3. 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
  4. 不支持方法重载,receiver 只是参数签名的组成部分。
  5. 可用实例 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. 基本介绍

  1. 反射可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind)
  2. 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段,方法)
  3. 通过反射,可以修改变量的值,可以调用关联的方法
  4. 使用反射,要import ("reflect")
  5. 变量, 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. 反射的注意事项和细节

  1. reflect.Value.Kind 获取变量的类别,返回的是一个常量

  2. Type是类型,Kind是类别,Type和Kind可能是相同的,也可能是不同的
    比如: var num int = 10 num 的type是int, Kind 也是int
    比如: var stu Student struct 的Type 是包名.Student,kind 是struct

  3. 通过反射可以让变量在 interface{} 和reflect.Value之间相互转换

  4. 使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如x 是int, 那么就应该使用reflect.Value(x).Int(),而不能使用其他,否则会报panic

  5. 通过反射来修改变量,注意当使用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)   // 修改值类型,需要传地址,否则是值拷贝,无法修改原值
    
}
  1. 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)
}
posted @ 2021-09-23 20:40  河图s  阅读(228)  评论(0)    收藏  举报