Trying to understand aversion to main package

305 views
Skip to first unread message

Jerry Londergaard

unread,
Feb 14, 2024, 6:12:49 AMFeb 14
to golang-nuts
I see quite a few modules out there where they seem to be putting in as little into the main package as possible. Literally they will sometimes be a few lines:
```
import foobar
func main() {
    os.Exit(foobar.Run())
}
```
Yet then go on to do all the things I would've thought are the domain of a main package, in the foobar package, like arg parsing, pulling in config items from env vars, setting up deps etc, and then running the actual thing.

Why not just put that code into the actual main package? Sure, maybe not all into the main *function* itself, but in the main package at least.

I understand that you don't want to be putting any real business logic into that main package, but I'm just talking about entry point type specific stuff. People might say they want to keep main 'light', but sometimes it feels like it's a competition to have the fewest lines as possible in the main package.

kube-proxy[1] is an example of this. Maybe this starts to make more sense when the amount of code you would be putting into main become a lot, kube-proxy may qualify for this, but I see other cases where I don't understand the reasoning.

Certainly not all are like that[2], but in some circles that I travel it seems to be a common way of thinking.

Am I missing something ?

1. https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-proxy/proxy.go
2. https://github.com/ethereum/go-ethereum/blob/master/cmd/geth/main.go

Mike Schinkel

unread,
Feb 14, 2024, 6:23:37 AMFeb 14
to golang-nuts
I cannot speak for others but I can tell you why I keep my `main()` small:

1. I prefer to put as much logic into reusable packages as I can, and `main()` is not reusable outside the current app.

2. CLI packages like Viper are configured to be invoked with just one or a few small commands in `main()`, and then there is an idiomatic standard for putting the code for commands in a `/cmd` directory, so that's were most of the code is typically found that is still part of the app but that is not in reusable packages.

Is there a reason this approach is problematic for you?  Just curious. 

If you prefer to put more in `main()` then AFAIK there are no real issues with it other than lacking package reusability, so if it works for you, knock yourself out. #jmtcw

-Mike

Dan Kortschak

unread,
Feb 14, 2024, 6:29:06 AMFeb 14
to golan...@googlegroups.com
This can be used for testing the complete application.

Jerry Londergaard

unread,
Feb 14, 2024, 4:40:19 PMFeb 14
to golang-nuts
On Wednesday 14 February 2024 at 10:23:37 pm UTC+11 Mike Schinkel wrote:
I cannot speak for others but I can tell you why I keep my `main()` small:

1. I prefer to put as much logic into reusable packages as I can, and `main()` is not reusable outside the current app.

This is understandable. I guess I'm talking about things that you probably aren't going to be expecting to be re-imported elsewhere.
 

2. CLI packages like Viper are configured to be invoked with just one or a few small commands in `main()`, and then there is an idiomatic standard for putting the code for commands in a `/cmd` directory, so that's were most of the code is typically found that is still part of the app but that is not in reusable packages.


This also makes sense, if there is an idiomatic standard for things in Viper, then doing it in the expected way is benefit in and of itself.
 
Is there a reason this approach is problematic for you?  Just curious. 

I'm relatively new to Go, so I guess I'm just wondering if there's something else I'm missing. It would seem that having a dedicated package that exists purely as the entry point for the application, and can't be re-imported,
would be the most logical place to put code specific to that purpose that you don't want re-imported.

If it isn't anything beyond personal preference, or maybe idiomatic when using specific libraries, that's obviously fine. I just wasn't sure if there were
historical, or other technical reasons I was missing.

Jeremy French

unread,
Feb 14, 2024, 6:31:40 PMFeb 14
to golang-nuts
I really think the testability issue is the biggest one.  Generally, testing the main package is....cumbersome at least.  So it's reasonable to say, "I'm not going to test the main package, but I will keep it so simple that it is impossible for a bug to exist in there."  Then everything else in your application can be easily and thoroughly tested with the built-in testing tools.

Jerry Londergaard

