Hacker News new | past | comments | ask | show | jobs | submit login
Deadcode: Finding unreachable functions in Go (go.dev)
112 points by rbanffy 10 months ago | hide | past | favorite | 58 comments



For those interested in this topic, I worked on improving dead code elimination in js_of_ocaml [1], a compiler from OCaml to JavaScript. The problem is more difficult in that case because of the indirection of OCaml's higher-order modules (functors).

[1]: https://www.micahcantor.com/blog/js-of-ocaml-dead-code/


Finds unreachable functions but does not seem to find dead code in sense of unused writes to a variable (unless I misread the article). Guess there's something else that already catches that in Go?


One of the checkers in golangci-lint does this. I forget which one.

golangci-lint rolls up lot of linters and checkers into a single binary.

There is a config file too.

https://github.com/golangci/golangci-lint


It's called ineffassign


One of the mandatory ones, in my opinion. It's 3rd or 4th in terms of the linter that catches actual bugs for me before they are bugs, behind the number one errcheck and probably number two "unused", checking for unexported package-level variables that are unused. Go prohibits variables to be unused within a function but the compiler does not check that unexported package-level variables are used; this has helped me find a lot of places where I created a metric for something but never quite followed through to actually triggering it, log messages, bespoke errors, etc.


Cool but please don't make it a compile error!


I still find Go too tolerant on this. It should scan your Git history and stash as well, and fail to compile if it finds any unused code there.


That'd still be a bit too tolerant for my tastes. It should also scan your future commits.


That'd still be a bit too tolerant for my tastes. It should have some kind of reinforcement signal that's stronger than just preventing the code from compiling. Some kind of shock therapy to help users learn and internalize things might be more appropriate here.


    go vet -basilisk ./...


Really it needs to scan my coworkers commits as well. Can't be too careful.


Ah, I know the programming language for you: https://news.ycombinator.com/item?id=5002597


Could you please expand on this? Why would local Git stashes relate to code quality?


That comment might not be entirely serious.


Why not? What do you gain by keeping unused code in the codebase?


I definitely don't want to keep unused code in version control, but for debugging or for working on things iteratively as the sibling comment said, my Go code usually ends up peppered with

  if 1==2 { ...some code to temporarily disable ... }
"Just comment it out" is not always enough because when I want to quickly toggle the code on and off multiple times in a single session, it's not possible to do by only commenting/uncommenting that single block.

Sometimes the code that's commented out contains the only reference to an imported package, and by commenting it out the autoformatter helpfully removes the import from the top of the file, so then uncommenting the line is not enough -- I need to go back and re-add the import.

Or I may want to comment out this line

  x = f(a, b)
but that makes a and b in turn become unused as well, and I need to travel up to wherever they're defined and comment them out too, and so on with anything else that became thus unused in that chain.

And this is all suboptimal because the warning is also important when debugging. I do want to get warned about the unused code! Just let me compile.


> Sometimes the code that's commented out contains the only reference to an imported package, and by commenting it out the autoformatter helpfully removes the import from the top of the file, so then uncommenting the line is not enough -- I need to go back and re-add the import.

The same tool that removes your imports also adds them so there usually isn’t an extra step.


It often breaks stuff for me when I import two packages with the same name so I use a named import:

``` import "alpha/stuff" import bstuff "beta/stuff"

stuff.buyStuff() bstuff.stuffAnimal() ```

Once the autoformatter sees the named import is unused it removes it. Then when I uncomment my code again the autoformatter doesn't remember that it's supposed to use a named import. So in my example it wouldn't know "bstuff" is actually "beta/stuff".


When I was doing Genuary in Rust, I started with a template that I would add some helper functions too that I could anticipate I would need for the art. Easy ones like "xy_to_polar".

I would do the first-time compile on this template and then add things iteratively.

Go would be a nonstarter for this experimental/artistic code, because I would usually have dead code in my "toolbox" to iterate and experiment with until I liked the end product.


In reality you just comment out the block, in day-to-day work it really is a non-issue in my experience.


I get what you're saying, but I don't share your experiences.

My reality is that I don't comment and uncomment lines, and I don't want to start. Commenting out a block of code takes more time and it's still unused code.

It really feels like a "Go is designed around problems Google had within Google" design choice.


Well, unused imports are a compile error, so there's precedent.


Yes, unreachable code after a "return" in a function is also a compile error. Very frustrating when I just want to nop out part of a function for debugging something else and have to keep fighting the compiler until I find a way that is acceptable.


If you find yourself doing this often, maybe a feature flag pattern using environment variables would end up being a bit nicer looking than throwing a bunch of unreachable code into your code base.


Huh? Its a warning, not an error.

https://go.dev/play/p/8GazBQv5xAb


My mistake!


