Also regarding warnings/errors: time-travelling back into the 90s it was normal that C/C++ code - also in Open Source Projects - was full of warnings and often not portable across compiler/library versions. That was a major pain, I remember myself experimenting a lot with compiler flags (-Weffc++, -strict etc.) for my own code that also prevented stepping into common pitfalls.
Except that it only happens in C and C++.
Why? Because C and C++ were designed to have sharp edges for you to cut yourself. The default behaviors are absolutely bizarre and stupid, and the breadth of things that are undefined behavior guarantees that any given program is non-conformant. Warnings and -Werr and linters were the only possible ways to keep any semblance of sanity and code quality.
But we're not talking about those dangerous languages and their sordid history. We're talking about the here and now and modern languages, where an unused import NEVER causes bugs, and where unused values aren't flagged as errors in any other language for good reason: we debug code far more often than we write it, and slowing the debug process for the sake of cleanliness is stupid.
And it still astonishes me how unused values are treated as errors in a language that happily allows variable shadowing that is literally one colon away (= vs :=), and doesn't allow file-level variable and function isolation. Those are FAR more dangerous and bug inducing.
Speaking of myself, in various programming languages both ancient and modern I've run into the situation where debugging is difficult because of hard to read code. Code written by others (or written by myself 6+ months ago) is always more difficult to read. Even more so when there are unused artefacts ("dead code").
So often it happens that people have to work under a lot of time pressure and write 300 lines for something that can be solved in 30 lines in the same language. It doesn't matter so much usually but when there is a difficult bug, sometimes the only way to solve it is to thoroughly understand the code. For me it's often easiest to just remove dead code/compactify existing code so there is less code I have to reason about.
Probably a lot of Go's design is about simplicity, its spec can be read in 1-2 afternoons if you're already familiar with the language.
- comment out a line of code to check something.
- compile, get an error about unused variables because a variable is unused now that the line that used it is commented out.
- go back to the code and comment out that other line.
- compile, get another error because there's another two variables that are now unused.
- comment those lines out.
- compile, get another error because an import is now considered unused.
- repeat ad nauseum.
- Fix the bug.
- Uncomment everything you commented, and try to undo any changes you made to get around unused variables so that your code is mostly back to where it was, plus the bugfix.
- Last compile to make sure everything works.
--
I prefer this method:
- Install a custom go compiler from https://github.com/kstenerud/go
- comment out a line of code to check something.
- go test -gcflags=-warnunused
- fix the bug.
- uncomment the line.
- go test
--
This has nothing to do with the difficulty of debugging the code, or the complexity of the code you are debugging. This is about getting the compiler to stop fighting you.
_ = unusedVar
and then do the debugging stuff. Not sure why, but over time I ran into this procedure far less often but I do very pedantic error checking like return fmt.Errorf("foo(%v): %v", fn, err)
and putting in checks sometimes that can return custom errors. So in my log I often end up with messages like "bar: baz: foo(/data.txt): open: file not found". Debugging usually means for me replacing return err
with a more verbose return fmt.Errorf("...: %v", err)
and adding mentioned checks and/or additional log messages. Either with the log package or logrus. (And possibly adding unit tests/factoring out code into separate functions.) This gives me information content like in a full Java stack trace but in one line.But YMMV, probably it also depends what kind of software one writes.
Everything being an error makes this worse not better; if -Wdeprecated turns into an error you're just not going to be able to fix everything in someone else's code. Unused code being an error is easy to fix, but it's so disrespectful. I shouldn't get compile errors as I'm editing when I haven't done anything wrong.
It's been a long while since I last touched Golang, but I recall the process of learning it.
My coworker and I were tasked with creating an interface for our employer (a cloud service provider) to allow Rancher (or clients of Rancher - I forget) to use our backend system (which was built out of a combination of PHP, Java, and Bash - among other parts) to provision servers.
Neither my coworker or I had ever touched Go; we were PHP developers. But Rancher used it, had examples, and we set out to learn it (while we were employed as PHP developers, we both had extensive prior experience with a number of other languages).
It took about a week until we were comfortable enough to begin our implementation, and about a month later we had a working library written in Go that we understood and had documented well, with tests.
But the process: Exactly like you noted! We railed, we gritted our teeth! We shook our fists at heaven and loudly proclaimed "WHY?!"
Because we were so used to leaving things lying about in PHP; for experimentation, debugging, and other reasons. But Go wouldn't let us - no-siree-bob! - you had to make it just so before it would successfully compile, and we tore our hair out over it. Day after day, week after week...and then:
Something changed. We understood. We realized why the designers of Go did what they did, and we also started to wish fervently that PHP could be the same way. We also noticed that by these changes, we no longer needed to leave this "cruft" around, this "dead code" that could possibly cause us to trip up, or wonder if it was needed later, or whatever. Yes, it made development more difficult - but we came to recognise that it helped greatly to prevent errors, or future problems, and kept things very maintainable.
But like I said - I haven't used Go in years since that time; there hasn't been a call or need for it, but it is a language that I keep in my "back pocket" just in case I ever need it. Maybe things have changed a lot since then, or maybe they haven't. Regardless, I learned from that experience that sometimes you have to power and struggle through things before the revelation appears. I got to experience a fairly unique language, and I feel that I'm better as a developer for it.
In the end it's just about the variables anyway, most IDEs will remove unused libraries automatically.
Like having an annoying wife (erm, spouse) constantly tell you you've wrong all the time. In the end, some if not all will understand and adapt their behavior as complaining is of no further use.
I think the worst part of that decision is I've never seen a bug arise from unused imports and variables (even in languages with ubiquitous importing side-effects like Python, imports and import ordering can cause issues but usually you need the import anyway, you can't just nuke it).
Meanwhile variable shadowing / overwriting or unused return values which do cause issues are perfectly fine by the compiler.
Not every line of code is life or death.
Stopping compilation with an error for every unused import & variable is particularly annoying, so much so that I've patched the go compiler to treat them as warnings instead [1].
Ultimately, I think the problem boils down to a language that outgrew its original design, and hasn't evolved elegantly (yet).
[1] https://github.com/kstenerud/go