Administrator
发布于 2023-03-08 / 46 阅读
0
0

Golang---Types & Methods & Interface

Types

type Person struct {
	FirstName string
	LastName  string
	Age       int
}

this should be read as declaring a user-defined type with the namePerson to have the underlying type of the struct literal that follows.

In addition to struct literals, you can use any primitive type or compound type literal to define a concrete type.

type Score int
type Converter func(string) Score
type TeamScores map[string]Score

Methods

the basics of using method

首先,Go要求,定义的type 和 这个type对应的method,必须存在于同一个package中。

虽然可以让user-defined type 和 对应的method,放在同一个package下的不同file中,但是最好keep your type definition and its associated methods 放在同一个file中。这样,代码可读性更好,更容易维护

package main

import "fmt"

func main() {
	p := Person{
		"zhang",
		"san",
		20,
	}

	outMsg := p.toString()
	fmt.Println(outMsg)
}

type Person struct {
	firstName string
	lastName  string
	age       int
}

func (p Person) toString() string {
	return fmt.Sprintf("%s ,%s ,age %d", p.firstName, p.lastName, p.age)

}

输出

zhang ,san ,age 20

Method declarations look just like function declarations, with one addition: the receiver specification. The receiver appears between the keyword func and the name of the method.

Just like all other variable declarations, the receiver name appears before the type. By convention, the receiver name is a short abbreviation of the type’s name, usually its first letter.

Pointer Type Receivers and Value Type Receivers

If your method modifies the receiver, you must use a pointer type receiver.

If your method doesn’t modify the receiver, you can use a value type receiver.

If your method needs to handle nil instances (see “Code Your Methods for nil Instances”), then it must use a pointer type receiver.

对一个变量来说:

  • 如果是the variable of value type,那么这个变量,既能调用the method of value type receiver,也能调用the method of pointer type receiver

  • 如果是the variable of pointer type,那么这个变量,也是既能调用the method of value type receiver,也能调用the method of pointer type receiver


package main

import (
	"fmt"
	"time"
)

func main() {
	var c Counter
	/**
	c is the variable of value type
	c.string() 表示 调用the method of value type receiver
	c.Increment() 表示 调用the method of pointer  type receiver
	*/
	fmt.Println(c.String())
	c.Increment() // 这个会自动转换成 &c.Increment()
	fmt.Println(c.String())

	fmt.Println("==========================================")
	/**
	c is the variable of pointer type
	pointerC.string() 表示 调用the method of value type receiver
	pointerC.Increment() 表示 调用the method of pointer  type receiver
	*/
	var pointerC *Counter
	pointerC = &c
	pointerC.Increment()
	fmt.Println(pointerC.String())
}

type Counter struct {
	total       int
	lastUpdated time.Time
}

func (c *Counter) Increment() {
	c.total++
	c.lastUpdated = time.Now()
}

func (c Counter) String() string {
	return fmt.Sprintf("total: %d, last updated: %v", c.total, c.lastUpdated)
}

输出

total: 0, last updated: 0001-01-01 00:00:00 +0000 UTC
total: 1, last updated: 2023-03-08 11:22:13.1010574 +0800 CST m=+0.001646201
==========================================
total: 2, last updated: 2023-03-08 11:22:13.1049303 +0800 CST m=+0.005519101

我们需要注意的是,如果调用了the method of pointer type receiver,那么很有可能会修改原始变量的值

nil receiver

在java中,如果我们使用一个值为null的变量,去调用方法,会报空指针异常

而在go中,如果一个变量的值为nil,此时,调用这个变量的方法,有可能会报异常,有可能不会报异常。

我们先看一个nil value receiver的例子:

func main() {
	h := GetHuman("zhangsan")
	fmt.Print(h.Name())
}

type Human interface {
	Name() string
}

// GetHuman returns nil indicating the person was not found
func GetHuman(name string) Human {
	return nil
}

type chinese struct {
	name string
}

func (c chinese) Name() string {
	return c.name
}

输出

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x12c796]

goroutine 1 [running]:
main.main()
        E:/ProfessionalDoc/GoProject/raw-demo/src/method/Receiver.go:9 +0x16


