Get familiar with workspaces
go.dev
go.dev
For example, one of my repos contains a bunch of command line tools (under /cmd/toolA, /cmd/toolB subfolders) that make use of shared libraries. Right now, that's one module. But I suppose these could also be separate modules that are joined by a shared workspace.
They go into this a little bit under the "Work with multiple interdependent modules in the same repository" section. But what they tout as the benefit in that section isn't really a problem in my current workflow. What would be useful is to let my tools depend on different versions of libraries; I think this might be enabled by workspaces? (whereas right now they share a go.mod)
There's a couple of repos at work which contain multiple Go modules (each in their own subdirectory, with their own go.mod files). It's a bit of a pain to get LSP working correctly for each individual module if I open the root directory of the repo in my editor.
There's almost no reason to do this. 1 repo = 1 module. Even for monorepos.
Say I'm working on project A, project B, and misc-utils. Projects A and B include misc-utils. I put project A, project B, and misc-utils in the same folder along with go.work. Then, if I need to add something to misc-utils for project A, I don't have to re-publish misc-utils for it to affect project A, it does so automatically.
Now you can already do this by editing the go.mod of project A so that it depends on the local version of misc-utils, but then you have to make sure to revert to the upstream version whenever you publish project A.
In your case if the dependencies are already in your repo, then you're fine. A workspace is good if you have multiple repos which depend on one another, and you're working on all of them at the same time.
Workspaces just simplifies that workflow so that you can make changes locally in `a` which is used by `b` without ever pushing `a`, and without needing them to be within the same module.
So long as `a` and `b` are both in the virtualenv (on the path somewhere) it doesn't matter where you got a from and you don't need to push your changes globally. If you start tweaking `a` it won't effect anybody outside the virtualenv either.
This really reminds me of the approach that the Eclipse IDE takes.
Some loved it, some hated it - much like how i might want to organize my Go and other projects in rather arbitrary ways, e.g. in a bunch of subfolders where Git checkouts are done, like:
~/projects_work/project_{bar,baz}
~/projects_personal/2022/project_{baz_but_altered,other}
~/Downloads/temp_stuff/*As opposed to what you’ve referenced: a single repository that contains a single go.mod.
Imagine if you could create a file on the solution level that uses the source code in a directory instead of the nuget without affecting the csproj, sln or nuget references
This particular issue is a huge bugbear for our team of 50'ish developers in a .NET development environment, working on a large product with a lot of shared code/models/etc that often has to be updated at the same time as application logic.
It is astonishing to me that MS haven't addressed this issue with NuGet workflow and its hostility towards developers working on internal code.
See: https://docs.microsoft.com/en-us/visualstudio/msbuild/custom...
I don't use them for package references myself, but this blog post has some information on how you'd do that: https://www.mytechramblings.com/posts/centrally-manage-nuget...
Package version consistency is certainly an issue of merit and deserves attention, however for our company's use case is a minor bugbear in comparison to the issue of package references (for build) vs source or project references (for development) in large codebases that lean heavily on shared dependencies.
My current approach is to use a directory.build.props file in the folder where I clone into (i.e. outside of all the repos), and use it to set the package output path to a local folder and timestamp the package version. That way every build creates a new package to test locally, but I still have to update the references in the dependent projects when testing (and build everything separately).
Not particularly happy with it, but I don't have to manage that many shared dependencies either.
I wonder if a precompile target has enough access to tinker with the references? I.e. remove items from the PackageReferences item group, and create corresponding ProjectReference items. Probably will need to also call the MSBuild build task on those projects too so they can have a fresh build in case any changes have been made in them too.
But yes, it would be great if Microsoft supplied something for that.
Yes. Great for debugging too.
> I think this can be done relatively easily in a csproj with conditional references though?
Maybe, but you’d have to do this manually for all the projects in a solution. This is built into the tooling and produces a .gitignore-able file
Workspaces are a convention built into the language that requires no configuration. This convention applies broadly across projects equally, using identical rules for all projects. You need to download/checkout your repos in a certain way, but then they all play together.
I know people like when their language comes with build system, but that creates a cost when that language/runtime has to be used with others...
Didn’t realize I could use this for protobuf libs pulling in from a common schemas repo
if err != nil {
return err
}
i’m hoping they find a way to simplify thisThough personally I feel in this case defaulting to rethrowing uncaught errors is better. Since that's the 99% case. I'd rather it be zero line.
> Though personally I feel in this case defaulting to rethrowing uncaught errors is better. Since that's the 99% case. I'd rather it be zero line.
This is ambiguous with the "doesn't error at all" case. If you're looking at source code `foo()` you can't tell whether that's equivalent to `if err := foo(); err != nil { return err }` or just `foo()`. You have to check the function signature to see what the return arguments are (or in Java's case, whether or not it throws).
Unchecked exceptions are not explicit.
Most IDEs will conveniently put a red squiggle line beneath the exact call site as well, and show you the compile error when you hover over it.
And if you choose to rethrow it, you will need to add to your method an explicit annotation.
This is explicit and verbose. Explicit does not need to be verbose.
How much additional context needs to be there and how often is this convention used by simply duplicating the message with basically repetitive text?
panic(err) become "Problem with date formatting: invalid date format" or similar
I also think "antipattern" gets thrown around too often. This sounds more like a preference or convention.
I too would label it a preference.
To get out of the error handling tedium in our platform I largely opt to panic instead whenever viable, which gives a nice trace for free. (I am but human, and the error handling particularly grates once you have gotten used to just typing `?`.)
I think V2 errors are taking more of a lead from the pkg/errors error.Wrap approach anyway.
Programs which use `panic` as ersatz error mechanisms are fundamentally broken. `panic` expresses an invariant violation that's much different than normal errors.
For many errors, in many situations, terminating the process is quite reasonable.
In my particular situation, the greater system will restart failed processes, and retry failed tasks. I find this useful as in many cases my program can just die when something weird happens, simplifying it's own logic.
Correct. This is something I have to design for in the system anyway, because in practice anything can (and does!) die at unpredictable times. It's typically an inevitable fact of life that a machine/kernel/program will occasionally die, and your system has to survive that.
`panic` is not for business logic.
Neither are 500 responses. A perfect HTTP server never responds 500 and there aren't any situations I've ever encountered where there's any valuable error recovery or business logic left to be done once the server has run into a 500-worthy issue.
IIRC, the HTTP server in the Go standard library recovers panics and issues 500 responses, which is what I would expect it to do.
Panic isn't an ersatz error mechanism.
panic brings important context, but you're right in that it's not an annotation in itself. It's a program flow mechanism, but I'd argue it's very often utilized as an error flow one.
My larger point was that this still doesn't feel like an "antipattern" and I see that word thrown around enough as a conversation stopper that I've become pretty cynical about it.
Go proverbs: "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite."
Plus once your code throws one error, every bit of calling code also needs to handle that error. The problem cascades through a codebase quickly. Seemed like a huge violation of DRY principles.
Rob Pike (my understanding as one of the main guys who created the language) has actually addressed this, it's a good read:
https://go.dev/blog/errors-are-values
Tldr refactor your error handling to treat it like code. DRY and SOLID principles would apply and etc. Article makes example of handle your errors in one place rather than 20 by using no-ops on remaining operations after an error occurs.
I don't actually agree with the choice, as it takes one key library which throws errors at every call (like I'm dealing with now) for this to just become a huge pain to do. I had to completely change business logic to implement his suggestion, which isn't always viable (and I'm subsequently finding that out that that wasn't completely viable for us first hand now). Also a lot more boilerplatey type no value add type code needs to be written.
I much prefer unchecked exceptions for the most part, but at least I can understand WHY error handling is the way it is in Go.
Checked exceptions would have been fine if they composed decently with rest of the language...
No I hung up my Go boots in 2018, so may be a little out of touch.
Rust integrates a bit better with existing software. Inclusion of Rust in Linux being an example. Go culture tends to value pure Go projects a bit more than Rust's, where Rust wrappers around C libs are more accepted and common.
Go is generally easier from a social point of view, there's not much to learn as it's largely a repeat of what people are familiar with. So introducing it in a workplace is easier as the on ramp is minor.
Ubiquitous greenthreading in Go seems to make the integration piece a bit harder, but the onramp a bit easier.
> For more on this, Ding Yuan et al. have a great paper and talk: Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems. The paper is basically what it says on the tin. The authors define a critical failure as something that can take down a whole cluster or cause data corruption, and then look at a couple hundred bugs in Cassandra, HBase, HDFS, MapReduce, and Redis, to find 48 critical failures. They then look at the causes of those failures and find that most bugs were due to bad error handling. 92% of those failures are actually from errors that are handled incorrectly.
People who believe Golang is explicit and hard to ignore errors but think that Rust is implicit and easy to ignore errors have never used both languages, full stop.
I challenge you to provide me with an example of Rust code that ignores an error without explicitly and obviously doing so.
If you're writing alone in a vacuum without linters, then I agree that Rust is more explicit.
If you need sufficiently advanced linters to catch every case, your error handling is not explicit. Especially if those linters are not currently sufficiently advanced.
You're conflating "explicit" and "statically verified".
Agreed that static verification by default is ideal. Rust wins here.
I was thinking about the case where there is a `?` in the code but the reader glossed over it, thinking it didn't return an error.
> Unlike Go, you cannot accidentally ignore an error condition in Rust.
There is no way to discard the error without explicitly doing so, and even then you still have to decide on a non-error return value to return (because Rust functions that fail provide an error or a value and not both, unlike fallible golang functions). I agree, I think Rust is strictly better in this capacity.
> People who believe Golang is explicit and hard to ignore errors but think that Rust is implicit and easy to ignore errors have never used both languages, full stop. I challenge you to provide me with an example of Rust code that ignores an error without explicitly and obviously doing so.
You seem to be unduly defensive. This was never my claim. I have used and enjoy Rust. We're all friends here :)
The error is explicitly in the function prototype. Every code path that returns must ensure the result is an `Ok(T)` or an `Err(E)`. You just can't accidentally overlook this. Even if you do gloss over it when skimming, the error is handled and dealt with.
> You seem to be unduly defensive. This was never my claim. I have used and enjoy Rust. We're all friends here :)
I'm just tired of hearing that Golang's error syntax is explicit but Rust's is somehow implicit because it's fewer characters, even if they desugar to virtually exactly the same thing. Both are explicit. Golang's is verbose and (ironically) error-prone.
Some of us are mere mortals. We tire and make mistakes. Perhaps even unworthy of the mantle of "Rust programmer".
> I'm just tired of hearing that Golang's error syntax is explicit but Rust's is somehow implicit because it's fewer characters, even if they desugar to virtually exactly the same thing. Both are explicit. Golang's is verbose and (ironically) error-prone.
I already admitted sympathy to the viewpoint that Rust's error handling is technically explicit, and that Go's error handling would be improved by static verification that errors are handled. What more do you want from me?
https://www.amazon.com/Maven-Definitive-Guide-Sonatype-Compa...
Go is way way easier to use than Maven and Gradle.
I think I use only 4 commands daily to manage dependencies in Go, it's that easy: https://github.com/golang/go/wiki/Modules#daily-workflow
Go's module docs are 82 printed pages. Is that better or worse? https://go.dev/ref/mod