There are some singleton implementations in the Go standard library, such as the default Logger
and net.DefaultResolver
in the log
package, which provide convenient methods, but there are times when we need to do some customization and need to change these objects.
There are even times when we need to change specific methods of the standard library, and the conventional means do not work, we must use some “hacking” methods.
In the process of developing the project, I also encountered some needs to change the default behavior of the standard library, so I did some exploration in this area and put together this article for the benefit of readers.
If you want to implement your own logging library (and there are many, many logging libraries in the Go ecosystem), you may want to “intercept” the standard library’s default Log, so that your code, or third-party code that outputs logs from the standard library log
, can be fed through your own logging library.
In fact, the standard library log
default Logger is defined as follows: var std = New(os.Stderr, "", LstdFlags)
, std implements a Logger
that outputs to os.Stderr
. The Go standard library Logger is not an interface, so you may not be able to do much customization per se, but at least you can change the destination of the log output, for example from the standard err output to a log file. Here std
is the unoutput variable, but the standard library provides the func SetOutput(w io.Writer)
method to change the output destination.
So it seems that the log library is ok, at least it exposes a way to change the customization, but there are many cases where the standard library does not provide a way to customize, or a way to not facilitate customization.
When I was implementing a project these days, I came across a situation where a machine had multiple IPs.
It is not uncommon for a machine to have multiple IP addresses configured, so when you connect to other TCP servers on this machine, which IP address is used locally? As serverfault has asked, by default Linux will choose the local address of the same subnet as the server according to the subnet classification, but if multiple IP addresses are configured in the same subnet, then Linux will choose the “primary” IP address of this subnet as the local Ip address to connect to the server.
In my project, there are many network connections, such as to mysql, to clickhouse, to third-party HTTP API services, to Kafka, to Redis, and so on. Unfortunately, when using third-party libraries like go-sql-driver/mysql, go-redis, the local IP address selected by Linux is not the local IP address I expect, resulting in permission verification failures and inability to connect.
Essentially, either go-sql-driver/mysql
or go-redis
, both are TCP connections established based on net.Dial
or `net.
go-sql-driver/mysql
provides RegisterDialContext
for customizing Dial
, go-redis
provides Dialer
field for customization, and you can specify local IP address if you want by customizing `net.
This is a good, traditional method, the only irritation is that for each type I need to address, access mysql, access redis, access kafka, access third party libraries, access the server …… , is there a one-and-done approach?
There is!
bouk/monkey is a rather “hacky” technique that dynamically replaces method implementations with JMP new functions
at runtime to achieve method replacement at runtime. We often use it to Mock
some methods during unit testing, and it works very well. This time, I’m going to try to use it to replace all `net.
Of course, agiledragon implements agiledragon/gomonkey based on its principle to facilitate calls, but it does not support temporary recovery of the original function at present. Chunhui Cao has implemented cch123/supermonkey based on it, which can replace the unexported function by parsing the symbol table to get the function pointer, which can be said to be more powerful. In this article, we still use the original bouk/monkey, because for me, the function is enough.
Don’t use bouk/monkey to do evil.
You can use bouk/monkey to replace the net.Dialer.Dial
or net.Dialer.DialContext
functions of the standard library to use the local IP address when establishing a TCP connection. This way, either mysql’s library, redis’s library, or any other third-party library that is based on the net.Dialer.Dial
or net.Dialer.DialContext
function will use the method we replaced.
The relevant code is also very simple, as shown below, and comments have been added to the code:
|
|
After replacing these two methods, even if a new net.Dailer
object is created afterwards, it will be executed using the replaced methods.
If your program has concurrent calls to Dial or DialContext, you need to add locks.
Thus, we have solved the problem of specifying local IP addresses to create TCP connections once and for all, without changing the code of the standard library and without customizing Dial methods individually.
Similarly, you can change the standard library’s net
.
This is the implementation of the standard library for domain name resolution, and supports Go’s own resolution implementation and CGO queries. It is a struct in itself, not an interface, so although it is a singleton object, you usually don’t have much customization possibilities. For example, when calling the LookupIP
method, you want to use a protocol of your own to return a list of IPs, rather than querying local files or DNS servers, you basically have no way to do that. But with bouk/monkey, you can change the LookupIP
method so that you can customize it.
So, bouk/monkey can be used not only to mock objects and methods in unit tests, but also to replace functions that are not routinely changed while the application is running.