接下来,我们再看一个nil pointer type receiver的例子

func main() {

	var h *chinese = nil
	fmt.Print(h.Name())
}

type Human interface {
	Name() string
}

// GetHuman returns nil indicating the person was not found
func GetHuman(name string) Human {
	return nil
}

type chinese struct {
	name string
}

//func (c chinese) Name() string {
//	return c.name
//}

func (c *chinese) Name() string {
	if c == nil {
		return "person was not found"
	}
	return c.name
}

输出

person was not found

比较2个例子,得到的结论:

If it’s a method with a value receiver, you’ll get a panic (we discuss panics in “ panic and recover”), as there is no value being pointed to by the pointer. 

If it’s a method with a pointer receiver, it can work if the method is written to handle the possibility of a nil instance.

Create Function Instance by method----method value & method expression

在函数章节,我们说过 function Instance 和 function type。我们先来回顾下:


我们普通写的一个function,其实就类似于一个实例---zhangsan,而我们定义的function type 就类似于Person类型。

那么,我们如何从写法上,来区分定义的是function type  还是function instance呢?
如果带有函数体,就是一个function instance
如果不带有函数体的,就是一个function type
type opFuncType func(int,int) int   // 不带有函数体,这是一个function type

func add(a int, b int) int { //带有函数体,这是一个function instance
    return a + b  
}

The type of a function is built out of the keyword func and the types of the parameters and return values.

This combination is called the signature of the function.

Any function that has the exact same number and type of parameters meets the type signature.


这里针对function instance,我们可以将function instance看做一个value,即可以将function instance分配给一个变量,如下:

package main

import "fmt"

func add(i int, j int) int {
	return i + j
}

func main() {

	opFunc := add // 这里的opFunc是一个变量
	res := opFunc(5, 3)
	fmt.Println(res) // 8
}

如果上面的代码,再结合function type,那么修改如下:

package main

import "fmt"

func add(i int, j int) int {
	return i + j
}

type opFuncType func(int, int) int

func main() {

	var opFunc opFuncType = add // 这里的opFunc是一个变量
	res := opFunc(5, 3)
	fmt.Println(res) // 8
}

回顾完function Instance 和 function type之后,我们这里,再介绍method value 和 method expression。

首先,明确一点,不管是method value 和 method expression,得到的结果,都是function instance,不是function type

how to create a method value: type instance.methodName

how to create a method expression: type.methodName。 In the case of a method expression, the first parameter is the receiver for the method;

看下下面的例子:

package main

import "fmt"

func main() {
	myAdder := Adder{
		10,
	}
	f1 := myAdder.AddTo // method value --- 对应的function type: func(int)int
	fmt.Println(f1(5))

	f2 := Adder.AddTo // method expression--- 对应的function type: func(Adder,int)int
	fmt.Println(f2(myAdder, 50))

}

type Adder struct {
	start int
}

func (a Adder) AddTo(toAddVal int) int {
	return a.start + toAddVal
}

输出

15
60

functions vs methods

those values should be stored in a struct and that logic should be implemented as a method.

Ifyour logic only depends on the input parameters, then it should be a function.

Declare Type Based on Other Type & Embedded Field

在Go中,只有composition,没有inheritance。

但是,这并不代表着,别的语言关于inheritance的优点,go缺失了。

以前,我们使用继承,一般有以下几种场景:

  • 2个类,有很多相同的字段,不想重复定义
  • 2个类,有很多相同的方法,不想重复定义
  • 将子类实例,赋值给父类引用,这样在定义一些工具类的方法时,方法参数的类型是父类,这样这个方法,就可以给多个子类实例使用

如果,2个类,有很多相同的字段 或者 方法,我们一般将相同的字段 和 方法,写在父类中,这2个类继承这个父类。这样,在子类中,就可以使用父类的字段 和 方法了。

go针对这2个目的,使用以下几种的方式,也实现了部分

Declare Type Based on Other Type

这种方式主要是,针对2个类,有很多相同的字段,不想重复定义。也就是说,2个类的字段,是完全相同的,但是这种方式,新类是无法使用旧类的方法的。

