Most languages would encode such behavior in a trait or a protocol instead of a zero-length struct field. It's type information and ought to be encoded in the type. From that perspective, I think it is a hack.
This feels like a style complaint and not one about substance. An inaccessible (because it's named _) zero-length struct field is just another kind of metadata. It also doesn't require you to pollute the method set, which would be a bigger issue.
The real hack to me is that anything which simply has Lock() and Unlock() methods is considered uncopyable.
Coming from Rust anyway (can't say I'm familiar with Go), nothing about having it as a trait would pollute the method set, traits don't have to have methods (e.g. pin, unpin, send, sync, etc.). Yes a zero length field can be metadata and this is a style complaint but a struct's fields are traditionally its data and the type is its metadata, I feel like it's harmful to mix these two as I at least wouldn't generally look at fields to find metadata for the struct.
Interfaces in Go (the closest equivalent to traits in Rust) don't have to have methods either, but they are structurally typed. This is like "static duck typing" if you will. So an interface with no methods is implemented by every type in the language (indeed, this became such a useful pattern that the name "any" was reserved for it in Go 1.18).
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.
I'm only peripherally familiar with Go. IIRC, Rust defaults to move-only structs with the option to make them copyable with the Copy trait. Swift defaults to copyable structs (like Go), but has a ~Copyable generic type constraint to make them move-only.
The real hack to me is that anything which simply has Lock() and Unlock() methods is considered uncopyable.
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.
Go's entire schtick is being simple, eschewing language complexity in favour of letting the programmer handle it themself. Like C, but with pointer safety and garbage collection.
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.
How much additional complexity do users need to be aware of when writing new code because of the way this was implemented? Adding new features makes the language bigger, for something that Claude tells me is used 20 times in total in the entire Go codebase.
Because it doesn't expand the language and force the user to learn new features, it instead puts the ugliness deep in the library where nobody needs to interact with it.
This explicitly leverages a hidden "feature" of the language, which is how Sync impacts copying (rather unintuively). It leverages this to create an interface based on that hidden feature to create a marker that is consumed by a specific tool (not the compiler).
This feels like at least two layers of special behaviors.
noCopy. In order to understand it, they have to learn that this is enforced only by `go vet` and that the marker relies on everything described in the `2. How go vet and noCopy work` section of this article.
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?
It's the difference between trying to learn the language and trying to learn the implementation details of a library. Is the kind of crazy hack that Rust std uses to prevent TOCTOU attacks with symlinks a language complexity issue or not?
I suppose you can add features to the language and then tell people to avoid learning them.
Is the kind of crazy hack that Rust std uses to prevent TOCTOU attacks with symlinks a language complexity issue or not?
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 answer is that the implementation details are abstracted away by the library, the same way that the implementation details of Rust std::file access are abstracted away. That means that while the implementation may not be beautiful, the language doen't need to expand in order to accommodate 20 uses of the feature.
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.
Nah... I like Go, but this is a hack. Wiktionary --not the ultimate authority, I know, but still-- defines to hack as: To make a quick code change to patch a computer program, often one that, while being effective, is inelegant or makes the program harder to maintain.
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.
I thought about it again, but you can of course use your own types in this very way. You can "test" if a struct has a certain (zero-sized, embedded) member. It's just cumbersome, and needs to be implemented through-out your code, so it's only suitable for very small "flagging" operations. It's not comparable to annotations in other languages.
Comments
It's not really a hack. It's a hint to a static analyser, that's all.
Most languages would encode such behavior in a trait or a protocol instead of a zero-length struct field. It's type information and ought to be encoded in the type. From that perspective, I think it is a hack.
This feels like a style complaint and not one about substance. An inaccessible (because it's named _) zero-length struct field is just another kind of metadata. It also doesn't require you to pollute the method set, which would be a bigger issue.
The real hack to me is that anything which simply has Lock() and Unlock() methods is considered uncopyable.
Coming from Rust anyway (can't say I'm familiar with Go), nothing about having it as a trait would pollute the method set, traits don't have to have methods (e.g. pin, unpin, send, sync, etc.). Yes a zero length field can be metadata and this is a style complaint but a struct's fields are traditionally its data and the type is its metadata, I feel like it's harmful to mix these two as I at least wouldn't generally look at fields to find metadata for the struct.
Interfaces in Go (the closest equivalent to traits in Rust) don't have to have methods either, but they are structurally typed. This is like "static duck typing" if you will. So an interface with no methods is implemented by every type in the language (indeed, this became such a useful pattern that the name "any" was reserved for it in Go 1.18).
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.
I'm only peripherally familiar with Go. IIRC, Rust defaults to move-only structs with the option to make them copyable with the Copy trait. Swift defaults to copyable structs (like Go), but has a ~Copyable generic type constraint to make them move-only.
Rust has similar zero-sized-types, probably the most 'magical' being PhantomData.
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.
Soooo like a PhantomData in Rust?
Go's entire schtick is being simple, eschewing language complexity in favour of letting the programmer handle it themself. Like C, but with pointer safety and garbage collection.
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.
How is this simple? It's basically a hint to `go vet` that uses a special interface pattern. I'm baffled by what some people call "simple" lol
How much additional complexity do users need to be aware of when writing new code because of the way this was implemented? Adding new features makes the language bigger, for something that Claude tells me is used 20 times in total in the entire Go codebase.
I don't know the answer to those things, it feels like it's sort of on the one saying "This is simple" to explain why.
Because it doesn't expand the language and force the user to learn new features, it instead puts the ugliness deep in the library where nobody needs to interact with it.
This explicitly leverages a hidden "feature" of the language, which is how Sync impacts copying (rather unintuively). It leverages this to create an interface based on that hidden feature to create a marker that is consumed by a specific tool (not the compiler).
This feels like at least two layers of special behaviors.
What's "this", and what does the user need to learn to make use of it?
noCopy. In order to understand it, they have to learn that this is enforced only by `go vet` and that the marker relies on everything described in the `2. How go vet and noCopy work` section of this article.
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?
It's the difference between trying to learn the language and trying to learn the implementation details of a library. Is the kind of crazy hack that Rust std uses to prevent TOCTOU attacks with symlinks a language complexity issue or not?
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 answer is that the implementation details are abstracted away by the library, the same way that the implementation details of Rust std::file access are abstracted away. That means that while the implementation may not be beautiful, the language doen't need to expand in order to accommodate 20 uses of the feature.
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.
Nah... I like Go, but this is a hack. Wiktionary --not the ultimate authority, I know, but still-- defines to hack as: To make a quick code change to patch a computer program, often one that, while being effective, is inelegant or makes the program harder to maintain.
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.
I thought about it again, but you can of course use your own types in this very way. You can "test" if a struct has a certain (zero-sized, embedded) member. It's just cumbersome, and needs to be implemented through-out your code, so it's only suitable for very small "flagging" operations. It's not comparable to annotations in other languages.