golang中的方法集合

98 阅读8分钟

一、 方法的概念

如果我告诉你,go中既有方法,也有函数,你也许会困惑,方法和函数不是一回事吗?

方法是以所绑定的接收器(receriver)实例作为第一个传入参数的函数。在golang中函数和方法区别为,是否有接收器。

下面通过一段代码实例,了解方法如何表示为函数的形式,并了解方法表示式和方法集合的概念。

type A struct {
     num int 
}
  
func (a A) get(){
  fmt.println(a.num)
}
// 等价于(以实例作为传入参)
func get(a A){
   
    fmt.println(a.num)
}
​
func (b * A) set(num int ){
     a.num = num
}
​
//等价于
func  set(b * A,num int ){
     b.num = num
}
​
 
func main(){
    var a A
    var b * A 
   a.get() 
   b.set(1)
  
  //方法表达式
  A.get(a)
  (*A).set(b,1 )
  
 
}

从上面的代码可以看出,方法可以改写成函数的格式,传入的第一个参数为方法从属类型的实例。

什么是方法表达式? 方法表达式和方法集合有什么关联?

方法表达式,是一种以类型名来调用所拥有方法的表达式。一个类型所能调用的所有方法,称为方法集合。值类型型A只能调用A的方法集合。但*A的方法集合 = A的方法 + *A的方法。所以, *A 可以调用A的方法,但A 去调用 * A的方法就会报错。

A.set(1)     // set()属于 *A的方法集合,调用会报错

但是, 如果使用的使用类型的实例去调用方法,而不是使用方法表达式的时,无论是 A的实例,还是 *A的实例,都可以调用A和 *A 的方法集合。

//这两种调用方式都是正确的 
var b *A 
var a A  
​
b.get()
b.set(1)
​
a.get()
b.set()

此外,golang里接收器类型的问题,本质上就是函数参数类型问题:值传递,还是应用传递。

  • 值只传入副本,不影响原始的实例;引用传递则是传入地址,可以修改实例的值。
  • 值传递的开销更大 。
  • 考虑值类型是否要实现某个接口

方法集合决定接口实现。 通过判断结构体的方法集合是否包含了接口的方法集合,来判断某个结构体,是否实现某个接口。思考下面的代码中,为何将结构体实例赋值给接口变量时报错。

 type In interface {
    A()
    B()
 }
 
 type ob struct{
    
 }
 
 func(o *ob) A(){
 
 }
 func(o ob)B(){
 
 }
 
 func main(){
    
    ob1 := ob{}
    ob2 := &ob{}
    var  i In
    i = ob1        //报错
    i = ob2 
 }

从方法集合的角度可以看出,In的方法集合有A()和B() ,ob类型的方法集合只有方法A(), *ob集合的方法集合有A()和B() 。方法集合决定接口实现。ob没有实现接口In, 因此不能进行赋值。

二、嵌套和方法集合

结构体和接口两两嵌套,一共有三种组合:接口-接口 、 结构体- 接口 、 结构体-结构体

1.接口-接口:

在一个接口中,嵌入一个或者多个接口。相应地,方法集合也会增加对应的方法。下方代码中,C的方法集合包含: f1()、 f2()、 f3()。

type A interface{
     f1()
}
​
type B interface{
    f2()
}
​
type C interface{
   f3()
   A
   B
}

在老版本的go中,不能嵌套两个包含相同方法的接口。但在新版本中取消了这个规定。

2.结构体 - 接口:

在结构体中嵌套,嵌套一个或者多个接口。这里有一个方法调用次序的问题:结构体有方法A() , 嵌套的接口中也有方法A() , 当调用方法A时,优先调用哪个方法呢?

答案是,优先调用从属与类型的方法,而非嵌套的方法。在方法调用时,会优先调用直接从属的方法,找不到再去调用嵌套的方法。因此,嵌入的两个接口有相同的方法时,如果没有直接从属的方法时,就会报错。

// chapter4/sources/method_set_6.go
​
type Interface interface {
    M1()
    M2()
}
​
type T struct {
    Interface
}
​
func (T) M1() {
    println("T's M1")
}
​
type S struct{}
​
func (S) M1() {
    println("S's M1")
}
func (S) M2() {
    println("S's M2")
}
​
func main() {
    var t = T{
        Interface: S{},
    }
​
    t.M1()
    t.M2()
}

注意,接口是需要通过结构体的实例来赋值的。但只有被赋值之后,才能调用响应的接口方法。

在这里,通过实现接口实现了多态。

3.结构体 - 结构体:

首先,初学者都会有一个普遍的困惑:组合引用类型和非引用类型有什么区别? 从方法集合

type C struct{
    A 
    *B
}

从方法集合来看,

C的方法集合  =  A的方法集合 + *B的方法集合 + 直接从属C的方法集合

*C的方法集合= *A的方法集合 +*B的方法集合 + 直接从属*C的方法集合

从实例调用的角度看,无论是C的实例,还是*C的实例,都可以调用*C的方法集合中的方法,

 

三:水平组合 

水平组合,是通过将接口函数/方法参数的形式,从而实现接口的横向扩展。常见形式为:

func

包裹函数(wrap function): 包裹函数是接口水平组合的方式之一。包裹函数的输入包含接口,返回也是同样的接口类型。包裹函数可以对传入的参数进行修改。常见的形式为:

 func MyFunction(myInterface)  myInterface 

