What I learned in 2017 writing Go
commandercoriander.net
commandercoriander.net
A potentially contrarian view: a long-ish straight linear function without duplicated code is often much easier to understand than a smaller function broken into subroutine callouts (because you don't have to chase down the implementation of the subroutines to figure out what's going on).
It's early days but I'm making progress! https://github.com/ebanner/pynt
https://news.ycombinator.com/item?id=16064412
I think the important thing is that modularity (breaking up longer functions into shorter ones) helps, but only if not done blindly. One has to spend some time doing it, since there are many possible ways it can be done, and some of them will be better than others. So it can pay off in the long term, but only if one is willing to put in the effort and time, and be ready to throw away some not-so-good attempts. I've done this sometimes and seen that it does help.
I've seen 1k LoCs functions that were incredibly easy to understand, and 10 liners that were complete gibberish messes.
I do however agree that declaring new variables every 10 lines over hundreds of lines would be hell to follow.
For some cases that results in more confusing code.
Usually I try to weight the overhead of having more function prototypes to track, indirections in the main function and whatnot versus the overhead of having it all inlined.
A lot of things become more evident as you inline one-use functions, because the whole context is now local.
Interesting, your comment reminded me of inline functions in C++ and C99; not the same thing (or at least goal) as what we were discussing, but closely related:
Still, I think the GP has a point - clear naming is very important, regardless of one's attitude towards function size. ;-)
let lines = {
let ret = vec![];
// Open file
// Read into ret
ret
};
let processed_data = {
// Open another file
// Use lines
// Construct ret
ret
};
....
You get the best of both worlds this way: scoped, meaningful names like with sub-functions and the continuity of a long one.Everything is an expression is an underrated feature of Rust.
List<String> lines;
{
int whatever;
lines = new ArrayList<>();
// modify lines
}
List<Object> processed_data;
{
int whatever;
processed_data = new ArrayList<>();
// modify processed_data, using lines
} if err := func() error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// work with f
return nil
}(); err != nil {
return err
}Also, if we're talking about OOP, small methods are better. With small methods it's easier to change behavior by subclassing. In several occasions, I've been forced to make a subclass with a copy of a big method with only one line changed.
If the subroutines are sensibly named, you shouldn't need to. I'd argue they're also easier to follow than blocks of code within a long function as well-written small functions will depend only on their inputs, whereas code in a large function could depend on any of the params to that large function.
Splitting functions is basically adding indirections and spreading the context all over the place.
For reusable tasks that works wonders, but for single-use functions that usually obfuscates the intent more than anything.
I don't want to follow fifteen 10-lines one-use functions when I could read one 150-line function (which would most likely be a lot less due to less boilerplate).
It can be a pain sometimes to satisfy it, but most of the time, my code ends up better than it was before.
Well, except for gocyclo. Some functions just are complex - simplifying them or breaking them up into several smaller functions just to keep each function small is not always the best idea or even possible. But that can easily be disabled on a per-function basis.
The word "lib" makes more sense. But, also, its unnecessary.
Comingling non-Go dirs with Go code makes it hard to navigate, since some dirs will be Go packages, and some won't. You may want to have documentation (/docs, perhaps), test data (/test?), build output dirs (/build or /dist), Docker/Kubernetes/Helm dirs (/docker, /k8s, etc.), Protobuf files (/proto), and so on, usually all in the root of the project.
Secondly, these directories might conflict with Go package names. We have a project that has a package called "documents", which is about persistent documents, which we'd have preferred to call "docs", but the root already had a /docs folder for actual documentation.
Personally, I prefer "src".
More fundamentally, it feels a bit screwy for a multi-faceted 'project' to live wholly inside $GOPATH in the first place. But at the same time, it's natural.
I've solved my main issue with GOPATH in a very easy way though. I just create symlinks in my normal directories (like ~/work) to my projects in GOPATH. This allows me to go to my own projects easily while still having everything in GOPATH.
# setup easy access to cd paths
setopt auto_cd
cdpath=($HOME/code/go/src/github.com \
$HOME/code/go/src/github.com/collinvandyck \
$HOME/code \
$HOME/code/go/src)Specifically this bit is relevant here: > Every package in a program must have a unique import path. By convention, this is arranged by starting each path with a unique prefix that belongs to you. For example, paths used internally at Google all begin with 'google', and paths denoting remote repositories begin with the path to the code, such as 'github.com/user/repo'.
Most (all?) Go tooling assumes this convention. Conceptually, I don't see it as that different from how other languages determine input paths, except in Go they are guaranteed to be unique which I consider a good thing. In Java for example you still have file namespaces they're just inside the project itself (looking at you `src/java/com/example/package`) or Python which looks for the root module name in `PYTHONPATH` and then recurses down for submodule names.
Most if not all languages use file structures to namespace imports in one way or another, Go just applies it globally instead of at a project level. I agree it raises the barrier to entry for someone to just come in and run `go run` on a project but I find it works very well once you've got it setup. Convention over configuration is a big thing in Go tooling.
Let's say I have my-package (version 1.0) installed and in use in a lot of my other projects. But I'm working on version 1.1, but need to have 1.0 as the reference for other projects.
Right now I just fiddle with $GOPATH but that feels like a hack and is an annoyance if I have to swap around a bit.
Right now my-package might be in a broken state because I'm working on it, so I can't build anything relying on it if I go with the single $GOPATH hierarchy that every discussion seems to assume.
It supports working on a dependency of a project locally with ease, by using "vg localInstall" which is a extreme pain with vendoring. And also it supports version pinning of executables.
It's also important to limit cross package dependencies as much as possible to avoid these situations.
Just a few days ago using dep instead of godep on a medium-sized web application gave me a delta of 1,920,834 additions and 1,942 deletions (_after_ manually pruning _test files), mostly caused by depending on aws-sdk-go.
Do you have any strategy for keeping your vendor small with dep or do you just accept it the way it is?
There seems to be this: https://github.com/golang/dep/issues/858 , but it looks like it's not implemented yet and the status is highly unclear.
What would be one of the cases where you'd say you need to commit vendor?
Other than working around that issue the only case where you actually need to commit vendor (at the moment) is when you want your project to compile reproducibly by only running "go get project". If you are fine with telling your user to run "dep ensure" (or make) first this is not needed. This is not usually an issue when working with colleagues, but can be nice for publicly released projects.
I do generally use the testify assert package for convenience[0], but it’s not strictly necessary.
For the problems it doesn't solve I've created virtualgo at my job, which is similar to rvm or virtualenv: https://github.com/GetStream/vg
That one's easy. Go is a brilliant low-level language. You hack on servers, tooling, IPC, batch pipelines, etc. Nothing that people endlessly keep pondering about, from "deps" to generics to gopath to "practices", really hinders the enthusiastic hacker much from just getting busy with it, and producing. A Go coder doesn't envy the "spoiled Rails developer" because he feels blessed by a low-level systems language that actually has prim types other than int, sane pointers, concurrency primitives, utf8, heck simply strings to begin with, fast compilation times, no header-includes mess, etc.. =)
We switched to Go because Ruby's concurrency model was obtuse/inefficient and scaling the codebase was challenging even with excellent developers and great engineering practices. The performance gains with Go were a really nice bonus.
If you're solving Google-style problems like writing a PAAS, then the benefits of Go outweigh the headaches. If you're writing a more straightforward CRUD-style Web API, then Rails might be a better choice.
But that didn't stop early adopters to build libraries and push Rails to where it is right now.
(I don't mean to bash Ruby, just trying to put your argument in a different perspective.)
Often these things are done in a half-assed way that isn't well suited to production deployments (e.g. not secure, requires connections to random hosts on the Internet from production boxes).
"Testing" means many things to many people in the context of many different kids of software. Defining a fixed framework for same can be detrimental to some proportion of that space.
But still starting project where you can pull dependencies for development with dependency manager and setup first thing in let's say 25% faster, I'd choose that.
For testing I would agree framework is not a must have.