Go标准库中有一些单例的实现,比如log
包中默认的Logger
、net.DefaultResolver
, 这些对象提供了便利的方法,但是有的时候,我们需要做一些定制话的功能,需要更改这些对象,
甚至有的时候,我们需要更改标准库的特定方法,常规手段是不起作用的, 必须使用一些"骇客"的方式。
国庆北京连绵秋雨,整好我窝在家里,实现了一个原本想12月份实现的产品,在开发项目的过程中,也遇到了一些需要更改标准库默认行为的需求,所以在这方面做了一些探索,整理出这篇文章,以飨读者。
如果你想实现自己的一个日志库(事实上Go生态圈已经有很多很多的日志库了),你可能想"拦截"标准库的默认的Log,这样你的代码,或者第三方代码中通过标准库log
输出的日志都能通过你自己的日志库输入出来。
其实标准库log
默认的Logger是这样定义的: var std = New(os.Stderr, "", LstdFlags)
, std实现了一个输出到os.Stderr
的Logger
。Go标准库中的Logger不是一个接口,所以本身你可能还不能做太多的定制化的改造,但是至少,你可以改变日志输出的目的地,比如从标准err输出改到日志文件中。这里std
是未输出的变量,但是标准库提供了func SetOutput(w io.Writer)
方法,用来更改输出目的地。
这样看来,日志库还好,至少还暴露了一个更改定制化的方法,但是有很多情况下,标准库并没有提供定制的方法,或者说不方便定制的方法。
这几天我在实现项目的时候,遇到了一台机器有多个IP的情况。
一台机器配置了多个IP并不罕见,当你在这台机器连接其它的TCP服务器时, 本地到底使用的是哪一个IP地址呢?如serverfault有人提出的问题,在默认的情况下,Linux会依照子网的分类,选择和服务器在相同的子网的本地地址,但是如果同一个子网中配置了多个IP地址,那么Linux会选择此子网的"主"IP地址作为本地Ip地址连接服务器。
在我这个项目中,会有很多的网络连接,比如连接mysql,连接clickhouse,连接第三方的HTTP API服务,连接Kafka、连接 Redis等。不幸的事,当使用第三方库比如go-sql-driver/mysql、go-redis时,Linux所选择的本地IP地址并不是我期望的本地IP地址,导致权限验证失败无法连接。
本质上,无论是go-sql-driver/mysql
或者go-redis
,都是基于net.Dial
或者net.DialContext
建立的TCP连接。go-sql-driver/mysql
提供了 RegisterDialContext
用于定制化Dial
,go-redis
提供了Dialer
字段用来定制,你如果想指定本地的IP地址,可以通过定制的net.Dialer
来实现:
|
|
这是一个不错的、传统的方法,唯一不爽的是,每种类型我都需要进行地址,访问mysql、访问redis、访问kafka、访问第三方库、访问服务器......, 有没有一劳永逸的方法呢?
有!
bouk/monkey是一个相当相当"骇客"的技术,当过运行时动态将方法的实现替换成JMP 新的函数
, 来实现在运行时替换方法。经常我们会在单元测试的时候用来"Mock"一些方法,非常的有效,这一次,我要尝试使用它替换所有的net.Dialer.Dial
或者net.Dialer.DialContext
方法,来实现强制指定本地地址。
当然,agiledragon基于它的原理实现了agiledragon/gomonkey以方便调用,不过目前不支持临时恢复原函数。曹春晖基于它实现了cch123/supermonkey,通过解析符号表得到函数指针,可以替换未输出的函数,可以说功能更强大了。本文中还是使用原始的bouk/monkey,因为对于我来说,功能足够了。
不要使用bouk/monkey做恶。
可以使用bouk/monkey替换标准库的net.Dialer.Dial
或者net.Dialer.DialContext
函数,在建立TCP连接的时候,使用本地IP地址。这样,无论是mysql的库、还是redis的库,或者其它的第三方库,只要基于net.Dialer.Dial
或者net.Dialer.DialContext
函数,就会使用我们替换的方法。
相关代码也非常简单,如下所示,注释已经加到代码中:
|
|
替换了这两个方法后,之后即使新建立net.Dailer
对象,也是使用替换后的方法执行。
这里并没有考虑并发定位情况,如果你的程序有并发的调用Dial或者DialContext,你需要加锁。
这样,我们就一劳永逸的解决了指定本地IP地址创建TCP连接的问题,无需改动标准库的代码,无需逐个定制Dial方法。
同样的,你可以更改标准库的net.DefaultResolver
, 这是标准库用来进行域名解析的实现,支持Go自己的解析实现和CGO方式查询。本身它是一个struct,而不是一个接口,所以虽然它是一个单例的对象,但是通常情况下你也没有多少定制化的可能。比如在调用LookupIP
方法时,你想使用自己的一个协议返回IP列表,而不是查询本地文件或者DNS服务器,你基本是没有办法的。但是通过bouk/monkey,你可以更改LookupIP
方法,这样你就可以定制了。
所以,bouk/monkey不仅仅可以用来在单元测试中mock对象和方法,还可以在应用运行中替换一些常规没有办法更改的函数。