Illustration: pixabay
Not long ago my friends and I came up with the idea of merging our IRC bots and rewriting them with Go. To prevent overwriting most of the existing functionality, we tried to find existing libraries that support the Web apis used in bots programs. Our project requires a library of the Reddit API. This article is inspired by the first three libraries I found, and I’m not going to name them to avoid dishonoring their authors.
Each of the above libraries has some basic problems that make them unusable in real life scenarios. And each library is written in such a way that it is impossible to fix the problem without modifying the API of an existing library in a way that is not backward compatible. Unfortunately, many other libraries have the same problem, so I’ll list some of the authors’ mistakes below.
Don’t takeHTTP Client
Hard coded
Many libraries include hard coding for HTTP. DefaultClient. While this is not a problem for the library itself, the library authors do not understand how to use HTTP.defaultClient. As the Default Client recommends, it should only be used if the user does not provide another HTTP. client. In contrast, many library authors are happy to hardcode parts of their code that involve HTTP.DefaultClient, rather than using it as an alternative. This can cause the library to become unavailable in some cases.
First, many of us have read the article Don’t Use Go’s Default HTTP Client (in Production) about HTTP.DefaultClient not being able to customize the timeout. When you can’t guarantee that your HTTP request will complete (or at least wait for a completely unpredictable response), your program may experience strange Goroutine leaks and unpredictable behavior. In my opinion, this makes every library that hardcodes http.DefaultClient unusable.
Second, the network requires some additional configuration. Sometimes a proxy is needed, sometimes the URL needs to be slightly overwritten, and maybe even http.transport needs to be replaced with a custom interface. All of this is easily implemented when a programmer uses their own http.Client instance in your library.
The recommended way to handle http.Client in your library is to use the provided Client, but there is a default alternative if needed:
func CreateLibrary(client *http.Client) *Library {
if client == nil {
client = http.DefaultClient
}
.
}
Copy the code
Or if you want to remove arguments from factory functions, define a helper method in your struct and let the user set its properties as needed:
type Library struct {
Client *http.Client
}
func (l *Library) getClient() *http.Client {
if l.Client == nil {
return http.DefaultClient
}
return l.Client
}
Copy the code
In addition, if some global feature is required for every request, people often feel the need to replace http.client with their own instance. This is the wrong approach – if you need to set some extra headers in your request, or introduce some kind of common feature into your client, you can simply set it for each request or assemble a custom client instead of replacing it entirely.
Do not introduce global variables
Another anti-pattern is to allow the user to set global variables in a library. For example, in your library allow the user to set up a global http.Client that is executed by all HTTP calls:
var libraryClient *http.Client = http.DefaultClient
func SetHttpClient(client *http.Client) {
libraryClient = client
}
Copy the code
In general, you shouldn’t have a bunch of global variables in a library. When you write code, you should think about what happens when users use your library multiple times in their programs. Global variables make it impossible for different arguments to be used. Also, introducing global variables into your code can cause testing problems and unnecessary complexity in your code. Using global variables may cause unnecessary dependencies in different modules of your program. It is especially important to avoid global states when writing your libraries.
Return structs instead of interfaces
This is a common problem (and actually I’m guilty of it too). Many libraries have functions like the following:
func New() LibraryInterface {
.
}
Copy the code
In the above case, returning an interface hides the struct’s features from the library. It should actually read:
func New() *LibraryStruct {
.
}
Copy the code
There should be no interface declaration in the library unless it is used in a function argument. If the above case occurs, you should think about your convention when writing this library. When you return an interface, you basically have to declare a bunch of methods that are available. If someone wanted to use this interface to implement their own functionality (for testing purposes, say), they would have to scramble their code to add more methods. This means that while it’s safe to add methods to structs, it’s not safe to add methods to interfaces. This idea is best summarized in this article, Accept Interfaces Return Struct in Go. This solution also solves the configuration problem. If you want to modify some features in the library, you can simply modify some public fields in the struct. But if your library only provides the user with an interface, it doesn’t work.
For details, see Go http.client.
Use configuration structures to avoid modifying your APIs
Another configuration method is to receive a configuration structure in your factory function instead of passing configuration parameters directly. You can add new parameters freely without breaking existing apis. The only thing you need to do is add a new field to the Config structure and make sure it doesn’t affect its original features.
func New(config Config) *LibraryStruct {
.
}
Copy the code
The following is the correct scenario for adding a structure field. If a user forgets to add a field name when initializing a structure, this is one scenario where I think it would be forgivable to change their code. To maintain compatibility, you should use Person {name: “Alice”, age: 30} instead of Person {“Alice”, 30} in your code.
You can see the supplement in the golang.org/x/crypto package. In summary, I think it is a better approach for configuration to allow the user to set different parameters in the returned structure, and to use this particular method only when writing complex methods.
conclusion
As a rule of thumb, when writing a library, you should always allow users to specify their own HTTP. Client to perform HTTP calls. And you can try to write code in an extensible way to allow for the impact of future iterations. Avoid global variables. Libraries cannot store global state. If you have any questions – refer to what the standard library says.
I think it’s a good idea to test your library in your application and ask yourself some questions:
-
What happens if you try to get into the library more than once?
-
Do you have unit tests for your library?
-
Is there a non-invasive way to extend your library without breaking the original code?
-
Can additional configuration parameters be added without breaking the original code?
via: https://0x46.net/thoughts/2018/12/29/go-libraries/
By Filip Borkiewicz
This article is originally compiled by GCTT and published by Go Chinese