In my opinion, writing libraries on Go is a fairly well-lit topic ... but there are much less about writing applications (teams) of articles. When it comes to this, all the code on Go is a command. So let's talk about it! This post will be the first in the series, because I have a lot of information that I have not yet shared.
In today's article we will focus on the basic layout of the project, we will improve the reuse and test code.
When I develop not a library, but a program, I have three unique code organization rules:
This is the only package in the Go program that must be available. In addition to instructing the go
tool to create an executable file, this package has another unique thing β you cannot import code from it. This means that any code you put into the main
package cannot be used directly by another project, and this upsets the gods of open source software. Since one of the main reasons why I am writing open source projects is that other developers can use it, this inability to reuse the code from main
against my wishes.
Many times there were situations when I thought: "I would use the logic of program X in my code." But if the logic was in the main
package, it was impossible.
If you care about creating a program that does exactly what the user expects, then you should take care of what exit code it ends with. The only way to do this is to call os.Exit
(or call something that calls os.Exit
, for example log.Fatal
).
However, you cannot test the function that calls os.Exit
. Why? Because the os.Exit
call during the execution of the test will lead to the termination of the application under test . It is quite difficult to detect if you got it by chance (I know this from personal experience). When you start testing, no testing actually takes place, these tests simply end earlier than they should have, and all you have to do is scratch your head.
The simplest thing to do is not to call os.Exit
. Most of your code in any case should not call os.Exit
... someone can really "go to the roof" if he imports your library, and it will randomly, under certain conditions, stop its application.
Thus, call os.Exit
exactly one place, as close as possible to the "appearance" of your application, with the minimum number of entry points. By the way, let's talk about them ...
This is the only function any Go program should have. You probably think that every main
function should differ from program to program, since all programs are different, right? Well, it turns out, if you really want to make your code testable and reusable, there is, by and large, only one correct answer to the question "what is in your main function?"
Looking ahead, I think that there is also only one correct answer to the question "what is in your main package?" and this answer looks like this:
// command main documentation here. package main import ( "os" "github.com/you/proj/cli" ) func main() { os.Exit(cli.Run()) }
That's all. This is the most minimal code that should be in your useful main
package. We spent almost no effort on code that others cannot reuse. At the same time, we isolated os.Exit
in a single-line function, which is the outermost part of our project and, in fact, does not need to be tested.
Let's take a look at the overall design of the project:
/home/you/src/github.com/you/proj $ tree . βββ cli β βββ parse.go β βββ parse*test.go β βββ run.go βββ LICENSE βββ main.go βββ README.md βββ run βββ command.go βββ command*test.go
We already know what we have in main.go
... and, in fact, main.go
is the only go file in the main
package. The LICENSE
and README.md
files are LICENSE
explanatory. (Always indicate the license! Otherwise, many people will not be able to use your code.)
Now we go to two subdirectories - run
and cli
.
The cli
package contains the command line parsing logic. Here you define the interface (UI) of your program. The package contains the analysis of flags, the analysis of arguments, help texts and so on.
It also contains the source code, which returns the exit code for the main
function (which passes it to os.Exit
). That way, you can test the exit codes returned by these functions, instead of trying to test the exit codes of your program as a whole.
If the main
and cli
are the "bones" of the logic of your program, then the run
package contains its "meat". You should write this package as if it were a separate library. During its development, you should not think about CLI, flags, and the like. The packet should receive the structured data and return errors. Imagine that it might be called by another library, or a web service, or someone elseβs program. Make as few assumptions as possible about how it will be applied ... well, it should be a regular library.
Obviously, larger projects will require more than one directory. In fact, you can divide your logic into separate repositories. It depends on how likely you think that other people will want to reuse your logic. If you think this is very likely, I recommend implementing the logic in a separate directory (repository). In my opinion, a separate directory for logic requires more quality and stability than some random directory, hidden somewhere deep in the repository.
The cli
package forms the command line interface for the logic implemented in the run
package. If someone sees your program and wants to use its web API logic, they will just need to import the run
package and use this logic directly. Similarly, if someone doesnβt like your command line parameters, they can simply write their own argument parser and use it as an interface to the run
package.
This is what I mean when it comes to code reuse. I donβt want anyone to βhackβ pieces of my code to use it more. And the best way to facilitate code reuse is to separate the interface from the logic. This is the key part. Do not let ideas from your interfaces (UI / CLI) leak into the logic . This is the best way to keep logic in a general form, and the interface is manageable.
This scheme is good for small and medium projects. There is a single program that is in the root of the repository, so it is easier to get (go-get) than if it were in several subdirectories. In large projects, things can be completely different. There may be several executable files, but they cannot all lie in the root of the repository. However, such projects usually have custom assembly steps and require more than just go-get (which I will discuss later).
Details will be soon.
October 18, 2016
Series: Go application development.
Note: the series as such has not happened, this is the only article in the βseriesβ since its publication. However, the article is quite interesting.
Source: https://habr.com/ru/post/325454/
All Articles