Leveraging the Go Type System(gopherguides.com)
gopherguides.com
Leveraging the Go Type System
https://www.gopherguides.com/articles/leveraging-the-go-type-system
111 comments
If generics are a representative example, Go will eventually get enums, and probably ones that look very much like what C# or Java have, after years of insisting that you don't actually need them, and that everybody is doing them wrong. ~
I agree, this article is a perfect example of the weak parts of Go.
I love Go for many decisions that have been made during its development, but the handling of enumerations is certainly not part of it.
I love Go for many decisions that have been made during its development, but the handling of enumerations is certainly not part of it.
I don't mind that the compiler doesn't provide a default string implementation by default. I'm happy to use string values in my enums (in most cases const string vs int doesn't matter much for performance) for convenience; what I do wish for is full-blown ADTs. I want to be able to express `'Hello' | 'World' | 42 | struct{Name string, Age int} | []int` and know that the compiler will enforce. You can sort of get there with Go through various workarounds and with enough boilerplate (and no, I'm not one to complain about error handling or for loop boilerplate, but the boilerplate to workaround ADTs is much more tedious and much less discernible.
Do you have any examples of how you would implement a makeshift sum type like your example?
Here's one such workaround: https://play.golang.org/p/4Fd7aKRtmvz
It's not great for a number of reasons. First of all, `nil` is always a valid permutation even if we don't want it to be. Secondly, it's a lot of work to express the constraints we want. Thirdly, it's not perfectly type-safe; someone who was determined to shoot themselves in the foot could construct other instances of our "singleton types" if they really wanted to. Fourthly, boxing all of these values will almost certainly cause unnecessary allocations which would be a bummer if we wanted to create a lot of these quickly (this is an implementation detail and may not be any worse than a language with first-class support for sum types).
It's not great for a number of reasons. First of all, `nil` is always a valid permutation even if we don't want it to be. Secondly, it's a lot of work to express the constraints we want. Thirdly, it's not perfectly type-safe; someone who was determined to shoot themselves in the foot could construct other instances of our "singleton types" if they really wanted to. Fourthly, boxing all of these values will almost certainly cause unnecessary allocations which would be a bummer if we wanted to create a lot of these quickly (this is an implementation detail and may not be any worse than a language with first-class support for sum types).
Fifth, and most critically for me why it’s not really any better than a comment on an interface{}: a user’s matches won’t be checked for exhaustiveness if you add a new type, let alone them poking around the internals. You can box them safely, but not unbox them.
Yes, that’s an important point; thanks for noting.
[deleted]
X interface{} // int(3) | int(5) | []float64 | *Z
It sucks.We can recoup a fair bit more type safety than this if we're interested in going through the effort: https://play.golang.org/p/4Fd7aKRtmvz. See my sibling comment for more details.
I had heard that the Go type system was just annoying and too simple to be useful, and this blog post has convinced me that that is 100% true.
It might be a little premature to judge an entire language on an introduction article for beginners. This article isn't intended to show every possible way Go can solve a problem, but intended to show people new to Go different ways to think about types. In a production scenario, it's unlikely I would use this solution. This is really an "academic" exercise in types for Go, not a practical application.
My reply was at least partially facetious as this wasn't my first exposure to Go. If you like the simplicity of Go's type system I can't argue with that, but I do think your article demonstrates the way that simplicity at the language level pushes complexity into the users code. Pick any modern statically typed language and this whole post will boil down to something like:
data Genre = Adventure
| Comic
| Crime
| ...
deriving (Show)So what you are describing is enums but way more complicated?
In a way, yes. Go doesn't have the concept of enums, so this is as close as it gets.
Further demonstrating the point that simplicity in the language just winds up forcing complexity into your code.
And unsafe, since you can trivially make a `Genre(1138)` and that's correct, compile-able code.
The same is true for a `Genre(0)`, which is easy to make by accident with just a `var genre Genre` declaration (that's a `Genre(0)` already). Or by ignoring an error return, like in `json.Unmarshal(data, &genre)` (that func returns an error, but that fact is invisible in text, and you are not required to assign or handle it. fun for code reviews!).
You can also have a Genre-arg func like `func GenreToString(g Genre) string` and call it with `GenreToString(12345)` and that'll also compile.
---
Go does not have anything even remotely like enums. It has a short-hand for auto-incrementing constants, nothing more. As if that were somehow the most valuable part of constants / enums.
The same is true for a `Genre(0)`, which is easy to make by accident with just a `var genre Genre` declaration (that's a `Genre(0)` already). Or by ignoring an error return, like in `json.Unmarshal(data, &genre)` (that func returns an error, but that fact is invisible in text, and you are not required to assign or handle it. fun for code reviews!).
You can also have a Genre-arg func like `func GenreToString(g Genre) string` and call it with `GenreToString(12345)` and that'll also compile.
---
Go does not have anything even remotely like enums. It has a short-hand for auto-incrementing constants, nothing more. As if that were somehow the most valuable part of constants / enums.
I really like this example, because it highlights both the strengths and weaknesses of Go in the same example.
On the one hand: Good language support for a specialized type and a method on that type, without making the type a heavyweight class (i.e. all that magic can happen at compile-time; Genre can still be an int "under the hood").
On the other hand: wow, that's a lot of boilerplate to write just to get the string representation of a token the compiler already knows about. As other commenters have noted, `go generate` will decrease the amount of code that needs to be hand-written, but it still smells like something the language should handle without explicit code.
On the one hand: Good language support for a specialized type and a method on that type, without making the type a heavyweight class (i.e. all that magic can happen at compile-time; Genre can still be an int "under the hood").
On the other hand: wow, that's a lot of boilerplate to write just to get the string representation of a token the compiler already knows about. As other commenters have noted, `go generate` will decrease the amount of code that needs to be hand-written, but it still smells like something the language should handle without explicit code.
For an example with Go generation and using iota see stringer[0] and Rob Pike's related blog post[1]
[0] https://pkg.go.dev/golang.org/x/tools/cmd/stringer
[1] https://blog.golang.org/generate
[0] https://pkg.go.dev/golang.org/x/tools/cmd/stringer
[1] https://blog.golang.org/generate
Go has so many `go generate` tools to overcome its shortcomings, it's ridiculous.
This is true, but I'll take these shortcomings over deficiencies in tooling, ecosystem, performance, learning curve, etc any day. In other words, I can ship software pretty easily with a suboptimal type system, but much less easily if I have to select for candidates with a decade of experience in a particular VM runtime or if the performant runtimes are incompatible with important parts of the ecosystem or if there is no sane package management tooling or if the ecosystem itself is lacking important high quality packages. I'm of the opinion that in-language features are overrated.
To be fair, Go's package management is far from sane (though better than nothing, of course). The tooling is also sub-par for modern languages, especially in terms of free development environments, though go-pls is slowly improving.
> To be fair, Go's package management is far from sane (though better than nothing, of course)
It's not perfect, but it's among the best. It's far better than the popular offerings for Python, C, C++, and Java. NPM was also pretty terrible last I used it (needed to regularly blow away node_modules and reinstall things, random ENOENT errors, etc) and I think the .net world almost adopted a sane project file structure before reverting back to MSBUILD but I might be wrong there. OCaml was also pretty terrible last I used it, but people swear to me that Dune has fixed everything (though those guys have been telling me for the decade that no one needs shared memory parallelism and also it's right around the corner, so maybe take that with a grain of salt).
> The tooling is also sub-par for modern languages, especially in terms of free development environments, though go-pls is slowly improving.
Go doesn't have great IDEs because it's always been more of a text-editor language, and few languages have editor plugins of comparable quality. It also doesn't have a great debugging story (there is Delve, but I've heard that it's not a great debugging experience). These don't really chafe me because I don't like IDEs and I'm the unpopular guy who hates debuggers. Those are the biggest "gaps".
For other things though, Go is great. Testing? Built in. Benchmarking? Built in. Profiling? Built in. Cross compilation? Built in. Static linkage? It's the default. Formatting? Built in. Documentation generation? Built in. Code and documentation packaging and publishing? Built in (to git). Not only are all of these things built in, but they're standard across the ecosystem so developers can onboard with minimal learning curve.
It's not perfect, but it's among the best. It's far better than the popular offerings for Python, C, C++, and Java. NPM was also pretty terrible last I used it (needed to regularly blow away node_modules and reinstall things, random ENOENT errors, etc) and I think the .net world almost adopted a sane project file structure before reverting back to MSBUILD but I might be wrong there. OCaml was also pretty terrible last I used it, but people swear to me that Dune has fixed everything (though those guys have been telling me for the decade that no one needs shared memory parallelism and also it's right around the corner, so maybe take that with a grain of salt).
> The tooling is also sub-par for modern languages, especially in terms of free development environments, though go-pls is slowly improving.
Go doesn't have great IDEs because it's always been more of a text-editor language, and few languages have editor plugins of comparable quality. It also doesn't have a great debugging story (there is Delve, but I've heard that it's not a great debugging experience). These don't really chafe me because I don't like IDEs and I'm the unpopular guy who hates debuggers. Those are the biggest "gaps".
For other things though, Go is great. Testing? Built in. Benchmarking? Built in. Profiling? Built in. Cross compilation? Built in. Static linkage? It's the default. Formatting? Built in. Documentation generation? Built in. Code and documentation packaging and publishing? Built in (to git). Not only are all of these things built in, but they're standard across the ecosystem so developers can onboard with minimal learning curve.
> It's not perfect, but it's among the best. It's far better than the popular offerings for Python, C, C++, and Java.
Maven is significantly better than Go modules: it doesn't require special support from your git server, it doesn't rely on magic tags with magic formats, it does strict dependencies by default, it handles branching any way you want, it works with binary artifacts not just source code, it has explicit support for custom repos, it doesn't require renaming your package to do a major version upgrade, it doesn't rely on DNS for package names, it works for small cross-language dependencies, and probably others.
> For other things though, Go is great. Testing? Built in. Benchmarking? Built in. Profiling? Built in. Cross compilation? Built in. Static linkage? It's the default. Formatting? Built in. Documentation generation? Built in. Code and documentation packaging and publishing? Built in (to git).
Testing is actually pretty bad, go test is OK, but the lack of any kind of basic testing tools like a deep equality or mocking helpers make it much harder to use than most. Code coverage measurement is also pretty bad, and un configurable.
Benchmarking is decent, it's nice to have it built in, no complaints there.
Profiling is horrible, just some text format that's barely documented, no memory dumps, no thread bases profiling, no tooling to analyze complex profiles.
Cross compilation is nice, but it's not even required in the most direct competitors (Java, C#).
Static linkage is the only option, and neither a good nor a bad feature.
Formatting is forced on everyone, and is completely unconfigurable. Their choices often mess up for merges for stupid anesthetic reasons.
Documentation is built-in to Java and C# as well, and it supports more than basic text for describing your code. They also don't rely in any way on your choice of source control system.
Overall, Go's ecosystem is definitely sub par compared to Java or C#. It's not the worse, and it can do the job, but you'll never be as productive.
Their support for small fats starting binaries is the only thing where Go really shines.
Maven is significantly better than Go modules: it doesn't require special support from your git server, it doesn't rely on magic tags with magic formats, it does strict dependencies by default, it handles branching any way you want, it works with binary artifacts not just source code, it has explicit support for custom repos, it doesn't require renaming your package to do a major version upgrade, it doesn't rely on DNS for package names, it works for small cross-language dependencies, and probably others.
> For other things though, Go is great. Testing? Built in. Benchmarking? Built in. Profiling? Built in. Cross compilation? Built in. Static linkage? It's the default. Formatting? Built in. Documentation generation? Built in. Code and documentation packaging and publishing? Built in (to git).
Testing is actually pretty bad, go test is OK, but the lack of any kind of basic testing tools like a deep equality or mocking helpers make it much harder to use than most. Code coverage measurement is also pretty bad, and un configurable.
Benchmarking is decent, it's nice to have it built in, no complaints there.
Profiling is horrible, just some text format that's barely documented, no memory dumps, no thread bases profiling, no tooling to analyze complex profiles.
Cross compilation is nice, but it's not even required in the most direct competitors (Java, C#).
Static linkage is the only option, and neither a good nor a bad feature.
Formatting is forced on everyone, and is completely unconfigurable. Their choices often mess up for merges for stupid anesthetic reasons.
Documentation is built-in to Java and C# as well, and it supports more than basic text for describing your code. They also don't rely in any way on your choice of source control system.
Overall, Go's ecosystem is definitely sub par compared to Java or C#. It's not the worse, and it can do the job, but you'll never be as productive.
Their support for small fats starting binaries is the only thing where Go really shines.
To be honest, looking at your arguments for Maven being better, most of those are pros or cons, depending on where you stand. They're tradeoffs.
Also, as far as I know, Go doesn't require special support from your git (or whatever VCS you use) server, as is confirmed by the reference: https://golang.org/ref/mod#vcs
Though I mainly wanted to comment on the profiles part. Could you please provide links to tools which are in your opinion good for complex profiling use cases?
I'm asking because I myself really like both the web based profile viewer, as well as the go tool trace viewer, for more complex situations.
However, to address the other points: testing has good libraries for equality and mocking (I.e. testify); formatting is a trade-off, many people prefer one forced consistent code style, even if they don't like it
Overall, your conclusion of the Go ecosystem being "definitely worse" looks quite far fetched based on your arguments. Not to say it's wrong, the reasoning just isn't at all convincing.
Also, as far as I know, Go doesn't require special support from your git (or whatever VCS you use) server, as is confirmed by the reference: https://golang.org/ref/mod#vcs
Though I mainly wanted to comment on the profiles part. Could you please provide links to tools which are in your opinion good for complex profiling use cases?
I'm asking because I myself really like both the web based profile viewer, as well as the go tool trace viewer, for more complex situations.
However, to address the other points: testing has good libraries for equality and mocking (I.e. testify); formatting is a trade-off, many people prefer one forced consistent code style, even if they don't like it
Overall, your conclusion of the Go ecosystem being "definitely worse" looks quite far fetched based on your arguments. Not to say it's wrong, the reasoning just isn't at all convincing.
> Also, as far as I know, Go doesn't require special support from your git (or whatever VCS you use) server, as is confirmed by the reference: https://golang.org/ref/mod#vcs
One point of clarification- pretty much no one uses the VCS qualifier (.git in the module path) so the nicer looking import paths work by having a web server respond with the special meta tag. That could be what OP was referencing.
Also, tangentially, Drew DeVault found out that the docs hosted on pkg.go.dev only use a whitelisted set of domains for sources.
https://drewdevault.com/2020/08/01/pkg-go-dev-sucks.html
One point of clarification- pretty much no one uses the VCS qualifier (.git in the module path) so the nicer looking import paths work by having a web server respond with the special meta tag. That could be what OP was referencing.
Also, tangentially, Drew DeVault found out that the docs hosted on pkg.go.dev only use a whitelisted set of domains for sources.
https://drewdevault.com/2020/08/01/pkg-go-dev-sucks.html
Makes sense, after all it is catching up with Mac OS, OS/2 and Windows 95 IDEs.
Yeah, I imagine it's a real problem for the people who don't know their way around a shell.
Or those that grew up with CP/M, MS-DOS and Xenix and aren't stuck in those days.
Yeah, remember macro based generators before C++ got templates, or Eclipse EMF before Java got annotations?
Although we did survive with them and delivered code into production, it doesn't mean we keep using them 20-30 years later.
Although we did survive with them and delivered code into production, it doesn't mean we keep using them 20-30 years later.
This is cool and something about Go that I hadn’t researched much yet. It does make me wonder, however, why not just include some higher level structure for this? Or even some method built into the compiler? It feels odd to have to install a dependency like `stringer` just to build. Do I had it to my go.mod file? Also, the comment structure, while flexible, could be greatly simplified for common cases like this if there was, for instance, an enum type.
I really do enjoy Go, but there are several examples of things like enums that are left out because there is some way of doing it otherwise to only the theoretical advantage of “elegance” at the cost of developer productivity.
I really do enjoy Go, but there are several examples of things like enums that are left out because there is some way of doing it otherwise to only the theoretical advantage of “elegance” at the cost of developer productivity.
Perhaps the Golang managers would do well to elevate stringer into their suite of supported commands:
https://golang.org/cmd/
https://golang.org/cmd/
The points about iota are interesting. I always define the first constant as "unknown" when defining a set of iota-driven constants. That way the zero value is "unknown" so if I create a new struct with no initialiser it doesn't accidentally inherit a value I didn't mean it to have, and instead gets the "unknown" value.
It also doesn't touch on the other useful "system" funcs to include on a type (Scan, Value, Marshal, etc)
It also doesn't touch on the other useful "system" funcs to include on a type (Scan, Value, Marshal, etc)
Agreed. There are a couple of clever ways to handle iota, but as my next article suggests, in general, I steer away from them if you really just need constants.
I didn't go into any of the Scan/Value/Marshal etc as it was a little beyond the scope of this article. I can certainly do a follow up article on it though
Thanks for the feedback!
I didn't go into any of the Scan/Value/Marshal etc as it was a little beyond the scope of this article. I can certainly do a follow up article on it though
Thanks for the feedback!
Yeah I read the article about iota. It was interesting, and there's stuff there I agree with, but on the whole iota is too useful not to use (imho).
I feel like there are times when zero values can make things awkward (e.g. boolean flags often need to be expressed in the "negative" form because the zero value is false), but this is a great idea for iota!
Totally agree. Time zero values are my bane. I've started using sql.NullTime for all time values regardless, just so I'm clear about what is what.
* yes I know time.IsZero() is a thing, and the semantics are similar, but I want the compiler to warn me if I'm trying to use a time without checking if it's actually initialised first.
* yes I know time.IsZero() is a thing, and the semantics are similar, but I want the compiler to warn me if I'm trying to use a time without checking if it's actually initialised first.
This article should be written with Rust/Haskell/Scala/OCaml/TypeScript ... or any languages with an insane type system. Anyway it can’t be Go, a language without sum type.
I keep wanting to get into Go, and I pick it up on and off over the years but just can't. It brings so much good, but it has actually removed too much.
It needs sum types and pattern matching. Once you have used them, not having them is a huge gray void. Const expr, interfaces, sum types and pattern matching get you 85% of the way there.
It needs sum types and pattern matching. Once you have used them, not having them is a huge gray void. Const expr, interfaces, sum types and pattern matching get you 85% of the way there.
Personally, I think the only part that makes sense is making a type to hide the implementation. That String() method is kinda nasty: It's O(n), and you need to edit it every time a category gets added/removed/changed.
I think it'd make more sense to just make a idiomatic struct:
This also means that you don't accidentally leak an "is-a" relationship between the nominal type and the underlying implementation type
I think it'd make more sense to just make a idiomatic struct:
type Genre struct {
id int
name string
}
func New(id int, name string) Genre {
return Genre {id, name}
}
Then, you can define your categories normally: var (
Adventure = genre.New(1, "Adventure")
Comic = genre.New(2, "Comic")
// etc
)
And have a O(1) String() method that doesn't need to be edited every time a new category gets added/removed/changed: func (g Genre) String() string {
return g.name
}
You can also change the implementation of the type without breaking consumers (e.g. maybe you want a pointer to a struct instead of a struct)This also means that you don't accidentally leak an "is-a" relationship between the nominal type and the underlying implementation type
var foo Genre = Adventure
foo + 1 // ought to throw a compilation error!> Then, you can define your categories normally:
> var (
> Adventure = genre.New(1, "Adventure")
> Comic = genre.New(2, "Comic")
> // etc
> )
Yet there is nothing preventing anyone from reassigning Adventure to genre.New(2, "Comic") or some other arbitrary value. The fact that golang doesn't have the equivalent of Java's `final` is just poor design.
> var (
> Adventure = genre.New(1, "Adventure")
> Comic = genre.New(2, "Comic")
> // etc
> )
Yet there is nothing preventing anyone from reassigning Adventure to genre.New(2, "Comic") or some other arbitrary value. The fact that golang doesn't have the equivalent of Java's `final` is just poor design.
There's nothing preventing someone from editing the source code to `const Comic = Adventure` either. Either you have access to edit String() AND have access to mess with the definitions in source code, or you don't have access to either. The idea that `final` can protect you against yourself is kinda silly IMHO.
Think about the use case of a library import. In golang, the user has the ability to change library "constants", wreaking havoc in the process.
Secondly, `final` serves as strong and clear documentation. We know just from looking at the definition that this is variable is not going to be re-bound.
On a side note, this compiles in golang:
Secondly, `final` serves as strong and clear documentation. We know just from looking at the definition that this is variable is not going to be re-bound.
On a side note, this compiles in golang:
func main() {
true := false
if !true {
fmt.Println("wat")
}
}I mean, at some point, garbage in garbage out, no? For example, in javascript, you can do `Array.prototype.map = null`, in C you can do `#define if while`, etc.
It's highly unidiomatic to mutate bindings from libraries in any language, even ones that technically allow you to, so the observation that go is one of those languages feels like nitpicking at obscurities.
My Java is rusty, but I recall that several years ago I ran into some stuff in the Java core API that was needlessly marked final requiring some nasty wrapping around a huge class to get around. Trade-offs, trade-offs.
It's highly unidiomatic to mutate bindings from libraries in any language, even ones that technically allow you to, so the observation that go is one of those languages feels like nitpicking at obscurities.
My Java is rusty, but I recall that several years ago I ran into some stuff in the Java core API that was needlessly marked final requiring some nasty wrapping around a huge class to get around. Trade-offs, trade-offs.
> I mean, at some point, garbage in garbage out, no? For example, in javascript, you can do `Array.prototype.map = null`, in C you can do `#define if while`, etc.
Yes, but a good language is supposed to minimize chances of error, especially one that was designed after C, C++, and Java. The industry progressed, and we have a better understanding of what some good ideas are, and which to avoid. The fact that golang allows null pointers is inexcusable IMO. C# was able to retrofit a nullable handling mechanism into the language, and practically any useful language that was designed after that had some sort of nullable type handling (Scala, Kotlin, Rust, and I believe Zig and Nim as well, etc.).
Yes, but a good language is supposed to minimize chances of error, especially one that was designed after C, C++, and Java. The industry progressed, and we have a better understanding of what some good ideas are, and which to avoid. The fact that golang allows null pointers is inexcusable IMO. C# was able to retrofit a nullable handling mechanism into the language, and practically any useful language that was designed after that had some sort of nullable type handling (Scala, Kotlin, Rust, and I believe Zig and Nim as well, etc.).
> The fact that golang doesn't have the equivalent of Java's `final` is just poor design.
That Java dropped C++-style `const` is also poor design.
We're just all really bad at this.
That Java dropped C++-style `const` is also poor design.
We're just all really bad at this.
C++'s const has some issues, so I can see why. Plus, it's possible to design around it by means of immutable data structures (e.g. see Scala).
I think that Rust's approach is superior to C++'s here, but even that has some issues so they have to retort to interior mutability via RefCell.
I think that Rust's approach is superior to C++'s here, but even that has some issues so they have to retort to interior mutability via RefCell.
There are times this makes sense to do (mapping an enum to a db without enum support eg mssql) but this isn’t how I’d do it.
Personally I use stringer[1] to handle all the string generation and then implement the valuer and scanner interfaces on that type. Boom transparent mapping between useful values and their corresponding db representation without having to write so much boilerplate.
[1] https://pkg.go.dev/golang.org/x/tools/cmd/stringer
Personally I use stringer[1] to handle all the string generation and then implement the valuer and scanner interfaces on that type. Boom transparent mapping between useful values and their corresponding db representation without having to write so much boilerplate.
[1] https://pkg.go.dev/golang.org/x/tools/cmd/stringer
It doesn't take much "leveraging" to do this in a language with ADTs. E.g. Typescript
type Book = {
id: number;
name: string;
genre: Genre;
};
type Genre =
| "Adventure"
| "Comic"
| "Crime"
| "Fiction"
| "Fantasy"
| "Historical"
| "Horror"
| "Magic"
| "Mystery"
| "Philosophical"
| "Political"
| "Romance"
| "Science"
| "Superhero"
| "Thriller"
| "Western";The benefit to having a type is mainly IDE experience and catching it at compile time. It's easiest to say `Genre.WESTERN` than using the string `western` and hoping you didn't accidentally type `westrn` and get a runtime error.
const someBook: Book = {
id: 1,
name: "Title",
genre: "wstern"
// Compile error: [tsserver 2322] [E] Type '"westrn"' is not assignable to type 'Genre'.
};
That's not how string literals work in Typescript. If you mis-type the string you'll get a compile error. It won't make it to runtime.Compile time checks on the strings are exactly what you get with that TypeScript definition above.
As a JavaScript developer I once abhorred TypeScript, until I realized TypeScript's type system is really powerful, it can do something like <Parameters<GetSomeFunc>> and <ReturnType<DoThisFunc>>.
Mind. Blown.
Mind. Blown.
While typescript typing is extremely powerful and expressive, I sometimes find that code written with it overuses some of the features in order to make types and interfaces align, rather than rethinking the design altogether. Especially with generics, things can get overly complex precisely because of the ability to extract return types, use partial types, omit fields, etc.
My biggest typescript gripe is the fact that interface and type are _nearly_ interchangable, and many codebases just use interface wholemeal where it isn't really appropriate.
My biggest typescript gripe is the fact that interface and type are _nearly_ interchangable, and many codebases just use interface wholemeal where it isn't really appropriate.
I tried to refactor a big code base without these special helper types, it was a massive amount of duplication, to the point I just copy paste it or just aggregate it into one folder and making it leak everywhere on my layers.
The type and interface is just because they are structural typing. It's attempting to stay faithful to JavaScript after all. Without that then the intuition breaks and not consistent.
The type and interface is just because they are structural typing. It's attempting to stay faithful to JavaScript after all. Without that then the intuition breaks and not consistent.
Yeah, when I first came across TS, I didn't see the point, mostly because I didn't see how powerful Typescript's type system is - it gets more amazing by the day, too.
I'm going to be honest, after writing some programs in Go a few years ago, I was hoping for something nicer by now. I know writing Go is good because it's boring, but all this boilerplate just feels like reading Java in 2005.
Without Algebraic Data Types, Go is just awkward. This example could be so simple typed as the sum of string literals.
[deleted]
[deleted]
Including code generation, remember Eclipse EMF?
Some people love reinventing the past.
Some people love reinventing the past.
You can also use `go generate` to generate the code that translates constants into strings.
That's kind of misleading, like there's some sort of built-in support in some sort of go command that does that already.
"go generate" is really a red herring. It's completely meaningless. It allows you to define some commands that get run when you type "go generate", but... if I define a shell script called "blah", I can get some commands to run every time I type "blah". go generate doesn't do anything useful.
"go generate" is really a red herring. It's completely meaningless. It allows you to define some commands that get run when you type "go generate", but... if I define a shell script called "blah", I can get some commands to run every time I type "blah". go generate doesn't do anything useful.
I don't really think that this article should be called "Leveraging the Go Type System", considering that the entire post was dedicated to "working around Go's lack of enums". That said, simplicity is a core part of Go's philosophy, and being a simple language isn't necessarily a bad thing, but it does make seemingly simple tasks very frustrating at times.
Honestly i miss the Java enums.
Those can have constructors implement interfaces and what not.
In Go i actually revert to the old C custom of adding a private length value at the end so i can iterate over all values.
Like so
In Go i actually revert to the old C custom of adding a private length value at the end so i can iterate over all values.
Like so
type Color int
const (
Blue Color = iota
Green
Red
limit
)
for i := Blue; i < limit; i++ { ... }
Go really needs proper enums...Semi related question: what resources do you recommend for go's state of the art regarding best practices, idiomatic project structure, tooling and the like? Preferably books, but I'll take anything.
I searched recently and most of what I saw were either in depth books about the language itself, outdated blog posts, or non-relevant books that just happened to use go to explore an unrelated topic.
I searched recently and most of what I saw were either in depth books about the language itself, outdated blog posts, or non-relevant books that just happened to use go to explore an unrelated topic.
Well, we obviously have paid courses that cover all this, but if you want my personal opinion on package layout, Ben Johnson really has a great article on it here: https://www.gobeyond.dev/packages-as-layers/
Thanks for the mention, Cory. Great article, btw!
To the parent post, there are several articles on Go Beyond[1] that walk through development of an open-source, real-world application called WTF Dial[2]. The source is available on GitHub and you can ask Go application design questions on the Discussions board[3].
[1] https://www.gobeyond.dev/
[2] https://wtfdial.com/
[3] https://github.com/benbjohnson/wtf/discussions
To the parent post, there are several articles on Go Beyond[1] that walk through development of an open-source, real-world application called WTF Dial[2]. The source is available on GitHub and you can ask Go application design questions on the Discussions board[3].
[1] https://www.gobeyond.dev/
[2] https://wtfdial.com/
[3] https://github.com/benbjohnson/wtf/discussions
This is a great argument to not use Go. This could all be implemented way more efficiently, easily, and correctly using ADTs (sum types).
"Make Genre an int instead of string to save space, but then bake the same strings into the String method of Genre anyway."
I don't understand why you would ever want to possibly save space in a database at the cost of consuming it in your binaries instead. You're almost certain to deploy more copies of your binaries than you are of your database.
I don't understand why you would ever want to possibly save space in a database at the cost of consuming it in your binaries instead. You're almost certain to deploy more copies of your binaries than you are of your database.
Are you going to deploy more copies of your binary than rows in your database? Or points in an event stream?
Plenty of problem spaces still wish to repeat the same value many times and give it a more human readable name when necessary and not coordinate a shared DB connection or even a dynamic string pool.
Plenty of problem spaces still wish to repeat the same value many times and give it a more human readable name when necessary and not coordinate a shared DB connection or even a dynamic string pool.
I have used Go in production for enterprise/business systems since 2015 and what I miss most is true sum types that really limits the state space of composite types. It’s being discussed somewhat actively for Go 2, and I would love to see more support for the idea. It’s really useful for business logic.
We have a useful lower bound for type-system richness: if you're developing a new language in the 21st century and your type system is not at least as rich as Hindley-Milner, do not expect programmers to take your language seriously.
If you want to have some fun, compare Go's type system with CLU.
Fun read. Personally I would have stored the `string` and called it a day without add to a stringer function. It's just easier, and there's no way those 14 words are going to cause a noticeable change in performance.
IMO this article really does language learners a disservice by stopping before at least mentioning stringer, which is even more critical to cleaning this up than iota (which does get a mention). It doesn't only generate most of the boilerplate code but does so in a safer and more efficient manner.
On the other hand, the efficiency concern it presents is real, and just repeating "sum types" is not, alone, enough to address it. Many languages with sum types would still store, and even more still serialize, a string.
On the other hand, the efficiency concern it presents is real, and just repeating "sum types" is not, alone, enough to address it. Many languages with sum types would still store, and even more still serialize, a string.
Frankly, this article has the opposite effect of convincing me. The author starts with a perfectly reasonable data model and munges it for some questionable space savings (that only apply in the dumbest of databases that don't do any sort of compression), for the price of a lot of boilerplate, and significantly less insight into the data if you look at it in serialized format (either directly in the database or on the wire). Maybe someone can explain to me how this is supposed to be better?
You aren't wrong. I think it depends on what your needs are. As well as what the scenarios are.
If you are serializing this data a lot, then having ints vs strings is a significant advantage.
Again, it depends on your use case. If you don't care about serialization size, then I agree, it's extra effort that you may not need.
If you are serializing this data a lot, then having ints vs strings is a significant advantage.
Again, it depends on your use case. If you don't care about serialization size, then I agree, it's extra effort that you may not need.
One of Go's strengths is that it should make it easier to swing between the optimized abstraction and the less-optimized abstraction. If you start with that data represented as strings and then find yourself backed into a performance corner and need to change the representation, Go types and type-based compile-time method selection can make it a bit easier to make that change.
Even in this era of fast computers and preposterously large storage, one sometimes hits a situation where the way to get the improvements you need is to start number-representing your strings.
Even in this era of fast computers and preposterously large storage, one sometimes hits a situation where the way to get the improvements you need is to start number-representing your strings.
That's a great point. Anyone who has spent significant time with Go knows how easy it is to do a large refactor (or even a minor one) due to it being a types language.
And yes, you may not start with ints, but you could easily add code for the marshal/unmarshal later on to convert those strings to ints for serialization purposes. And it wouldn't require a change to any of your other code, not to mention that if you stored these values in a database already you don't need to perform a migration either.
And yes, you may not start with ints, but you could easily add code for the marshal/unmarshal later on to convert those strings to ints for serialization purposes. And it wouldn't require a change to any of your other code, not to mention that if you stored these values in a database already you don't need to perform a migration either.
Go has many shortcomings that make it difficult and extremely annoying to do refactorings. Things like no constructors where types are declared as follows
A {
Field1: field1,
Field2, field2,
}
Now adding a new field to A would require ensuring that all code paths initialize Field3, otherwise you're going to have silent errors at runtime. This has been solved ages ago in Java and C# and similar languages by means of constructors.A constructor is just a function though... If I add a new field to a Java class and fail to add its initialization to the constructor, I have the exact same problem because Java initializes the field to its default value when the constructor is called.
You are correct that every situation where the struct in Go is initialized "bare" would need to be addressed if a field is added, but Go considers this a feature, not a bug (and, conversely, considers "bare initialization" of structs at dozens of places in your code to be bad practice if that struct could ever grow new fields). If you're bare-initializing structs, you're comfortable using them in a "loosey-goosey" context where zero-initialized fields are permitted (or, better, useful... https://www.youtube.com/watch?v=PAAkCSZUG1c&t=6m25s). In Go, the issue of required structure is addressed by wrapping the struct in an interface and then providing a function in the package that can create an instance of the interface. Used in that way, you get something very similar to a Java class (though Go doesn't force you into the "everything is a class" paradigm that Java demands).
You are correct that every situation where the struct in Go is initialized "bare" would need to be addressed if a field is added, but Go considers this a feature, not a bug (and, conversely, considers "bare initialization" of structs at dozens of places in your code to be bad practice if that struct could ever grow new fields). If you're bare-initializing structs, you're comfortable using them in a "loosey-goosey" context where zero-initialized fields are permitted (or, better, useful... https://www.youtube.com/watch?v=PAAkCSZUG1c&t=6m25s). In Go, the issue of required structure is addressed by wrapping the struct in an interface and then providing a function in the package that can create an instance of the interface. Used in that way, you get something very similar to a Java class (though Go doesn't force you into the "everything is a class" paradigm that Java demands).
> but Go considers this a feature, not a bug
I see this dismissive philosophy in golang a lot (I'm not saying you're doing it, mind you). The entire language is full of arbitrariness. I've seen enums being dismissed by the golang team just because, without providing any meaningful arguments. Same with how nullability was not addressed in the language, contrary to how proper modern languages have tacked the issue. The excuse? "that's how the underlying machine operates". Quite meaningless and dismissive really.
Time and time again, it's been shown that the goal is to have a simplistic (not simple) language that also makes it easy to write the compiler for. This breaks down at larger scales because reality is complicated.
I see this dismissive philosophy in golang a lot (I'm not saying you're doing it, mind you). The entire language is full of arbitrariness. I've seen enums being dismissed by the golang team just because, without providing any meaningful arguments. Same with how nullability was not addressed in the language, contrary to how proper modern languages have tacked the issue. The excuse? "that's how the underlying machine operates". Quite meaningless and dismissive really.
Time and time again, it's been shown that the goal is to have a simplistic (not simple) language that also makes it easy to write the compiler for. This breaks down at larger scales because reality is complicated.
Usually you would create a constructor without that parameter which sets the field to something backwards-compatible.
This perspective ignores the fact that there is no shortage of typed languages that don’t have all the—in 2021—inexcusable downsides of golang.
There is unfortunately a shortage, still in 2021, of languages that have a null set of inexcusable downsides.
I will trade - unhappily - ADTs for value types, automatic memory management, some language-level concurrency support, a compiler that builds our largest project in under two minutes, and a community large enough I can spend my time training new hires on fundamentals and business problems and not tool onboarding.
I will trade - unhappily - ADTs for value types, automatic memory management, some language-level concurrency support, a compiler that builds our largest project in under two minutes, and a community large enough I can spend my time training new hires on fundamentals and business problems and not tool onboarding.
Is this compared to a good language or (say) server-side JavaScript because the languages I tend to use allow me to do that and Go looks looks very Algol-68-y from that perspective as opposed to some hyper abstract wonder-language.
What languages do you tend to use?
I work for the D foundation, so mainly D for things I get to choose.
That boiler plate usually can be generated for you. See Go's stringer which does all the magic for you. All you have to do is define the type and constants, and a go generate directive.
And install stringer in every dev env, and debug all the boilerplate anyway when things go wrong.
Code generation is always the last resort.
Code generation is always the last resort.
I'm not saying it's the solution. I'm just pointing what is idiomatic go. Certainly there are arguments for and against code generation.
Where I work we are a go house, and the number of tools I've seen in our pre-commit configurations and in our development environment setup scripts is pretty intense. So is the number of questions around said tooling. There's a lot more friction than I'd expect. I can't even use lack of experience as an excuse for these questions because they're really talented people. Some of them have worked on the product for years, and have literally moulded the product's tooling into what it is today.
Where I work we are a go house, and the number of tools I've seen in our pre-commit configurations and in our development environment setup scripts is pretty intense. So is the number of questions around said tooling. There's a lot more friction than I'd expect. I can't even use lack of experience as an excuse for these questions because they're really talented people. Some of them have worked on the product for years, and have literally moulded the product's tooling into what it is today.
would you have a sample code? sounds interesting
Example from the docs:
// pill.go
package painkiller
//go:generate stringer -type=Pill
type Pill int
const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)
Then running `go generate` will yield: $ cat pill_string.go
// Code generated by "stringer -type=Pill"; DO NOT EDIT.
package painkiller
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Placebo-0]
_ = x[Aspirin-1]
_ = x[Ibuprofen-2]
_ = x[Paracetamol-3]
}
const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"
var _Pill_index = [...]uint8{0, 7, 14, 23, 34}
func (i Pill) String() string {
if i < 0 || i >= Pill(len(_Pill_index)-1) {
return "Pill(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}
Note: indentation is messing up because of Hacker News, so ignore the indentation.Code generators don't solve the problem of the language being too verbose/boiler plate. Instead of expressing a concept in a simple fashion, you have to generate 100s of lines of code that needs to be maintained across versions of golang and your data model.
I think this is mostly a toy example. In the real world the Genre would have it's own data, books can belong to multiple Genres, and you'd need the concept of SubGenres. So you'd have:
Book
Genre
BookToGenre
GenreToGenre
Book
Genre
BookToGenre
GenreToGenre
Rob Pike already made an easier way to do this, using go generate, in 2014. https://blog.golang.org/generate
The fact that you have the generate code with that approach already makes it worse.
I am once again underwhelmed by the amazing might of a type system.
I hope you mean Golang's type system. It is indeed underwhelming. However it is arguably the poorest type system in popular usage. Therr exists much better, which I encourage you to explore - Typescript, Haskell, Rust
Oberon-07 is even smaller, as Wirth decided to revisit Oberon and pursue the path of the minimalist type safe systems programming language. If any language of Oberon family would ever become mainstream I would vote for Active Oberon instead.
Very interesting, thank you for sharing. Do you recommend any resources to explore?
A couple, you can start here to see how the Active Oberon based OS looked like,
https://www.progtools.org/article.php?name=oberon§ion=co...
Some links are broken now, because ETHZ no longer hosts the A2 site, however you can get the latest language report at http://cas.inf.ethz.ch/boards/2/topics/1
And the source code and old documents for the original A2 OS at github, https://github.com/metacore/A2OS
https://www.progtools.org/article.php?name=oberon§ion=co...
Some links are broken now, because ETHZ no longer hosts the A2 site, however you can get the latest language report at http://cas.inf.ethz.ch/boards/2/topics/1
And the source code and old documents for the original A2 OS at github, https://github.com/metacore/A2OS
[deleted]
i probably would have used a map to associate those consts with strings.
might be slower but it would have been more succinct to write and easier to read.
might be slower but it would have been more succinct to write and easier to read.
So far I still haven't found a clean way to have proper enums, and this article shows us that unfortunately neither the author has :(