"Copy Slices and Maps at Boundaries. Slices and maps contain pointers to the underlying data so be wary of scenarios when they need to be copied. Keep in mind that users can modify a map or slice you received as an argument if you store a reference to it. Similarly, be wary of user modifications to maps or slices exposing internal state."
This could be used as an ad for Rust borrow checker, verbatim. You can't modify a map or slice you passed as an argument if a reference to it is stored!
Another solution is to use immutable and/or persistent data structures. Of course, because golang doesn't have generics, it becomes unwieldy to have a library of them, unlike what we see in Java, Scala, etc. where these enjoy a wider adoption.
Shared mutable state is evil, so "not mutable" has been proposed as a solution. Rust is different, because Rust's solution is "not sharing". You can still mutate!
Exactly. Ownership/lifetimes exist in almost every mainstream PL (anywhere where you can have any sort of references + mutability). It's just people pretend it doesn't, and hope everything is going to be fine. And when you bring it in front of their consciousness they scream in panic, like it didn't exist before. Once you internalize Rust ownership rules, they are almost effortless and you see and obey them in any code in any PL you happen to be programming in.
Most languages have a GC so there really is no ownership to pretend away (if you like, everything is owned by the GC). Rust can be super cool without inventing fake problems for other languages.
I'm sorry, but that is a naive misconception. "everything is owned by the GC" is a sign of not understanding concept of ownership, BTW. GC is not an actor in the system, so does not participate in the ownership. A Java finalizers might kind of do stuff, but that's when all references are gone, so they have exclusive ownership.
Anyway... Languages with GC still do have ownership. It is just defaulting to be the most problematic case of "shared ownership" everywhere. Shared ownership is only worry-free if you don't ever modify data. Which is possible, but actually harder and more constraining to implement in a practical software (especially if you care about any performance). So then you either have to synchronize everything with mutexes, and figure out a way to not deadlock yourself on every step (also hard), or you have to implicitly track ownership and decide who can and when mutate the data, which is exactly what Rust would make you do, but this time you get no compiler help. The only reason people don't notice that is because they do it incorrectly and it works most of the time, so they are unaware.
There is no escape from it - one has to figure out ownership with a GC or without it, because GC has nothing to do with ownership. It only reclaims memory after all references are gone. That's it. That is actually very, very little improvement.
"The only reason people don't notice that is because they do it incorrectly and it works most of the time, so they are unaware."
Ironically, that's Rust biggest problem: not using perfect ownership management works for most software most of the time and that's good enough for most people and companies.
You probably can't sell ownership management as a feature. The only thing that Rust can sell and that makes sense is "it won't crash" + "it's fast", but then there's a lot of other languages that have the first property and the second is not very appreciated, as seen in the rise of so-called Electron apps and web apps in general.
Sure. SWE is not a first industry where you can get away with cutting corners or straight incompetence. And some people are going to write a code taking care of ownership by experience, skill, etc. and they don't need Rust. They might not know they are taking care of ownership, and yet do it correctly. Rust is only formalizing and verifying it.
I think in time more and more people and teams will discover that doing stuff with Rust is just cheaper and more productive. You learn ownership once, you get great results ever after.
Ok, then educate me—what is ownership if not the responsibility for managing (cleaning up) a resource? In my C and C++ days, I never heard of it refer to some kind of exclusive permission to mutate a resource (which seems to be your meaning), but rather as the responsibility to free the resource (as in “the owner must and only the owner _can_ free the owner resource”).
It seems likely that you and I are using different definitions, and yours Rust-specific (and therefore not meaningfully generalizable to other languages, especially those with GC).
Ownership is (at least) two things: who has the responsibility for destroying an object when it's finished with, which GC takes care of, and who has the right to mutate an object while it's alive, which GC doesn't.
I mostly write Java for a living, have done for a decade, and still occasionally trip myself up by mutating an object I'd forgotten I've shared.
Thanks for responding, Steve. I understand that “ownership” implies the right to destroy in any language. The bit I’m not so clear about is whether or not there are other rights/responsibilities (such as the right to mutate) as the OP suggests and whether a definition that includes those rights/responsibilities can meaningfully apply to any language or just Rust.
How is it a fake problem? Uber is copying slices it doesn't need to copy, suffering significant performance penalty, because it is too error prone in Go. Rust completely prevents this error.
The point is that ownership also prevents these errors. If there are never two simultaneous mutable references to something, then you can get the advantages of immutable data structures without paying the performance penalty.
We’re having a semantic debate. Ownership appears to mean something different to Rust folks, which is fine. I agree that Rust’s borrow checker (what you appear to be referring to as ownership or the enforcer thereof) prevents errors and is generally very cool. It’s just not what I understand ownership to mean.
Language fights are of the things many of us enjoy most about HN. Would you go to a metal show and complain about the moshpit? Just stand safely to one side and enjoy the vibe, man.
I think this is where Rob Pike got it backwards (in terms of one of his stated goal for Go).
Go is a very effective tool in the hands of experienced programmers, without having to pay the cognitive load price of 'Mommy' languages. I am reminded of Bryan Cantrill's rant regarding threads in this context.
Writing solid software is hard. It takes skill, experience, and serious battle scars. Tools and languages can help, but at the end of the day, it comes down to the team that writes the code.
Even the most experienced programmer can’t hold all of a million line codebase in his or her head. And that is the issue with a language like Go - you can be careful about the code you write but it doesn’t have static analysis enforceable constraints on code other people write that interacts with your code / data.
That is a seriously ridiculous proposition in my opinion. If you find yourself involved in a project that requires you to hold a "million" lines of code in your head, refer to above point I made regarding "team". Something has gone seriously wrong somewhere if that is the situation.
This should not be news to this audience on HN: proper software architecture, design, and development processes trump "language" any day, any time, any planet. Conversely, even the most 'profoundly correct' language in the hands of unprepared development teams can result in software disasters.
The human factor is still paramount in software development. That is my experience after decades in this profession and is not mere conjecture.
Yes, that's not good, but it seems like in practice this doesn't come up too often in Go?
More typically you're building a new slice containing the results of a query or other computation, and returning it without keeping a reference. Or instead of exposing an internal map, you have a Get method.
I have found this to be true of everything other than byte slices, where the result is some of the worst bugs I've had the displeasure of tracking down.
Many Go libraries like to offer passing a byte slice to reuse as a destination. Many Go libraries take byte slices or structs containing them as arguments. A common result is "loop over some input, read it into the reusable slice, pass the slice to the next step". It even sort of works - until the next step does something asynchronous.
It doesn't help that:
- Lots of things return a new slice "sometimes" - _this_ append() result is safe to pass on; this other one is not; this other one "it depends".
- The `x[:]` idiom to turn an array into a slice looks exactly like a Python copy, but actually shares the same backing store.
In the end we basically banned reusing slices as a result. (The rule I tell new programmers is "if you pass the slice elsewhere in a loop, the slice needs to be allocated within the loop.") Which is a shame, because the performance gain from safe reuse is often significant. But until the type system blocks us from changes, usually in the oblivious callee, which turn a safe use into an unsafe one, it's simply too much effort to remember and review each case.
Someone using a string when they really want a byte array/slice would fail review on its face. Excepting external APIs forcing "strings" on us, we only use the Go string type for UTF-8 code unit sequences (and this is a common assumption in the Go world).
var buf bytes.Buffer
buf.WriteString("foo")
b := buf.Bytes()
fmt.Printf("b == %v\n", string(b)) // b == foo
buf.Reset()
buf.WriteString("bar")
fmt.Printf("b == %v\n", string(b)) // b == bar
Yes the documentation for .Bytes() says that "The slice is valid for use only until the next buffer modification" but people don't always read the docs for every method they use, especially if it's a method they've used a bunch before. Having a reusable buffer that you return the .Bytes() value from is very tempting. Bug-free code would either copy the result or not reuse the buffer.
> but it seems like in practice this doesn't come up too often in Go?
It comes up in any language that allows shared mutable state, so not much in functional languages which discourage mutability or Rust which discourages shared mutability.
On the other hand, most code tries to avoid shared mutable state by convention, there’s a nice bit in the Go lang design article that basically says convention is good enough, significantly simplifies the language, at the cost of the odd bug.
I would love to see that too, but I think that's likely never happened. Because Rust is kinda hard and takes quite amount of time and money to train new people, add more stacks on maintaining projects, and important thing is that I don't see why they have to use Rust. Go is good enough and they can work around with Go's disavantages.
You can always write a borrow checker for Go and have the lifetime annotated in comments similar to how pre-3.7 Python type checkers worked.
In Rust, the borrow checker is too a separate static analysis stage and not directly tied to code gen. There are Rust compilers without the borrow checker.
If you wrote such syntax on top of Go, it would no longer be go, it would be a new language you created.
The go authors wouldn't accept it, the ecosystem wouldn't work with it, and it would be fighting an uphill battle rather than just using rust or building a new language.
> There are Rust compilers without the borrow checker
Then those compilers do not compile rust. They compile a bastardized version of rust whereby programs that are not valid rust programs can compile.
Also, can you name these non-borrowck-ing rust compilers? You say there's multiple, so you should be able to at least name one or two.
+1. It is quite an achievement by Go as a language and/or community to have generally short lines of code where two columns fit in a regular website width. I don't like everything about Go, but it's usually pretty easy on the eyes for this reason.
I don't think that the prevalence of single letter variable names can be considered an achievement (which is how they get short lines to begin with). It's quite a hindrance to readability. golang as a language does nothing to support shorter lines.
> golang as a language does nothing to support shorter lines.
It does a few things.
- Most declarations, including method declarations, take place at the top level. This is unlike most languages which declare methods one level indented along with the object's fields.
- Short type definitions (`type T1 T2`) are encouraged and often used for any repeated non-scalar type. This shortens argument specifications and (if a good name was chosen) improves readability.
- Anonymous struct fields are shorter both to declare and use. In class-based OO this is often the case for subclasses but not wrappers/facades. Go's approach covers both.
- Go linters push people to avoid `if x { ... return a } else { ... return b }` in favor of `if x { ... return a } ... return b`. I actually hate this, but it does keep indentation down.
- The lack of exception handling means there is a constant "pressure" to push errors back up manually, via early return if it's locally unhandled or to the top of the current lexical scope of it is. The result is definitely verbose and one of the easiest parts of Go to criticize - but the same pressure results in short (but plentiful) error handling lines.
(I do agree the majority of savings comes from short variable names, though I disagree that it affects readability much for the case of _variable names_ - struct fields are another issue...)
> - Most declarations, including method declarations, take place at the top level. This is unlike most languages which declare methods one level indented along with the object's fields.
You can do the same in C++ or Kotlin, etc., but you don't find people saying that those automatically make lines shorter.
> - Short type definitions (`type T1 T2`) are encouraged and often used for any repeated non-scalar type. This shortens argument specifications and (if a good name was chosen) improves readability.
Also possible in C++ or Kotlin, among others.
> - Anonymous struct fields are shorter both to declare and use. In class-based OO this is often the case for subclasses but not wrappers/facades. Go's approach covers both.
This can save lines of code (at the expense of not being able to easily figure out what classes implement or embed other classes or interfaces), but does nothing to shorten lines of code.
Same applies to error handling. Returning early can be useful in certain situations, but cause readability issues in others. golang linters did a bad job here as you point out.
> You can do the same in C++ or Kotlin, etc., but you don't find people saying that those automatically make lines shorter.
I don't know what happens in the Kotlin community, but you absolutely find people saying C++ is "less nested" than Java/Python/JavaScript/etc (and claiming it as an advantage and disadvantage depending on their viewpoint).
I don't know what you're really arguing with here. Go gives you a construct that makes lines shorter than most other OO languages. It's not "automatic" as it's a feature of the language someone had to design - but regardless it is shorter.
> [Short type definitions are] possible in C++ or Kotlin, among others.
Again I don't know Kotlin, but no, it's not possible in C++ (at least up through the 11-and-a-bit I work with). The equivalent C++ would be "struct T1 : public T2", plus another line of "{}". And it's rare (for good-ish reasons) to subclass specializations of the default containers rather than wrap them.
> [An anonymous structure] does nothing to shorten lines of code.
I don't see how this is a debatable point. Not having to specify the field name in the definition is a shorter line than than having to specify it, strictly so. Not having to specify the full path to such an embedded object to call a method or access a field is nearly always shorter (it can be longer if your struct name is longer than your field name would have been and you have an ambiguous resolution - the former happens a lot, the second less, and both in combination even less).
> Returning early can be useful in certain situations, but cause readability issues in others.
Regardless, it keeps the lines short.
You seem to want to argue "Go is ugly", which, fine. But the claim was "Go doesn't do anything to help make shorter lines." It does quite a bit.
Pretty good. Some opinions that fall under "be consistent within our code base" (which is why Uber should have a style guide at all), so all good.
Just one comment though:
> Embedding sync.Mutex
Never do this on an exported type. If you embed sync.Mutex that makes "Lock" and "Unlock" part of your exported interface. Now the caller of your API doesn't know if they are supposed to call Lock/Unlock, or if this is a pure internal implementation detail.
your `type Stats` "isn't" a mutex. It has a mutex.
> Never do this on an exported type. If you embed sync.Mutex that makes "Lock" and "Unlock" part of your exported interface. Now the caller of your API doesn't know if they are supposed to call Lock/Unlock, or if this is a pure internal implementation detail.
Isn't that exactly what they say?
> Embed for private types or types that need to implement the Mutex interface.
Personally, I would never embed a sync.Mutex though because even for an internal type it is confusing.
Agree, but I can't recall ever seeing a package in the wild that labels itself as "concurrency-safe" and yet requires the caller to explicitly call Lock!
Something that I don't see style guides addressing, but think they should, is how to cancel work when the caller no longer cares about the answer. (Consider an aborted RPC, or someone pressing the "stop" button in their browser.)
A lot of people will do things like:
func (s *Server) HandleFoo(ctx context.Context, in *input) status {
ch <- workFor(in)
return status.Accepted
}
This will block on the channel write even if the context becomes cancelled. Instead, you really need to be explicit about what you want to block:
func (s *Server) HandleFoo(ctx context.Context, in *input) status {
select {
case ch <- workFor(in):
return status.Accepted
case <- ctx.Done():
return status.Error("handle foo: wait for work to be accepted: %w", ctx.Err())
}
}
Now we don't wait for the work to be accepted if the caller decides it doesn't care about the job anymore.
My impression from digging around the open source world is that nobody except me cares about this. So maybe I'm just crazy. But I really like not leaking goroutines on long-running programs, making Control-C behave gracefully, etc.
You are not crazy! However cancellation is an advanced topic. It requires some experience to get it right - and even to understand why it's required at all.
Most people will not see the need for cancellation - until they either need to implement a deadline timeout later on, or run into the problem that lots of outdated tasks take up their memory. Those things can come up years after the inital program had been written.
It's also not purely a Go thing. E.g. in Java thread interruptions are also barely handled correct. Or CancellationTokens in C#.
the way to do is to pass context all the way through, until the other thing you are waiting on uses up no resources.
so in your example it should be done like this:
ch <- workFor(ctx, in)
and the 'workFor' function should be the one that gets canceled with the context.
Deep down at the very end, you would have select statement with '<-ctx.Done()' and something else that doesn't need canceling (if it needs canceling it should have taken context too).
Of course the world isn't perfect and not everything takes context right now (even in standard library), so you do have to do some workarounds every once in a while.
This is a very trivial example so doesn't dive into all the complexity. There is a lot of nuance here, like whether or not you really want to do an operation in the background in the first place. Background work does means that you lose the ability to apply backpressure to the calling system, and that will cause a lot of problems under load. Even if you do want to do the operation in the background, you still want to provide a (derived) context so that you can link the background work to the initial request (at the very least, for tracing purposes). That is omitted here for simplicity.
Where I was going with all of this was that the linked style guide mentions cases where people create buffered channels presumably because their channel writes start blocking. No buffer will make up for the case where the rate of work requested is higher than the rate at which work can be completed. What people really want in that case is the ability to get out of the channel write and return an error to the downstream system; you don't want to buffer, you want to cancel. There are many ways to accomplish the act of cancelling work, but you do have to watch your channel writes explicitly.
It does not need to be the background. You can check context cancellation without any goroutines by:
select {
default:
case <-ctx.Done():
return ctx.Err()
}
When there's no other channels to select with the context, just use that code block between lengthy operations to check context cancellation and return early.
(If you are curious about how many extra time is wasted, it's easy to write a benchmark test for that. Last time I checked it was ~20ns when you are going the default/not-cancelled route)
Having gone from a lot of C# to some Go, I’m use to passing in a CancellationToken to all asynchronous methods. I’ve noticed that there’s a lot of Go libraries missing Context support too .
For backends they've standardized on Go and Java, deprecating/eliminating node and python where possible. I understand node is still prevalent for serving react-based frontends, both external and internal facing.
I'd guess it's usually up to the developers/owning team, but also to some extent depending on the ecosystem it's interacting with (e.g. Hadoop would tend to prefer Java)
Often comes down to the preference of the engineers in a given team and evaluating pros and cons with engineers outside their team (aka review their plans before they start building)
> Uber have well over 1,500 microservices written in Go
1500?! I can't even imagine how micro a microservice could be such that Uber could have so many.
edit: Having said that, the Monzo blog post also on the front page says it has 1100 microservices, so I suppose I've just not worked somewhere with 'actual' microservices. What constitutes a 'service', a single procedure for RPC?
I haven't worked with microservices directly but what I gather is:
- build a UNIX philosophy, "efficient atomicity" so 1 microservice = 1 command
- you then build logic with these like Lego bricks, that's your glue code => backend ties that together
On a typical base Linux install you have something between 250 and 1500 packages. Average Java repo has like, what, 80 files maybe? 120? Break it down to functions / structs and you can see the 1500 being a reasonable estimate of a modular architecture.
So you have one microservice to return a catalog, another to take a search input, another to serialize this stuff, another to autofill something, another to autofill some other thing, etc. Your app architecture schema is produced by/in microservices.
That's the extreme approach to microservice architecture (the 1 command / 1 object / 1 function / 1 struct atomicity Lego-style), and in such a case most microservices would typically look very much alike, with exactly the same "general" parts (auth, db connect, etc) and just your business logic varying in between.
With so small microservices, a team of 4 may be responsible for a good dozen microservices, a team of 10 may handle a hundred... You quickly rise to high hundreds in such scenarios.
Disclaimer: This is extensively parroted from the latest episode of "Go Time", a fantastic podcast imho.
This seems really excessive. You'd have to have really good tooling to build, deploy, and debug (since log streams are oriented per-function, not per-request), and I'm not sure how that could be performant, especially if you're making O(N) (or worse) network calls per request. My understanding is that microservices are typically 1-5 per team.
I would imagine quite a services to just deal with legislation/taxation stuff in all the different markets they operate in. These can also result on several integrations to government systems - all country specific. In some markets they are partnering with taxi companies - maybe more integrations for ordering and reporting.
In general it is easy to underestimate the complexity of systems you are not familiar with. The devil is in the details and deeper you look, more complicated it usually gets.
> In general it is easy to underestimate the complexity of systems you are not familiar with.
Very easy.
A key-value store is theoretically easy, “just store data and give it back”, but a distributed / highly reliable key value store is a crazy beast.
Collision detection and response seems like a “solved” problem but perk at the code and you’ll find just how many lines of code a basic game spends preventing players from waking through walls.
My team has on the order of 20 microservices? 50 teams of that size would add up to 1,000.
These services include all sorts of worker processes, things that manage queues or monitor things, probers, administration, public interface and configuration. It works really well for us.
When I joined Amazon in 2014, there were ~120,000 microservices. A lot of them arne't used or just hello world tests apps. Every new employee had to spin one up during orientation.
1500 microservices? Are they exposing each function as a microservice or something? At which point could be having 1500 microservices a good thing? I honestly cannot think of one unless you have some massive design flaw or have a different definition for microservice.
I'll bite. Why do you suppose it's an emotional decision? And why do you assume they're rewriting things and not writing new code in support of their business model?
> As our business grew, so did our resource utilization and latencies; to keep Schemaless performant, we needed a solution that would execute well at scale.
That's the third sentence. Clearly it was not an "emotional decision" and it was in support of their business model (scale).
That's built on top of Mattermost which is open source. They do build their own infra instead of using cloud though so that probably uses a big part of it.
No buffered channels, or if you do, you must provide a very strong rationale ;)
I still use them quite a bit. Particularly synchronizing large rule sets as bit arrays. Of course you can use sync.Wait instead. But make(chan bool, N) semantics are just more convenient. It's stealth synchronization as a by-product. And hence the warnings about determinism!
I'd like to hear more about this rational. With a single writer, buffering channels can help smooth out inputs when the p90 is much higher than the average with channel writers but not readers. At least that's my impression.
Put in English, the type of an unbuffered channel is "a channel that always blocks on writing until the value has been read". The type of a buffered channel is "a channel that doesn't block when written to, until it is full in which case it blocks on writing until some other value has been read by some other unrelated goroutine".
The former is a reasonable concurrency primitive. The latter superficially seems similar, but is actually a much more complicated primitive, with the corresponding code understanding problems and the increased likelihood of more concurrency problems being hidden at low scale but coming out at scale (mostly deadlocks), plus the fact it seems similar is also problematic. The fact that the Go type system does not allow distinguishing between the two is also problematic.
It has some special-case purposes, but I also always scrutinize any channel created with buffering to make sure it is one of those special cases.
Buffered channels have not impressed me with their ability to do any sort of performance improvements. In almost all cases, if you've filled up your readers, you really want your writer to block. It provides cheap, effective backpressure; arguably for software-at-scale it's the most useful aspect of the channel primitive.
Backpressure to writers is the main reason to use buffer channels IMHO. I have never seen any performance differences between buffered or unbuffered for anything I have used.
The other reason to use them is controlling CPU. Spin up a pool of consumers and control how many cores they use with the buffer.
Personally, I prefer "var x = ..." and think short variables should be used as limited as possible. I remember that Go team has a not-very-concrete plan to depreciate short variable declarations.
This suggestion has its own drawback. When a parameter name changed, all the places using it must be modified to keep consistent. This is one reason why Go team rejected named arguments.
If this must be done, I recommend to define and use some named bool constants instead.
At my place of work, we pretty much always make the default (0) value “Unspecified”.
The rationale is, it’s often quite useful to know that the originator hasn’t specified a value, and to help new originators avoid unintended side-effects.
This style guide has the same effect, but it’s implicit; takes a few more mental cycles to parse in exchange for less code. Personally, I think it’s probably worth the trade off to type it out once and save time thinking later.
I'll throw in one request for functional options as written here: please don't use closures to capture the data.
If you use closures, it's nearly impossible to compare the results for equality in tests or code that might care to validate / deduplicate / whatever those args. You can achieve it with a moderate amount of heavily-implementation-dependent reflection (get the func type, construct arg(s), call func, check result), but that's about it.
Please just use values as the backing type. `type thing int` is utterly trivial to compare if needed, by comparison - just use `==`. Any code anywhere can construct the same argument in the same way and `==` to see if what they are checking is part of the args, and you're still not binding yourself to a specific implementation-type that'll later risk a compile-time failure.
(if users are casting to the private `int` type to check stuff, yea, it breaks - but the reflection-to-read-closure approach has that same problem, you can't stop it)
Also, in my experience, the "loop and apply" pattern tends to fall apart rather quickly, and you start wanting more complicated interactions between args / validation that you didn't pass both "include deleted" and "exclude deleted" and the second just silently clobbered the first. With values you can pretty easily loop and switch on the type and do whatever you need.
Which makes it a pain in 1) testing, since you can't see the options that were passed, and 2) middleware, since you can't see the options that were passed, to validate or extend them safely. And 3) debuggers or Printf since you can't see the value in the closure until it's executed.
Instead do:
type myOption int
func MyOption(arg int) Option {
return myOption(arg)
}
since it has none of those problems.
Whether you use the "loop and opt.apply(arg)" pattern or not doesn't really matter - that's a purely internal detail (personally I find it over-simplifies things and causes problems). Just "avoid passing values through func closures".
Not an expert, but could someone explain why it says: "Panic/recover is not an error handling strategy. A program must panic only when something irrecoverable happens such as a nil dereference." Why is that any more irrecoverable than anything else? (You can check if it's nil before referencing it, right?)
> Use go.uber.org/atomic
Atomic operations with the sync/atomic package operate on the raw types (int32, int64, etc.) so it is easy to forget to use the atomic operation to read or modify the variables.
go.uber.org/atomic adds type safety to these operations by hiding the underlying type.
I don't agree with this guideline. When you anyway have to remember to invoke an intrinsic function on the variable (unlike, say, operator overloading), I wonder why it's necessary to include yet another dependency that's just a wrapper for sync/atomic all because the developer doesn't pay attention to the right use of atomics.
The preference of channel size being unbuffered or just 1 is interesting. That seems like something specific to a problem domain; for instance, in projects I am working on now, having a large buffered channel (1000s deep) is useful for worker queues of thousands of goroutines, that all read from a task feeder channel. This type of queuing seems go-idiomatic, and negates the need for additional synchronization. In this case, the backpressure blocking on writers is a feature.
Picking a queue size is a latency/throughput/determinism tradeoff.
Picking a higher queue size can increase the peak throughput. The queue will smoothen out peaks and valleys in the workload, and the other end of the queue will get rarer into situations where there is no work. If you know the duration of the "valleys" in your workload you can size your queue exactly to get over them.
However a too big queue size can easily lead to higher latencies. In extreme situations the work might be already outdated at the point of time it's processed by the receiver. And backpressure on the producer is less given.
Queue sizes > 1 can also mask concurrency issues (like deadlocks), which will then only show up rarely in production when the queue is fully exhausted. I guess that's one of the main reasons why they picked the 0/1 rule.
Why having a 1000s buffered channel is useful in this case? If it's unbuffered, you still get the backpressure blocking on writers as a feature, since you have a worker queues of thousands of goroutines to read and handle them.
That's a good point, at steady-state, I'd imagine unbuffered channels would have the same throughput as a deeply buffered channels. The main advantage is being able to spool up faster and smooth out throughput, but it could be for most workloads that is not valuable enough. Perhaps I placed too much value on that.
Your comment (and others) have convinced me to do some more empirical testing and see how necessary buffered channels are for my goal.
A lot of my coworkers were getting hung up on this point too. I think they're just emphasizing that large channel buffers can hide concurrency problems, so you want to be careful when using them.
The last sentence in the recommendation emphasizes this: use them with scrutiny.
Yes, it's great. I know a few folks working on Go backends - I'm going to point this out to them. And I think like you, it raises the question of 'Is there a guide like this for what I'm working in?'
In my early React years, I had a lot of conversations about good vs bad as we got used to working in a declarative component hierarchy rather than the old imperative (tweak that DOM!) way. I still come across projects where people write html + bootstrap translated into a render method rather than using the power that is components.
I find it amusing that the advice in the style guide gives a good example contradicting another good example, and contains a subtle bug.
In the "Reduce Scope of Variables", second good example leaks an open file when WriteString fails, because it doesn't follow the own advice of "Defer to Clean Up" if you are curious.
How would you enforce any of these? Are these guides or reasons to not accept PRs? Seems like it would be worth encoding in a tool if this is for teams.
Yes. Variables that start with lower case look like a locally defined variable in a func. Adding the underbar makes it absolutely clear, and prevents shadowing, or the need to work around shadowing with a slightly different name.
This, to me, seems largely a superset of Google's style guide. There is some specific guidance for working with channels, mutexes, and atomics. My impression from my time at Google was that the Go team really expects you to never use mutexes and atomics, preferring goroutines for all synchronization. Sometimes that is impractical, though.
This is referenced in the first paragraph of the style guide:
"Styles are the conventions that govern our code. The term style is a bit of a misnomer, since these conventions cover far more than just source file formatting—gofmt handles that for us."
I've often seen guides for many languages that say don't use an assignment as an "if" condition. It may be a typo, and so is a source of errors, or it hides errors. Many compilers warn about it, and some people will consider it poor enough to refactor it out.
Of course it's not the assignment which is being tested in the Go code above. But it might as well be.
In Swift, Rust, Clojure and Perl, "if let" is common. ("if-let" in Clojure, "if my" in Perl). It's useful, and conforms well to the idea of limiting scope of the tested variable. I use it all the time.
So scope limiting in "if" is a good idea. But in those languages, they have the benefit of a clear syntax with a keyword, and it's a single assignment+test operator, very unlikely to be an accident due to a typo, or to hide an accident.
In contrast, I think the Go snippet looks error prone because the actual condition is all the way off the right. The assignment is obvious and the intention to test it can be presumed when skimming the code. But because the test is way off the right, at a horizontal position which will vary in different code, and at the end of a long line with a compound statement, I think it will be easy when skimming code to fail to notice if the condition is wrong due to a typo.
It helps reduce uninitialised variables, and if the condition is false, you don't have an extra variable hanging around in the scope that could potentially be misused by the developer later.
This could be used as an ad for Rust borrow checker, verbatim. You can't modify a map or slice you passed as an argument if a reference to it is stored!