// A Mutex is a data type with two methods, Lock and Unlock.
type Mutex struct         { /* Mutex fields */ }
func (m *Mutex) Lock()    { /* Lock implementation */ }
func (m *Mutex) Unlock()  { /* Unlock implementation */ }

// NewMutex has the same composition as Mutex but its method set is empty.
type NewMutex Mutex

// The method set of PtrMutex's underlying type *Mutex remains unchanged,
// but the method set of PtrMutex is empty.
type PtrMutex *Mutex

refer to : https://golang.google.cn/ref/spec#Type_declarations

Embedded Field

上面的Declare Type Based on Other Type,有一个缺陷,就是新类无法使用旧类的方法。

为了解决这个文,这里引入了Embedded Field。

package main

import "fmt"

type Employee struct {
	name string
	id   int
}

func (e Employee) description() string {
	return fmt.Sprintf("%s %d", e.name, e.id)
}

type Manager struct {
	Employee
	level string
}

func main() {
	e := Employee{
		name: "zhangsan",
		id:   10,
	}
	m := Manager{
		e, "高级经理",
	}
	fmt.Println(m.name)
	fmt.Println(m.description())
	fmt.Println(m.level)
}

输出

zhangsan
zhangsan 10
高级经理

可以看到,embedded field,除了能让新类使用旧类的方法,同时,还提供了一个新的特性,就是能定义一些新的字段,而且是通过composition的方式,去实现的。

Note that Manager contains a field of type Employee, but no name is assigned to that field. This makes Employee an embedded field


Any fields or methods declared on an embedded field are promoted to the containing struct and can be invoked directly on it.

区分比较

我们之前,提到了,别的语言提到的inheritance的使用场景:

  • 2个类,有很多相同的字段,不想重复定义
  • 2个类,有很多相同的方法,不想重复定义
  • 将子类实例,赋值给父类引用,这样在定义一些工具类的方法时,方法参数的类型是父类,这样这个方法,就可以给多个子类实例使用

golang通过declare type based on other type 和 embedded field,解决了前2个场景,而且是通过composition的方式,实现的。至于第3个场景,在golang看来,是缺点,所以不支持。

如下:

var eFail Employee = m; // compilation error
var eOk Employee = m.Employee // ok
fmt.Println(eOk)


The methods on the embedded field have no idea they are embedded.

If you have a method on an embedded field that calls another method on the embedded field, and the containing struct has a method of the same name, the method on the embedded field will not invoke the method on containing struct

package main

import "fmt"

type Inner struct {
	A int
}

func (i Inner) IntPrinter(val int) string {
	return fmt.Sprintf("inner: %d", val)
}

func (i Inner) Double() string {
	return i.IntPrinter(i.A * 2)
}

type Outer struct {
	Inner
	S string
}

func (o Outer) IntPrinter(val int) string {
	return fmt.Sprintf("outer: %d", val)
}

func main() {
	o := Outer{
		Inner{10},
		"hello",
	}
	fmt.Println(o.Double())

}

输出

inner: 20

上面的demo中,我们先调用double方法,紧接着,double方法中,又调用了IntPrinter方法,但是注意,IntPrinter方法,这个方法名,Inner中有,Outer中也有,那么从结果来看,最终调用的是,Inner中的IntPrinter方法。

从这个demo,我们可以得出结论,go中没有动态调度,也就是说,虽然内嵌struct中的方法,没有意识到,他们被内嵌了,他们仍然保持,和原来没有被内嵌时,一样的运转模式。我们可以这样理解:

inner是一家独立公司,outer也是一家独立公司,突然有1天,inner被outer并购了,只剩下了outer这一个独立的公司。此时,outer变得更加强大了,outer不仅能够提供原来就有的能力,还能对外提供inner的能力(outer能访问inner内部所有的属性和方法)但是,并购做的不够彻底。

所以,outer突然使用inner原有的一些能力,做一些事情的时候,inner内部,如果需要调用某些资源时,仍然只用inner原有内部的资源,而不用outer提供的资源。

iota & const

the basics of using iota

package main

import "fmt"