常见的比如, wrapCode 函数。输入接口 error 。返回为,也为 error 接口的实例。从 Error 的定义可以看出,其组合了 error 接口,并重新实现了 error() 方法。

func WrapCode(code gcode.Code, err error, text ...string) error {
    if err == nil {
        return nil
    }
    return &Error{
        error: err,
        stack: callers(),
        text:  strings.Join(text, commaSeparatorSpace),
        code:  code,
    }
}
​
type Error struct {
    error error      // Wrapped error.
    stack stack      // Stack array, which records the stack information when this error is created or wrapped.
    text  string     // Custom Error text when Error is created, might be empty when its code is not nil.
    code  gcode.Code // Error code if necessary.
}

包裹函数可以嵌套调用,每个函数分别对接口进行一定程度的修改。

  interface : =  A( B( C( a myInterface)))

适配器函数类型(adapter function type): 属于一种基于函数定义的类型。通过类型转化,可以将函数转化成某个类型的实例,转化后的实例,实现了某个方法,该方法内的实现了方法本身。 从结果上看,将某个方法转化成接口实例。接口实例的方法,可以调用原来的方法。

func birdCall() {
    fmt.Println("hiahiahiahia")
}
​
type animal interface {
    call()
}
​
type animalCallFunc func()
​
func (f animalCallFunc) call() {
    f()
}
​
func animalFeature(animal2 animal) {
    animal2.call()
}
​
func TestAdapter(t *testing.T) {
​
    a := animalCallFunc(birdCall)
    a.call()
​
}
​

在例子中 type animalCallFunc func() 定义了一个类型, animalcallFunc , 可以将一般的方法转化成某个类型的实例,a := animalCallFunc(birdCall) ,并且这个实例的方法 call(), 就是原来的方法。

1.定义适配器函数类型。

    1.1使用 type关键字定义函数类型。注意,函数类型的输入输出参数,要和原始的函数一致

   1.2定义归属于该类型的方法。 注意,在方法的body中,是递归调用了类型本身。

2.使用定义的类型,将函数转化成接口类型。

有了适配器,就可以把函数转化成接口的实例;否则,就需要创建一个结构体,来绑定这个方法。而且有多个函数时,就需要创建多个结构体。

为什么要将函数转化成接口的实例? 因为这样更加方便管理。

// 统一的日志接口
type Logger interface {
    Info(msg string)
    Error(msg string)
}
​
// 适配器函数类型
type LogFunc func(msg string)// 实现 Info 方法
func (f LogFunc) Info(msg string) {
    f(fmt.Sprintf("[INFO] %s", msg))
}
​
// 实现 Error 方法
func (f LogFunc) Error(msg string) {
    f(fmt.Sprintf("[ERROR] %s", msg))
}
​
// 使用示例
func main() {
    // 控制台日志
    consoleLogger := LogFunc(func(msg string) {
        fmt.Println(msg)
    })
​
    // 文件日志
    fileLogger := LogFunc(func(msg string) {
        // 写入文件
        file, _ := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
        fmt.Fprintln(file, msg)
    })
​
    // 使用日志
    consoleLogger.Info("这是控制台日志")
    fileLogger.Error("这是文件日志")
}
​

中间件传入

中间件是由包裹的链式调用,以及适配器类型组合在一起形成,并且将适配器类型作为返回

logHandler 中,代码中 return内容如下,也是一个被 http.HandlerFunc 类型转化的实例。

http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t := time.Now()
        log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t)
        h.ServeHTTP(w, r)
    })

下面也是一个实例

// chapter5/sources/horizontal-composition-4.go
​
​
// net/http 包中的 Handler 接口定义
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
​
​
func validateAuth(s string) error {
    if s != "123456" {
        return fmt.Errorf("%s", "bad auth token")
    }
    return nil
}
​
func greetings(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}
​
func logHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t := time.Now()
        log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t)
        h.ServeHTTP(w, r)
    })
}
​
func authHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        err := validateAuth(r.Header.Get("auth"))
        if err != nil {
            http.Error(w, "bad auth param", http.StatusUnauthorized)
            return
        }
        h.ServeHTTP(w, r)
    })
}
​
func main() {
    http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings))))
}

是 log 还是 auth 先执行呢? 答案是外层的 log

从外层往内层看,外层的 log 返回的形式为

 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t := time.Now()
        log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t)
        h.ServeHTTP(w, r)
    })

这里返回的是http.Handler的实例,通过类型适配器 http.HandlerFunc 将闭包(匿名函数) 转为成 http.HandlerFunc 类型。这里的闭包转化和下面传图方法的签名来转化,本质上是一样的。

 http.HandlerFunc(greetings)

http.ListenAndServe() 的内部,会调用 h.ServeHTTP(w, r) ,从而调用最外层的 log 返回实例的方法。而在 log 这一层的 h.ServeHTTP(w, r),又调用了下一层 的 h.ServeHTTP(w, r) 所返回实例的方法。到了 auth 层级,调用 h.ServeHTTP(w, r) 时,就等价于调用 greeting() 方法。

可以在IED中debug,看看代码的调用层级。

$ go run horizontal-composition-4.go$curl http://localhost:8080
bad auth param
​
$curl -H "auth:123456" localhost:8080/
Welcome!