【Go语言从入门到实战】面向对象编程篇

面向对象编程

Go语言的面向对象编程和其他语言有非常大的差别。

image-20230428111344935

Go 是一种面向对象的语言吗?

是和不是。虽然 Go 有类型和方法,并允许面向对象的编程风格,但没有类型层次结构(继承)。Go 中的“接口”概念提供了一种不同的方法,我们认为这种方法易于使用,并且在某些方面更通用。还有一些方法可以将类型嵌入到其他类型中,以提供类似于(但不完全相同)子类化的东西。此外,Go 中的方法比 C++ 或 Java 中的方法更通用:它们可以为任何类型的数据定义,甚至是内置类型,例如普通的“未装箱”整数。它们不限于结构(类)。

此外,缺少类型层次结构使得 Go 中的“对象”感觉比 C++ 或 Java 等语言中的“对象”轻量级得多。

封装数据和行为

结构体定义

image-20230428121213526

实例创建及初始化

image-20230428121307421

type Employee struct {
    
    
	Id   string
	Name string
	Age  int
}

func TestCreateEmployeeObj(t *testing.T) {
    
    
	e := Employee{
    
    "0", "Bob", 20}
	e1 := Employee{
    
    Name: "Mike", Age: 30}
	e2 := new(Employee) // 返回指针
	e2.Id = "2"
	e2.Name = "Rose"
	e2.Age = 22
	t.Log(e)
	t.Log(e1)
	t.Log(e1.Id)
	t.Log(e2)
	t.Logf("e is %T", e)
	t.Logf("e2 is %T", e2)
}

image-20230428122127640

行为(方法)定义

image-20230428122402465

  • 第一种

    func (e Employee) String() string {
          
          
    	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
    	fmt.Println()
    	return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
    }
    
    func TestStructOperations(t *testing.T) {
          
          
    	e := Employee{
          
          "0", "Bob", 20}
    	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
    	fmt.Println()
    	t.Log(e.String())
    }
    

    image-20230428123715323

    所以这种写法会有复制的开销。

  • 第二种

    func (e *Employee) String() string {
          
          
    	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
    	fmt.Println()
    	return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
    }
    
    func TestStructOperations(t *testing.T) {
          
          
    	e := &Employee{
          
          "0", "Bob", 20} // 传递引用
    	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
    	fmt.Println()
    	t.Log(e.String())
    }
    

    image-20230428123601046

    更推荐这种。

接口(定义交互协议)

Go的接口和很多主流编程语言的接口有很大的区别。

Java代码的示例:

image-20230428124443597

Duck Type式接口

Go语言的interface:

image-20230428125301615

type Programmer interface {
    
    
	WriteHelloWorld() string
}

type GoProgrammer struct {
    
    
}

func (g *GoProgrammer) WriteHelloWorld() string {
    
     // duck type 鸭子类型
	return "Hello World"
}

func TestClient(t *testing.T) {
    
    
	var p Programmer
	p = new(GoProgrammer)
	t.Log(p.WriteHelloWorld())
}

image-20230428143644240

Go接口

image-20230428130027398

接口变量

image-20230428130325699

自定义类型

image-20230428130434903

type IntConv func(op int) int

// 计算函数操作的时长
func timeSpend(inner IntConv) IntConv {
    
     // 以前的方法特别的长 我们可以用自定义类型做替换
	// 类似装饰者模式,对原来的函数进行了一层包装
	return func(n int) int {
    
    
		start := time.Now()
		ret := inner(n)
		fmt.Println("time spend:", time.Since(start).Seconds())
		return ret
	}
}

扩展和复用(类似继承)

Go语言无法天然支持继承,但是又想要实现面向对象的特性。

即父类对象 使用子类对象初始化,那么该父类对象调用的函数就是子类实现的函数 ,从而满足LSP(子类交换原则)。

案例一: Go语言 支持扩展父类的功能,如下代码:

package oriented_test

import (
  "fmt"
  "testing"
)

// Pet 类
type Pet struct {
    
    
}

func (p *Pet) Speak(){
    
     // Pet类的函数成员
  	fmt.Print("Pet speak.\n")
}

func (p *Pet) SpeakTo(host string) {
    
     // Pet类的函数成员
  	p.Speak()
  	fmt.Println("Pet SpeakTo ", host)
}

// Dog 扩展Pet的功能
type Dog struct {
    
    
  	p *Pet
}

// 扩展父类的方法
func (d *Dog) Speak(){
    
    
  	d.p.Speak()
}

// 扩展父类的方法
func (d *Dog) SpeakTo(host string) {
    
    
  	d.Speak()
  	fmt.Println("Dog Speakto ", host)
}

func TestDog(t *testing.T) {
    
    
  	dog := new(Dog)
	dog.SpeakTo("Test dog")
}

以上测试代码的输出如下:

image-20230428141543094

dog 的 SpeakTo 中调用了 dog 的 Speak,其中调用了 Pet 的 Speak,所以输出正常。
Pet 和 Dog 调用不会相互影响,完全由用户决定。