func main()  {

	const (
		Personal MailCategory = iota
		Spam
		Social
		Advertisements
	)

	fmt.Println(Personal,Spam,Social,Advertisements)

}

type MailCategory int

输出

0 1 2 3

Go 语言的代码中常量定义经常使用iota,下面看几个 Go 的源码。

标准库time源码:

// src/time/time.go
type Month int

const (
  January Month = 1 + iota
  February
  March
  April
  May
  June
  July
  August
  September
  October
  November
  December
)

type Weekday int

const (
  Sunday Weekday = iota
  Monday
  Tuesday
  Wednesday
  Thursday
  Friday
  Saturday
)

标准库net/http源码:

// src/net/http/server.go
type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosed
)

iota是方便我们定义常量的一个机制。简单来说,iota独立作用于每一个常量定义组中(单独出现的每个const语句都算作一个组),iota出现在用于初始化常量值的常量表达式中,iota的值为它在常量组中的第几行(从 0 开始)。使用iota定义的常量下面可以省略类型和初始化表达式,这时会沿用上一个定义的类型和初始化表达式。我们看几组例子:

const (
  One int = iota + 1
  Two
  Three
  Four
  Five
)

这个也是最常使用的方式,iota出现在第几行,它的值就是多少。上面常量定义组中,One在第 0 行(注意从 0 开始计数),iota为 0,所以One = 0 + 1 = 1。 下一行Two省略了类型和初始化表达式,因此Two沿用上面的类型int,初始化表达式也是iota + 1。但是此时是定义组中的第 1 行,iota的值为 1,所以Two = 1 + 1 = 2。 再下一行Three也省略了类型和初始化表达式,因此Three沿用了Two进而沿用了One的类型int,初始化表达式也是iota + 1,但是此时是定义的第 2 行,所以Three = 2 + 1 = 3。以此类推。

我们可以在非常复杂的初始化表达式中使用iota

const (
  Mask1 int = 1<<(iota+1) - 1
  Mask2
  Mask3
  Mask4
)

按照上面的分析Mask1~4依次为 1, 3, 7, 15。

另外还有奇数,偶数:

const (
  Odd1 = 2*iota + 1
  Odd2
  Odd3
)

const (
  Even1 = 2 * (iota + 1)
  Even2
  Even3
)

在一个组中,iota不一定出现在第 0 行,但是它出现在第几行,值就为多少

const (
  A int = 1
  B int = 2
  C int = iota + 1
  D
  E
)

上面iota出现在第 2 行(从 0 开始),C的值为2 + 1 = 3DE分别为 4, 5。

一定要注意iota的值等于它出现在组中的第几行,而非它的第几次出现。

可以通过赋值给空标识符来忽略值:

const (
  _ int = iota
  A // 1
  B // 2
  C // 3
  D // 4
  E // 5
)

说了这么多iota的用法,那么为什么要用iota呢?换句话说,iota有什么优点?我觉得有两点:

  • 方便定义,在模式比较固定的时候,我们可以只写出第一个,后面的常量不需要写出类型和初始化表达式了;
  • 方便调整顺序,增加和删除常量定义,如果我们定义了一组常量之后,想要调整顺序,使用iota的定义,只需要调整位置即可,不需要修改初始化式,因为就没有写。增加和删除也是一样的,如果我们一个个写出了初始化式,删除中间某个,后续的值就必须做调整。

例如,net/http中的源码:

type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosed
)

如果我们需要增加一个常量,表示正在关闭的状态。现在只需要写出新增的状态名:

type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosing // 新增的状态
  StateClosed
)

如果是显式写出初始化式:

type ConnState int

const (
  StateNew ConnState = 0
  StateActive ConnState = 1
  StateIdle ConnState = 2
  StateHijacked ConnState = 3
  StateClosed ConnState = 4
)

这时新增需要改动后续的值。另外需要键入的字符也多了不少 :

const (
  StateNew ConnState = 0
  StateActive ConnState = 1
  StateIdle ConnState = 2
  StateHijacked ConnState = 3
  StateClosing ConnState = 4
  StateClosed ConnState = 5
)

interface

implicitly interface