unread,
Feb 14, 2024, 7:08:44 PMFeb 14
to golang-nuts
On Thursday 15 February 2024 at 10:31:40 am UTC+11 Jeremy French wrote:
I really think the testability issue is the biggest one.  Generally, testing the main package is....cumbersome at least.  So it's reasonable to say, "I'm not going to test the main package, but I will keep it so simple that it is impossible for a bug to exist in there."  Then everything else in your application can be easily and thoroughly tested with the built-in testing tools

Yeah I've seen this before as well when there is a lot crammed into `func main()`. AFAICT anything that's in main *function* is only testable by building the binary and running it. If code is outside of the main function but still in the main package, it seems its testable like other functions? I suspect though if one is putting tests in package main_test, then I guess you can't import the main package to test it :( I generally keep my tests in the same package as the code they are testing, so hasn't been a problem *yet*.

Dan Kortschak

unread,
Feb 14, 2024, 7:37:11 PMFeb 14
to golan...@googlegroups.com
On Wed, 2024-02-14 at 15:31 -0800, Jeremy French wrote:
> I really think the testability issue is the biggest one.  Generally,
> testing the main package is....cumbersome at least.  So it's
> reasonable to say, "I'm not going to test the main package, but I
> will keep it so simple that it is impossible for a bug to exist in
> there."  Then everything else in your application can be easily and
> thoroughly tested with the built-in testing tools.

It's stronger than this. With a Main() int, you can use e.g.
https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript for
testing complete application behaviour.

Mike Schinkel

unread,
Feb 15, 2024, 12:04:37 AMFeb 15
to golang-nuts
Hi Jerry,
 
On Wed, 2024-02-14 at 15:31 -0800, Jeremy French wrote:
> I really think the testability issue is the biggest one.

On Wednesday, February 14, 2024 at 7:37:11 PM UTC-5 Dan Kortschak wrote: 
With a Main() int, you can use e.g.
https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript for
testing complete application behaviour.

These both are really the essential answer to your question.  I just did not think of it when I first replied.

-Mike

Brian Candler

unread,
Feb 15, 2024, 4:05:11 AMFeb 15
to golang-nuts
On Thursday 15 February 2024 at 00:08:44 UTC Jerry Londergaard wrote:
If code is outside of the main function but still in the main package, it seems its testable like other functions?

Yes indeed.
 
I suspect though if one is putting tests in package main_test, then I guess you can't import the main package to test it :(

Works fine as far as I can see:

(Tests don't run in the playground, unless you explicitly invoke a runner, but you can run this locally to demonstrate)

Marcello H

unread,
Feb 16, 2024, 4:23:57 AMFeb 16
to golang-nuts
My main acts just like a mini bootstrap to do as less as possible and hand the real action over to start.Run()
But is is still testable.

```

var mainRunner = runner

/* -------------------------- Methods/Functions ---------------------- */

// runner starts the application and can be overwritten in a test for mocking.
func runner() error {
return start.Run() //nolint:wrapcheck // not needed here
}

/*
main is the bootstrap of the application.
*/
func main() {
err := mainRunner()
if err != nil {
fmt.Println("Error:\n", libErrors.GetError(err))
}
}
```
In an internal test, the mainRunner is mocked and can emulate an error if needed.
func Test_Main_Error(t *testing.T) {
mainRunner = func() error {
return errors.New("test error")
}

defer func() {
mainRunner = runner
}()

captured := libCapture.Direct(func() {
main()
})

assert.Contains(t, captured, "test error")
}

Op donderdag 15 februari 2024 om 10:05:11 UTC+1 schreef Brian Candler:

Rick

unread,
Feb 16, 2024, 10:02:17 PMFeb 16
to golang-nuts
Another motivation I have heard used is that an os.Exit() from main by-passes defer(). So if you need to use defer from a "main-like" context move it to a function called from main(), do your defer(s) in it and then do the os.Exit() from main()..
Reply all
Reply to author
Forward
0 new messages