Indeed. Go types are like simple locks: they keep honest users honest, but they don't prevent dishonest users from breaking your assumptions. The point is just that "compile-time type safety for enumerations" isn't actually true, or even possible. You always have to do some amount of runtime validation of received values.
No, you don't. If your module relies on compile time checks for validity, there's no reason for you to also include runtime checks. Other developers breaking your defined contract is not your responsibility, it's not even a bug.
> If your module relies on compile time checks for validity,
...then it's broken, because the Go compiler, factually, cannot (in general) check or enforce the validity of specific values.
> there's no reason for you to also include runtime checks. Other developers breaking your defined contract is not your responsibility, it's not even a bug.
It is absolutely the responsibility of the function
func PrintColor(c Color)
to ensure, at runtime, that `c` is a valid Color. My example code -- which allowed an invalid Color value of "orange" -- is absolutely a bug in PrintColor.
Exactly. Someone could always connect to your program with a debugger and modify the instructions are being run, and that's why you need to regular runtime checks of the validity of your program text. If you want to handle people actively trying to undermine the assumptions your program is making, you can't leave out easy to replicate cases like 'gdb attach', obvious, non-accidental use of the type system, and other similar categories of issues.
What strategies do you use in your programs to ensure type safety in the event of debugging, mmap, and other mechanisms that can be used to subvert the memory layouts your compiler thinks it produced?
Most instructions are not atomic, so even if you had runtime checks something like a debugger could still inject invalid state in between your checks, making them ineffective.
If real-time code injection is your threat model, I don't see how runtime checks would get you anywhere.
You seem to be suggesting that (to use a common phrase) if something can be done, it should be done. The previous poster's example regarding a debugger was to challenge the notion that there was any meaningful reason to constrain non-accidental type subversion. You seem to be saying it's absurd to care about someone using a debugger to inject invalid code at runtime because there's nothing you can do about it, thus implying if you can do something, you should. But that only begs the question--why should you? To some people, defending against debugger code injection is no more absurd than defending against other examples where the programmer deliberately alters code to subvert static type checking. If you disagree then articulating more meaningful distinctions among cases would be worthwhile. Mere ability is generally not considered sufficient justification alone to do something.
This is an inane comparison. Casting as demonstrated in the example is normal and has to be expected. Go types aren't as strict as types in other languages. Code can't assume otherwise.
> Casting as demonstrated in the example is normal and has to be expected.
Can you give an example where someone has done similar casts accidentally? It seems like it would be hard to accidentally typo. That leaves malice, which seems difficult to defend against, in light of the kinds of system calls that are available to a program.
The example given wasn't a simple cast of a value, and definitely not an implicit coercion. The example is more like C-style type punning where you explicitly cast a pointer to the value and then write through the dereferenced pointer.
I don't doubt that there are niches where such explicit type coercion patterns are common in Go and susceptible to mistakes, but I doubt usage of constant identifiers is such a niche.
Rust is currently the standard bearer for strong, static type safety, and it even has both the enum types and pattern matching construct which Go lacks. AFAIU, you can use unsafe{} Rust code to perform a similar type punning trick, successfully assigning an invalid value to an enum object. I don't know if Rust's code generator always inserts runtime validity checks in match statements on an enum value without a catchall/default case, but certainly it's possible for Rust code to have an explicit if/else chain that at compile time appears comprehensive but which would neither panic nor produce the expected behavior. Does that mean Rust programmers shouldn't rely on Rust's static typing, instead always adding explicit code to handle unknown/invalid enum values?
Maybe the assertion that Go code should have such checks is more reasonable than for Rust code, but you haven't explained how. At least to me, the simplest, minimal code to achieve the subversion in both Go and Rust seems similarly stilted and similarly unlikely to be written by mistake. (To be clear, the context of this subthread as I understand it assumes the interface method hack, the subversion of which requires the type punning.)
Can you point at a git commit where someone had a similar bug implemented by accident and fixed it? The code you posted earlier seems to be something that would be incredibly hard to write without malice. And if you're considering malice, you also need to consider mmap, foreign function interfaces, writes to /dev/memory, and other similar perverse mechamisms.
It’s actually not and is one of those Java isms that have made its way into generalized software engineering.
If I write a function that takes Foo as an argument. I have a Foo implementation exposed elsewhere in my program. It is absolutely expected that I mean MY Foo, not your Foo. If you pass me your Foo, you will get unexpected results that are not a bug, not a side effect, not my responsibility. It’s yours, the caller, to adhere to the contract.
There are valid reasons to not adhere. To pass your own implementation. At that point, you are responsible for its use. Not me. If it adheres to MY Foo’s interface, you might get by, but it’s is not my responsibility to validate all permutations of unknown types to ensure you’re passing me mine. In go, it’s perfectly valid to return structs but accept interfaces as arguments so that you, the program author calling my api, can craft the correct program flow.
So please, leave that runtime type check reflection for Java and C# and the land of JS. We have no need for it here in machine code land.
> it’s is not my responsibility to validate all permutations of unknown types to ensure you’re passing me mine.
Of course it is! If you provide an API that takes a Color, and you define valid Colors as exclusively Red, Green, and Blue, then you are absolutely responsible for validating input Color values and rejecting anything which is not Red, Green, or Blue. If you delegate that responsibility to the caller, then your API provides no meaningful encapsulation, and isn't a useful abstraction -- it's entirely leaky. No bueno.
‘type Color string’ is just a type alias for string.
Likewise GLint is just a type alias for int. There are only value types (str, int, float, etc), everything else is a construct. The only true types are those value types (and pointers to them). If you call a type a Color and I call a type a Color, you are using my lib to build a program (not me using yours), you must adhere to my contract of what a Color type is to my API. Period. You can not call a function with an unknown type and expect it to behave properly.
The point here is that, in Go, "what a Color type is" isn't something that can be enforced by the compiler. And more specifically, Go doesn't allow you to define reliable enums.
It is enforced by the compiler, what it doesn't do is guarantee it at runtime. I agree with you on the enums. Go doesn't have a reliable enum construct. But the argument that I, the library author, must validate and check against any possible type of "color" you, the program author, can come up with is just crazy talk. I'll provide a Color type, I'll even pre-define some colors for you, but if you send me a Color that's rainbow, I'll panic.
By the same token, Rust has no type safety because std::mem::transmute exists. In practice, it's not a problem because transmute is not a valid way to construct or interact with values (outside of a few special cases). By calling transmute you're explicitly opting out of all safety and abstraction, and entering here-be-dragons territory.
(That said, the Go syntax looks far less "scary" than `unsafe { transmute::<_, Colour>("orange") }`, which I'd definitely call a design issue.)