但是这和我们所想需要的不同,Pet 类有自己的方法,Dog 类有自己的方法,两者作用域完全不同。

这里Go语言推出了匿名嵌套类型,即 Dog 类不用实现自己的和 Pet 类同名的方法即可,通过在 Dog 类的声明中变更 Pet 成 员。

匿名嵌套类型

案例二: Go语言支持匿名函数类型

// Dog 扩展Pet的功能
type Dog struct {
    
    
  	Pet
}

这样即不需要 Dog 声明自己的同名函数成员,默认的调用即为 Pet 成员函数的调用。

package oriented_test

import (
  	"fmt"
  	"testing"
)

type Pet struct {
    
    
}

func (p *Pet) Speak(){
    
    
  	fmt.Print("Pet speak.\n")
}

func (p *Pet) SpeakTo(host string) {
    
    
  	p.Speak()
  	fmt.Println("Pet SpeakTo ", host)
}

// Dog 扩展Pet的功能
type Dog struct {
    
    
  	Pet // 支持匿名嵌套类型
}

func TestDog(t *testing.T) {
    
    
  	var dog Dog

  	dog.Speak()
  	dog.SpeakTo("Test dog")
}

最终的输出如下:

image-20230428141503654

调用的都是 Pet 的成员函数,感觉像是继承了,因为继承默认就是子类能够使用父类的公有成员。

在匿名嵌套类型下,我们想要完整尝试一下Go语言是否真正支持继承,可以像之前的代码一样在 Dog 中实现 Pet 的同名函数,且能够通过父类对象调用子类的成员方法,像 C++/Java 这样进行向上类型转换(本身是不可能的,Go语言不支持显式类型转换)。

案例三: Go语言不支持继承,如下代码:

package oriented_test

import (
  	"fmt"
  	"testing"
)

type Pet struct {
    
    
}

func (p *Pet) Speak(){
    
    
  	fmt.Print("Pet speak.\n")
}

func (p *Pet) SpeakTo(host string) {
    
    
  	p.Speak()
  	fmt.Println("Pet SpeakTo ", host)
}

// Dog 扩展Pet的功能
type Dog struct {
    
    
  	//p *Pet
 	Pet // 支持匿名嵌套类型
}

// 重载父类的方法
func (d *Dog) Speak(){
    
    
  	fmt.Print("Dog speak.\n")
}

// 重载父类的方法
func (d *Dog) SpeakTo(host string) {
    
    
  d.Speak()
  fmt.Println("Dog Speakto ", host)
}

func TestDog(t *testing.T) {
    
    
	var dog Pet = new(Dog) // 这里就会编译错误
	dog.Speak()
	dog.SpeakTo("Test dog")
}
cannot use new(Dog) (value of type *Dog) as Pet value in variable declaration
不支持将Pet类型转换为Dog类型

总结一下,Go语言并不支持继承,能够支持接口的扩展 和 复用**(匿名嵌套类型)**,内嵌这种方式是完全不能当成继承来用的,因为它不支持访问子类的方法数据(重载),不支持LSP原则。

其中扩展 就是不同类实现相同的成员函数,能够实现类似于案例一中的扩展接口形态。

复用则是通过匿名嵌套类型实现 类似于重载的功能,可以看看案例二的代码。

多态

image-20230428143317910

type Programmer interface {
    
    
	WriteHelloWorld() string
}

type GoProgrammer struct {
    
    
}

func (g *GoProgrammer) WriteHelloWorld() string {
    
    
	return "fmt.Println(\"Hello World\")"
}

type JavaProgrammer struct {
    
    
}

func (j *JavaProgrammer) WriteHelloWorld() string {
    
    
	return "System.out.println(\"Hello World\")"
}

// 多态
func writeFirstProgram(p Programmer) {
    
    
	fmt.Printf("%T %v\n", p, p.WriteHelloWorld())
}

func TestClient(t *testing.T) {
    
    
	goProgrammer := new(GoProgrammer)
	javaProgrammer := new(JavaProgrammer)
	writeFirstProgram(goProgrammer)
	writeFirstProgram(javaProgrammer)
}

image-20230428144147613

空接口与断言

image-20230428144326958

func DoSomething(p interface{
    
    }) {
    
    
	// '.' 断言,p.(int),p断言为int类型
	if i, ok := p.(int); ok {
    
    
		fmt.Println("Integer", i)
		return
	}
	if s, ok := p.(string); ok {
    
    
		fmt.Println("string", s)
		return
	}
	fmt.Println("UnKnow type")
}

func TestEmptyInterface(t *testing.T) {
    
    
	DoSomething(10)
	DoSomething("10")
	DoSomething(10.00)
}

image-20230428145558309

Go接口最佳实践

image-20230428145718339

错误机制

error

image-20230428150518369

package error

import (
	"errors"
	"testing"
)