What makes Go’s interfaces special is that they are implemented implicitly.

A concrete type does not declare that it implements an interface.

If the method set for a concrete type contains all of the methods in the method set for an interface, the concrete type implements the interface.

This means that the concrete type can be assigned to a variable or field declared to be of the typeof the interface.

下面看一个示例:

package main

import "fmt"

type LogicProvider struct {

}

func (lp LogicProvider) Process(data string)  string{
	return fmt.Sprintf("LogicProvider :%s",data)
}

type Logic interface {
	Process(data string)  string
}

type Client struct {
	l Logic
}

func main()  {
	c:= Client{
		l:LogicProvider{},
	}
	fmt.Println(c.l.Process("aaa"))


}

输出

LogicProvider :aaa

what implicit interface benefit

先说 implicit interface 和 explicit interface的区别:
implicit interface是定义在消费端的;而explicit interface是定义在服务端的

refer to: https://www.fareez.info/blog/why-interfaces-in-go-are-not-explicitly-implemented/

举一个例子:
现在有一个服务端:用户服务(user server )
有2个客户端:kfc_pre kfc_delivery

假设这个user server 对接了kfc的2个渠道 kfc_pre kfc_delivery,这2个渠道,代表了2个不同的client

我们先看Java中,使用explicit interface是怎么处理的:

原来有一个创建用户的功能,是根据uuid创建用户的。

这个功能实现是这样的:user server端提供了一个接口UserCreator,并且给这个接口提供了一个实现类。

然后被kfc_pre kfc_delivery这2个client依赖,这样,client就能够利用这个接口和对应的实现类,去实现创建用户了。

但是呢,现在kfc_delivery有一个新的需求,根据手机号,来创建用户。为了不改动原来的代码,即不能创建新的接口;我们能做的就是,给这个接口添加1个新的方法,这个方法是根据手机号,来创建用户。同时,需要更改UserCreator的实现类的代码。

但是,注意,这个接口是服务端定义的,给接口添加新方法,更改实现类的代码,也会同时影响到kfc_pre。这个新增的方法中的实现,有没有bug不清楚,对kfc_pre有没有影响,也不清楚


为了解决上面的问题,我们再来看看 implicit interface 是怎么实现的:

在go中,接口是定义在客户端的,因此, kfc_pre定义的接口为PreUserCreator 、kfc_delivery定义的接口为DeliveryUserCreator。

在PreUserCreator和DeliveryUserCreator的各自实现类中,调用user server提供的方法,来创建用户

现在kfc_delivery有一个新的需求,根据手机号,来创建用户。为了不改动原来的代码,我们可以常见一个新的接口,PhoneDeliveryUserCreator,在这个PhoneDeliveryUserCreator接口的实现类中,我们可以引用服务端新提供的url,来创建用户。

因此,对kfc_delivery原来的,根据uuid创建用户的功能,没有任何影响;同时PreUserCreator接口,不需要做任何改动,因此不会影响到kfc_pre

Embedding interface

前面,我们看了embedded field,是2个struct之间,有嵌入关系

下面,我们再看看Embedding interface,即2个interface之间,有嵌入关系

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type ReadWriter interface {
	Reader
	Writer
}

interface之间,通过Embedding后,ReadWriter接口,就有2个method:Read 和 Write

通过上面的示例,Embedding其实也是一种composition方式,他能够让多个小接口,组合后,变成一个大接口。这样,每个小接口,职责单一,逻辑清晰。

Accept interfaces, return structs

What this means is that your functions should be invoked via interfaces, but the output of your functions should be a concrete type.


We’ve already covered why functions should accept interfaces; they make your code more flexible and explicitly declare exactly what functionality is being used.


If you create an API that returns interfaces, you are losing one of the main advantages of implicit interfaces: decoupling. You want to limit the 3rd party interfaces that your client code depends on because your code is now permanently dependent on the module that contains those interfaces, as well as any dependencies of that module, and so on.

This limits future flexibility. 


Another reason to avoid returning interfaces is versioning. If a concrete type is returned, new methods and fields can be added without breaking exiting code. The same is not true for an interface. Adding a new method to an interface means that you need to update all existing implementations of the interface, or your code breaks. If you make a backwards-breaking change to an API, you should increment your major version number.

