How Go detects struct copies with sync.noCopy
func25.dev
func25.dev
if it looks like a hack, walks like a hack, and quacks like a hack...
The real hack to me is that anything which simply has Lock() and Unlock() methods is considered uncopyable.
Marker interfaces can exist in Go, but here they are another kind of hack. They must define at least one method (which doesn't have to be public), though that method is never actually meant to be called.
It's a shame they made them generic, taking no arguments and returning nothing; if they'd gone for `Lock(sync.LockType)` or something like that, it would be more difficult to accidentally trigger the `noCopy` flag.
We learned from C++ and Rust that languages can be so smart that people can't effectively use them. Go is the opposite. It's so dumb that anyone can use it, but it doesn't have some native language features you might want.
This feels like at least two layers of special behaviors.
Of course, they don't have to learn anything other than "use `go vet`, it's what enforces this" to use it. But that's true of almost anything?
I suppose you can add features to the language and then tell people to avoid learning them.
I have no clue what you're talking about and I doubt that it's relevant.
The claim was made that this is "simple". It doesn't feel simple to me. I'm asking about the criteria used and how this fits into that to justify the "simple" claim.
The implementation details of this construct are implementation details of the language.
The language could be expanded to support this, but then every user either need to ignore a part of the language, or learn it. Oddball hacks in the internals of a library don't affect users of the library. This article is a deep dive into how the internals of a library happen to work on this iteration of Go.
I could accept having an anonymous embedding as a kind of syntax directive. So many languages have had directives bolted on later, so that wouldn't be a deal breaker. But then it should be accessible in all code and (external) code should be able to implement behavior as well.
Just like the magic comments, it's a sad thing to see appear in this language because it feels like magic incantations one has to know that bend over backwards to not actually extend the language to fit the use case.
This was Go's design philosophy until Rob Pike left - to do the simple thing simply and not try to be clever about it.
Wait, which thing do you mean by a "list type" ? A growable array type like Rust's Vec<T> or C++ std::vector<T> or the ArrayList type seen in several languages ?
Or do you mean a linked list type akin to C++ std::list or std::forward_list or Rust's std::collections::LinkedList ?
"List" is vague, which is appropriate if you're talking about very high level abstractions where it doesn't matter how it works and 5 gigabytes, 5 bits, 5 weeks or 5 seconds are all finite so who cares - but in the real world we usually do care.
Plus they're extremely easy to build if you truly have a good use for one, particularly with generics.
I don’t mind spending a few extra weeks learning a more complex language if doing so saves me months of time down the track programming and debugging. That is an excellent investment.
At my workplace we've used many languages over the years (C#, Python, Go) and Go teams are the ones that by far do the least amount of yak shaving and have the most intelligible codebases.
Sad that the one language that managed to occupy that nice spot in language design space for an extended period of time, isn't doing so anymore.
Of course, you can actively restrict yourself to standard go, but not needing to do that was the whole point.
The support for generics is years old and I don't believe anyone who claims it has ruined the language. I've barely encountered them in the wild and I've never encountered the thing people were really worried about in the wild where something has 4 generic parameters that are themselves complicated generic parameters of other things. If you're encountering that, it is either some one-off library I've never encountered, or it's because you or your team are writing it, to which the solution is, stop that.
I'm not even sure I've yet seen a "generic" in a library in Go that isn't simply straight up a generic data structure, the core use case for generics. I've written a couple of such things but they're all internal code.
There is always two aspects to a language works in practice: how it formally works, and how the community uses it.
Go is (was?) simple, and also (encouraged by the language and influence from its developers) the community mostly aims to keep the usage simple.
For example, in typescript you can define a json value as something like:
type JSON = null | bool | string | number | [JSON] | {[k:string]: JSON}
Go forces me to reach for interface {}, and use a nest of dynamic dispatch code. It’s horrible. Go code is harder to write, harder to read and it runs slower as a result.The decision is baffling. Especially given go now has generics, which are waaay more complex than enums. And sum types in go could be used to fix the constant (result | error) boilerplate. And remove nullability. Sigh.
No (useful) language is simple. Claiming go is not simple without naming a language for comparison is a party foul.
I don't write haskell, but I understand the appeal. Haskell is this philosophy taken to its natural conclusion.
The primary way to deal with error-on-clean-up in RAII languages is to not rely exclusively on RAII for it. Rust's File type, for example, has sync_data and sync_all methods (which, to be fair, only even need to be called for writable file handles). I don't think there's anything wrong with this approach, but it ends up being just as explicit and therefore forgettable as defer.
It should be noted that you can (at least in Rust) actually implement defer using RAII; see e.g. the scopeguard crate. Since RAII is block-scoped, this defer is also block-scoped (like Zig) rather than function-scoped (like Go).
A system that is based on linear types would have an advantage here. In a linear type system, the compiler guarantees that you always call the cleanup function (destructor) exactly once. With such a system, you can have the destructor return an error result, and since the call will always be explicitly written in the code (rather than generated automatically by the compiler), there will always be an explicit errorn handling code branch.
If closing a file fails then you treat it the same as how you would treat a write failure:
int err = 1;
FILE *f = fopen("whatever.txt", "w");
if(f){
if(5 == fwrite("Hello", 1, 5, f))
err = 0;
if(0 != fclose(f))
err = 1;
}
return err;
Code which writes to files and doesn't check for errors on close is subtly incorrect, although my understanding is that kernel devs bend over backwards to make failure unlikely, probably because everybody does it incorrectly anyway.- is fp NULL or already already closed? return error
- call fflush() and return error if it fails (fflush also happens in userland, it does a seek() then a write() of the userland buffer)
- call close() and return error if it fails
close() follows essentially the same process inside the kernel: check fd is valid, call flush() (this time truly to disk), close it.
"Market pressure for language adoption".
Just like all the features they keep adding where they said that Go doesn't need them in first place, turns out those features exist in other programming languages for a reason.
2009 Go released.
June 2010 https://github.com/golang/proposal/blob/master/design/15292/...
Jan 2011 https://github.com/golang/proposal/blob/master/design/15292-...
March 2011 https://github.com/golang/proposal/blob/master/design/15292/...
Oct 2013 https://github.com/golang/proposal/blob/master/design/15292/...
Dec 2013 https://github.com/golang/proposal/blob/master/design/15292/...
April 2016 https://github.com/golang/go/issues/15292
Aug 2018 https://github.com/golang/proposal/blob/master/design/go2dra...
Aug 2018 https://github.com/golang/proposal/blob/master/design/go2dra...
June 2020 https://go.dev/blog/generics-next-step
Jan 2021 https://github.com/golang/go/issues/43651
Jan 2021 https://github.com/golang/proposal/blob/master/design/43651-...
Feb 2021 Proposal accepted.
March 2022 Ships in Go 1.18.
You could criticize Go for moving too slow. But I'm pretty happy with the way they landed. They definitely don't feel bolted-on.
> In three years of Go surveys, lack of generics has always been listed as one of the top three problems to fix in the language.
https://blog.golang.org/why-generics
> In retrospect, we were biased too much by experience with C++ without concepts and Java generics. We would have been well-served to spend more time with CLU and C++ concepts earlier.
https://github.com/golang/proposal/blob/master/design/go2dra...
And naturally Rob Pike's pearl,
> think the transition to getting polymorphism into the language through generics is has still got some years to work through there's still things that don't quite work right
Seriousness is a hard topic among Gophers, what today does not matter, is apparently quite relevant tomorrow, and generics aren't the only example where this kind of pivots take place.
So whatever.
Also, AFAIK, they only leverage standard language behaviour and are not a hardcoded special case.
- https://x.com/valyala/status/2088638160242683954
He also gave an answer of what he would change now: https://itnext.io/go-evolves-in-the-wrong-direction-7dfda8a1...
It seems he's not happy anymore with the new direction of Go because they're implementing things from other languages.
Go advances one blub ragequit at a time.