一、 方法的概念
如果我告诉你,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!