nil in interface

an interface to be considered nil both the type and the value must be nil


In the Go runtime, interfaces are implemented as a pair of pointers, one to the underlying type and one to the underlying value.
As long as the type is non-nil, the interface is non-nil
var s *string
fmt.Println(s == nil) // prints true
var i interface{}
fmt.Println(i == nil) // prints true
i = s
fmt.Println(i == nil) // prints false, because interface type pointer is non-nil:*string
What nil indicates for an interface is whether or not you can invoke methods on it.

If an interface is nil,invoking any methods on it triggers a panic .

If an interface is non-nil, you can invoke methods on it. (But note that if the value is nil and the methods of the assigned type don’t properly handle nil , you could still trigger a panic.)

empty interface type—interface{}

var i interface{} //  interface{} is a type format, note the {},cannot leave off

i = 20
i = "hello"
i = struct {
	FirstName string
	LastName  string
}{
	"Fred",
	"Fredson",
}

An empty interface type simply states that the variable can store any value whose type implements zero or more methods.

type LinkedList struct {
	value interface{}
	next  *LinkedList
}

type conversion & type assertion & type switch


why we need type assert and type switch?

If you find yourself in a situation where you had to store a value into an empty interface, you might be wondering how to read the value back again. To do that, we need to look at type assertions and type switches.


Type conversions can be applied to both concrete types and interfaces and are checked at compilation type. 

Type assertions can only be applied to interface types and are checked at runtime. Because they are checked at runtime, they can fail.

Conversions change, assertions reveal.

type conversion

var x int = 65 
var y   = string( x ) 
fmt . Println ( y )

type assertion

func main() {
	var i interface{} = 42
	//
	fmt.Println(i.(int))  // 42
}

我们可以看到,type assertion的语法:

interfaceVariable.(type) is the syntax.
package main

import "fmt"

type MyInt int

func main() {
	var i interface{}
	var mine MyInt = 20
	i = mine
	i2 := i.(MyInt)
	fmt.Println(i2 + 1)  //21

	//
	i3 := i.(int)
	fmt.Println(i3+6)  // panic: interface conversion: interface {} is main.MyInt, not int

}

通过的demo中,报了panic,我们可以知道,

As we’ve already seen, Go is very careful about concrete types. Even if two types share an underlying type, a type assertion must match the type of the underlying value

type switch:

package main

import "fmt"

type MyInt int

func main() {
	var i interface{}
	var mine MyInt = 20
	i = mine
	doThings(i)

}

func doThings(i interface{}) {
	switch j:= i.(type) {
	case nil:
		fmt.Println(j," this is a nil")
	case int:
		fmt.Println(j," this is a int")
	case MyInt:
		fmt.Println(j," this is a MyInt")
	case string:
		fmt.Println(j," this is a string")
	default:
		fmt.Println(j," this is a unknown type")

	}
}

输出

20  this is a MyInt

use the function type implement interface

先定义一个接口

type Handler interface {
	ServeHTTP(s string)
}

这个接口,有一个方法ServeHTTP,如果是用1个struct来实现这个接口,那么我们就需要,在struct的ServeHTTP方法中,编写 业务逻辑,这样struct就是接口的实现类了。

在go中,处了能用struct来实现接口,还可以使用function type来实现接口,也就是说function type成为了实现类。如下:

type HandlerFunc func(s string)

func (f HandlerFunc) ServeHTTP(s string) {
	f(s)
}


上面,定义了一个function type ,并且给这个function type 增加了一个ServeHTTP method。这个形式,就代表,HandlerFunc这个function type,成为了Handler接口的实现类了。

接下来,我们再看这个ServeHTTP方法,我们可以看到方法中,是直接调用f(s) ,这个也比较容易理解,因为f是1个函数,所以,可以直接使用(),来实现函数执行

但是上面,还没有说到,我的业务逻辑,应该写在哪里?比如,业务逻辑,就是打印一行字符串,那应该写在哪里呢?如下:

func weipengHandler(s string)  {
	fmt.Println("----------",s,"---------")
}

这里,我们需要搞清楚,这几个东西的概念和关系:

weipengHandler—function instance
HandlerFunc-------function type
Handler------------interface

之前我们说过,function instance可以看做value,赋值给一个变量。而function type属于type,表示function instance是什么样的类型。就类似于,function type是person,而function instance是张三

而这里,又使用了function type,来实现interface。

所以,综合起来看,逻辑是,我们有一个接口(Handler),又创建了一个接口的实现类(HandlerFunc),接着又给这个实现类,创建了一个实例化对象(weipengHandler)。接下来,我们使用这个对象,来实现一些功能和效果。



完整例子如下:

package main

import (
	"fmt"
)


func main() {
	h := HandlerFunc(weipengHandler)  // type conversion  将weipengHandler转为HandlerFunc
	h.ServeHTTP("weipeng123456")

}

type Handler interface {
	ServeHTTP(s string)
}

type HandlerFunc func(s string)

func (f HandlerFunc) ServeHTTP(s string) {
	f(s)
}

func weipengHandler(s string)  {
	fmt.Println("----------",s,"---------")
}

输出

---------- weipeng123456 ---------

当一些small interface ,特别是只有一个方法的接口,我们完成可以使用function type 去实现接口,没必要重新定义一个struct type,让这个struct type去实现接口

implicit interface make dependent inject easy

在开始讨论这个话题之前,我们先思考下,为什么需要依赖注入呢?

因为,我们的版本,是在不断迭代升级的,特别是日志工具类 和 数据库工具类;但是我们的业务逻辑,不能因为这些工具类升级,就不断的修改。

因此,工具类的provider需要创建一个接口,然后提供多个实现类,实现这个接口,而我们在业务逻辑代码中,全部使用接口来完成业务逻辑,实际运行时,具体使用哪个接口的实例化对象,一般由spring框架,来对这个接口注入某个实现类的实例化对象。这就是依赖注入。

那如果说,我们能不能脱离spring框架的依赖注入,又能保证,日志工具类的升级,不会让我们的业务逻辑大改呢?

答案是能。



现在服务的provider,提供了如下的功能:

  • 日志工具LogOutput
  • 简单的数据存储工具SimpleDataStore

并且声明了,后面的日志工具 和 数据存储,都会做升级


// function instance
func LogOutput(msg string) {
	fmt.Println(msg)
}


//----------------------------------------

// DataStore 实现类
type SimpleDataStore struct {
	userData map[string]string
}

func (sds SimpleDataStore) UserNameForId(userId string) (string, bool) {
	name, ok := sds.userData[userId]
	return name, ok
}


//  工厂方法,实例化一个SimpleDataStore
func newSimpleDataStore() SimpleDataStore {
	return SimpleDataStore{
		userData: map[string]string{
			"1": "zhangsan",
			"2": "lisi",
		},
	}
}



那作为一个client端的编码人员,我就需要考虑了,我这边使用服务provider提供的日志工具 和 数据存储,来编写业务逻辑。我一旦写完之后,肯定基本就不改复杂的业务逻辑了,不可能说,provider升级了,我还改一遍我的复杂的业务逻辑。

因此,最好的方式,就是我抽象一层,这样,即使provider升级了,我也不用大改,怎么抽象呢?

// 接口
type Logger interface {
	Log(msg string)
}

// Logger实现类
type LoggerAdapter func(msg string)

func (la LoggerAdapter) Log(msg string) {
	la(msg)
}

// ----------------------------------------------------

type DataStore interface {
	UserNameForId(userId string) (string, bool)
}

我抽象出2个接口,在我复杂的业务逻辑中,我全部使用接口,来实现。这样,即使provider升级了,我复杂的业务逻辑不用改,只是在调用时,换一个新的实现类的对象即可。

这里,需要思考下,为什么要引入function type呢?因为provider只提供了一个LogOutput function,而我们无法直接从这个LogOutput function中,提取出一个接口出来,所以使用function type来转一下。

那,我的业务逻辑怎么写呢?如下:

// Logic的实现类
type SimpleLogic struct {
	l  Logger
	ds DataStore
}