If you're finding you're doing this often, perhaps your functions are too large


It would break reflection surely? It's nice to have as a configuration / flag though as many project will be able to require it.


Last I looked, reflection was missing a lot of things like looking up a function by name.


Go’s reflect package is not “missing” that feature, it is intentionally omitted as it defeats static analysis. In all my years writing Go (14 now), including some tricky metaprogramming, I have never needed to look up a function by name.


It creates busywork and ongoing maintenance issues in writing an interpreter. E.g., https://pkg.go.dev/text/template requires every possible function to be manually registered, and that’s not a good use of time.


`MethodByName`[0] allows you to find a method on a struct but I'm assuming you must mean a generic package-level function rather than a method?

[0] https://pkg.go.dev/reflect#Value.MethodByName


Yeah, you can look up a method but only if you already have the receiver’s type (which you also can’t look up by name, or by listing a package).


You don't need the receiver's type because you're calling `MethodByName` on a `reflect.Value` which you can construct from any receiver.

https://go.dev/play/p/hg-ugl7i0ei demonstrates this, I think?


Sweet. For some reason, this has been an ongoing problem with Go’s tooling. I’m sure that there’s some underlying reason why the dead code finding tools have kept getting deprecated or had various problems.


I'm somewhat confused about this because I've had gopls installed with vscode for years now, and as far back as I can remember, it always gave me a little yellow underlining warning when I had unused functions or unreachable blocks of code inside `if` or `switch-case` statements. It would seem to me that this has existed for a long time, or maybe it just builds on top of what was already there?


I believe the checks you're referring are local to a package. It doesn't do whole program analysis like deadcode does.


I inherited a go project that has two different commands under `cmd`, and it seems when I run this against one of those `main`s, it incorrectly detects what it thinks as dead code that is used in the other command.

Does anyone know how to work around this?


    deadcode ./cmd/...


Thanks. That doesn't work for me (for some reason _nothing_ is reported even though I know there's dead code) but it at least gives me a lead.


The given example is a poor choice. Capitalized elements are public, and it's not safe to assume this code isn't being used as a library by other code outside the immediate repo.


The paragraph titled "Tests" addresses this.

TL;DR: if your project is a library and an exported function looks like dead code, that means it isn't covered by any tests, and you should probably fix that.


I will definitely use this! Also eager to see if it's fast enough to just run every time I save a bigger project.


IMO tree-shaking and similar stuff must be not an afterthought, but instead built into language and tooling from the day one.


Seems like you're conflating two separate things here. The Go compiler has a DCE pass which prevents unused code from ending up in the binary. The linked blog-post describes a tool for identifying unused code in your codebase. Two completely separate use-cases.


I agree that "changes the generated binary" and "displays information to the user" are separate use cases, but the compiler already does both. There's even a compiler error for unused imports and variables.

Doesn't seem far-fetched to ask for the compiler to warn on unused types/functions too.


It's not that simple. The Go compiler uses a main package as its entry point when compiling a binary. A module can have a many main packages each using different subsets of the library packages.

So there are two choices here:

    1. The Go compiler complains about code which is unused by the current main package.
    2. The Go compiler tries to find/compile all main packages every-time.
Both of these options suck.


One nice thing about go is that it ships with an AST package for parsing go code. It allows building of tooling really easily.

https://pkg.go.dev/go/ast


And then, people start using reflection (Java), vtables/dynamic lookups (C, C++), eval (JS, PHP) and similar constructs (a lot of languages have them), and suddenly tree-shaking falls apart.

Tree shaking shouldn't be necessary in 2024 anyway.


Calling vtables "reflection" is a stretch.


I was merely listing stuff that can break or severely restrict tree-shaking.


Apologies, I misread your comment. But can you elaborate on your statement that tree-shaking (more commonly referred to as dead-code-elimination) is no longer required?


Tree shaking was mostly a thing invented in the NodeJS ecosystem for websites to save transfer traffic and thus lead to faster page loads, as well as save a bit of RAM.

Even budget phones routinely have more than 4GB of RAM, and high-speed Internet access is the norm, so you can get away with a lot more inefficiency than just a few years ago.


Do you happen to know languages that implement that? I vaguely remember remember some Zig post on the topic, but I don't remember if it was an idea exploration, or an implemented feature.


I can't name any language that was born with it.

Most have it on the standard tools, but I don't think any language was born with it.


Dart compiler does it, as I know.


I've used it a bit, didn't help me much more than "go vet" did. Also can't find code which is masked behind interfaces, unless I'm mistaken, which could amount of quite a lot of dead code.


> Also can't find code which is masked behind interfaces

Is this not the Greeter interface example from the OP? The example finds an unused variant of an interface and all code only invoked by said unused implementations and marks all of it as deadcode.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: