πŸ“œ ⬆️ ⬇️

Go application development: logic reuse

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:


Main package


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.


os.Exit


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 ...


func main ()


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.


Project outline


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 .


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.


Run


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.


Putting it all together


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.


Larger projects


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