func (sl SimpleLogic) sayHello(userId string) (string, error) {
	sl.l.Log("say hello for " + userId)
	name, ok := sl.ds.UserNameForId(userId)
	if !ok {
		return "", errors.New("unknown user")
	}
	return "hello," + name, nil
}

// 工厂方法,实例化一个SimpleLogic
func newSimpleLogic(l Logger, ds DataStore) SimpleLogic {
	return SimpleLogic{l, ds}
}

func main() {
	l := LoggerAdapter(LogOutput)
	ds := newSimpleDataStore()
	logic := newSimpleLogic(l, ds)
    
    // 以下,全部是我的业务逻辑,全部使用接口来实现的
	helloStr1, error1 := logic.sayHello("2")
	if error1 != nil {
		fmt.Println(error1)
	} else {
		fmt.Println(helloStr1)
	}

	fmt.Println("---------------------------")
	
	helloStr2, error2 := logic.sayHello("3")
	if error2 != nil {
		fmt.Println(error2)
	} else {
		fmt.Println(helloStr2)
	}
}

可以看到,只有main函数中最上面的3行,使用了provider提供的实现类;后面的代码,全部是用client端定义的接口,来完成的。这样,即使provider升级了,我们也只需要改main函数中,最上面的3行代码即可。

输出

say hello for 2
hello,lisi
---------------------------
say hello for 3
unknown user

和java对比:

java通过显示的接口,client 和 provider 都显示绑定了这个接口,而go只有client绑定了这个接口,provider并没有显示的绑定这个接口,而是用过implicit 的方式

This is very different from explicit interfaces in languages like Java.

Even though Java uses an interface to decouple implementation from interface, the explicit interfaces bind the client and the provider together.

This makes replacing a dependency in Java (and other languages with explicit interfaces) far more difficult than it is in Go.

backward-compatiable

type alias

After using a module for a while, you might realize that its API is not ideal.

you want to rename or move an exported type, you have to use something we haven’t seen before, an alias.

Quite simply, an alias is a new name for a type.

首先,我们需要考虑下,在什么场景下,需要使用type alias

假设,现在,我们有如下代码:

type Foo struct {
	x int
	S string
}

func (f Foo) Hello() string {
	return "hello"
}
func (f Foo) goodbye() string {
	return "goodbye"
}

后面,我发现,Foo这个名字起的不太好,我想换一个名字,但是业务逻辑已经写好了。如果改名字的话,涉及到的业务逻辑,都需要改,风险太高了。

这个时候,我就想,能不能给这Foo起一个别名。就类似于大名和小名,在家里小名叫做 小宝,在学校大名叫张三丰,其实都是同一个人。

go能够实现这个别名的需求,如下:
家里叫Foo,在学校叫Bar。


type Bar = Foo

更加重要的是,The alias can even be assigned to a variable of the original type without a type conversion:


func MakeBar() Bar {
	bar := Bar{
		x: 20,
		S: "Hello",
	}
	var f Foo = bar
	fmt.Println(f.Hello())
	return bar
}


下面,我们来看下,type alias,有哪些特性:

  • The alias has the same fields and methods as the original type

  • the variable of alias type 和 the variable of the original type,can assign each other, without a type conversion:

  • 给alias type添加的方法,the variable of the original type可以调用;给original type添加的方法,the variable of alias type也可以调用

  • You can alias a type that’s defined in the same package as the original type or in a different package. You can even alias a type from another module.


针对上面的第3点,我们看一个例子,展示了给alias type添加的方法,the variable of the original type可以调用

func main() {
	f := Foo{
		x: 20,
		S: "Hello",
	}
	fmt.Println(f.sing()) // sing happy
}

type Foo struct {
	x int
	S string
}

func (f Foo) Hello() string {
	return "hello"
}
func (f Foo) goodbye() string {
	return "goodbye"
}

type Bar = Foo

func (b Bar) sing() string {
	return "sing happy"
}


针对上面的第4点,我们需要注意下:There is one drawback to an alias in another package: you cannot use an alias to refer to the unexported methods and fields of the original type.


评论