func GetFibonacci(n int) ([]int, error) {
    
    
	if n < 2 || n > 100 {
    
    
		return nil, errors.New("n should be in [2, 100]")
	}
	fibList := []int{
    
    1, 1}
	for i := 2; i < n; i++ {
    
    
		fibList = append(fibList, fibList[i-1]+fibList[i-2])
	}
	return fibList, nil
}

func TestGetFibonacci(t *testing.T) {
    
    
	if v, err := GetFibonacci(-10); err != nil {
    
    
		t.Error(err)
	} else {
    
    
		t.Log(v)
	}
}

image-20230428161919642

最佳实践

image-20230428162534768

尽早失败,避免嵌套!

例:

func GetFibonacci2(str string) {
    
    
	var (
		i    int
		err  error
		list []int
	)
	if i, err = strconv.Atoi(str); err != nil {
    
    
		fmt.Println("Error", err)
		return
	}
	if list, err = GetFibonacci(i); err != nil {
    
    
		fmt.Println("Error", err)
		return
	}
	fmt.Println(list)
}

panic

image-20230428163230055

panic vs os.Exit

image-20230428163307433

revocer

image-20230428164252392

recover 类似于 java 的 catch

func TestRecover(t *testing.T) {
    
    
	defer func() {
    
    
		if err := recover(); err != nil {
    
    
			fmt.Println("recovered from", err)
		}
	}()
	fmt.Println("Start")
	panic(errors.New("something wrong"))
}

image-20230428183030584

image-20230428165703129

其实上面这种修复方式是非常危险的。

我们一定要当心自己 revocer 在做的事,因为我们 revocer 并不检测到底发生了什么错误,而只是记录了一下或者直接忽略掉了,这时可能系统某些核心资源消耗完了,但我们把他强制恢复之后系统依然是不能正常工作的,还会导致我们的健康检查程序 health check 检查不出当前系统的问题,因为很多 health check 只是检查当前系统的进程在还是不在, 因为我们的进程是在的,所以就会形成僵尸进程,它还活着,但它不能提供服务。

image-20230428165726627

如果出现了这种问题,我们可以用 “Let is Crash” 可恢复的设计模式,我们直接 crash 掉,这样守护进程就会重新把服务进程提起来(说的有点高大上,其实就是重启),重启是恢复不确定性错误的最好方法。

包 package

构建可复用的模块(包)

image-20230428183701611

image-20230428185412938

my_series.go

package series

func GetFibonacci(n int) ([]int, error) {
    
    
	ret := []int{
    
    1, 1}
	for i := 2; i < n; i++ {
    
    
		ret = append(ret, ret[i-1]+ret[i-2])
	}
	return ret, nil
}

package_test.go

引用另一个包中的方法:

package client

import (
	"mygoland/geekvideo/ch13/series" // 包路径要从自己的gopath开始写起
	"testing"
)

func TestPackage(t *testing.T) {
    
    
	t.Log(series.GetFibonacci(10))
}

init方法

image-20230428185555174

func init() {
    
    
	fmt.Println("init1")
}

func init() {
    
    
	fmt.Println("init2")
}

func GetFibonacci(n int) []int {
    
    
	ret := []int{
    
    1, 1}
	for i := 2; i < n; i++ {
    
    
		ret = append(ret, ret[i-1]+ret[i-2])
	}
	return ret
}

image-20230428185810992

如何使用远程的package

image-20230428192434197

ConcurrentMap for GO

https://github.com/easierway/concurrent_map

使用 go get 命令导入

go get -u github.com/easierway/concurrent_map

image-20230428192052651

package remote

import (
	cm "github.com/easierway/concurrent_map" // 导入远程包
	"testing"
)

func TestConcurrentMap(t *testing.T) {
    
    
	m := cm.CreateConcurrentMap(99)
	m.Set(cm.StrKey("key"), 10)
	t.Log(m.Get(cm.StrKey("key")))
}

image-20230428192409198

依赖管理

Go未解决的依赖问题

image-20230429184940847

vendor路径

image-20230429185019589

常用的依赖管理工具

安装glide

  • mac环境,使用 brew 安装 glide

    brew install glide
    

    安装成功
    image-20230430132905106

  • 初始化 glide

    glide init
    

    image-20230505090852387

    image-20230505092905006

    image-20230505093034649

    image-20230505093114018

    image-20230505093146424

    image-20230505093229266

    image-20230505093327934

  • glide init 执行完毕后,生成了一个 yaml 文件,并把依赖的包和版本号定义在了里面

    image-20230505093453057

  • 在之前的目录下执行glide install

    image-20230505094627270

    然后就会在我们的指定的文件下面生成一个 vender 目录和 glide.lock 文件。

    image-20230505094650525

  • 到此为止,Go 就能 搜索到 vender 目录下面的 package 了,我们就通过 vender 来指定了包的路径和版本号,即实现了在同一环境下使用同一个包的不同版本依赖了。

笔记整理自极客时间视频教程:Go语言从入门到实战

猜你喜欢

转载自blog.csdn.net/weixin_53407527/article/details/130854848