Hacker News new | past | comments | ask | show | jobs | submit login
From Python to Go to Rust: an opinionated journey (2018) (allo-media.net)
114 points by snth on June 24, 2019 | hide | past | favorite | 70 comments



As a Python dev getting into more Rust development, I'm surprised whenever I hear of this as a progression towards better tools, rather than a way of adding new tools into my belt.

Django/Flask are great tools for spinning up a CRUD application with authentication/authorization, API endpoints, and a template language if I don't want to build out a React or Vue SPA.

Rust is a great language that's opened me to new domains of programming, and also gives me the ability to write more performant, hardened API endpoints at the expense of productivity in hours (this gap will definitely shrink as I get better and as rust frameworks mature, but I'd be surprised if it went away entirely)

If a developer views a suite of tools they know very well as "something they used to use", they're more likely to make future architectural decisions based on incorrectly viewing framework-powered python as a relic of the past, rather than a viable (and often preferred, if hours of work are factored in) alternative.


I get the fascination people have with Rust and I love to use Rust myself. Part of the fascination certainly is it’s tooling and that Rust nearly seems to force you to make the right decisions. I’ve never wrote in a language were I were so focused on the structure of my program rather than hammering in what I felt. And when you hit compile and it works, it usually does so flawlessly.

However I use Python very often still, just not for every task. Just like you said: Rust is another tool in the tool box and it isn’t that single purpose machine that you buy and use once for a single project, it is that japanese handsaw that is incredible useful and powerful in the hands of the right person. But sometimes you just need to make many cuts fast and then the handsaw (no pun intended) doesn’t cut it.

Python certainly has it’s place for me, but so does have Rust..


> I compiled the code and … no error message. Everything went fine. But?! I just added a field to a struct, the compiler should say that my code is not good anymore because I’m not initializing the value where it should be!

This is your big hangup with Go? You added a new field, didn’t use that field anywhere, and the compiler didn’t complain? I’ve heard a lot of valid criticisms of the language but this is a new one for me.


Go has two mechanisms for initializing structs, name based and position based.

    type Sample struct {
        A int
        B int
    }
can be initialized as Sample{A: 1, B: 2}, or Sample{1, 2}.

The name-based method allows arbitrary elision of fields, which will be initialized to their zero value. This includes Sample{}, which will be interpreted as a name-based initialization that specified no fields.

The position-based method requires all fields to be specified, in order, with the correct type, or it's a compile-time fail.

So if the author of the article could have gotten the latter behavior with a slight syntax tweak.

This would not necessarily entirely satisfy, though, since you can't do any mix-and-match, and in particular, it can be nice to have names on the larger structs even if you want them to be fully initialized, but there is no in-between. But, nevertheless, if you are willing to pay for the behavior that the compiler will complain on any type-changes to the struct, including growing or shrinking, Go has that.

It seems to be the Go community's "best practice", above my objections, to claim that all struct initializations MUST use the name-based initialization and that positional-based inits is a mistake, on the theory that if structs change and add fields it's important for all uses of structs to continue compiling without changes. If the author's introduction to Go came from a tutorial from someone who believed that, they could easily have picked that up mistakenly as a characteristic of the language itself.

My objection is that there's a time and a place for each behavior, and there have been plenty of times I've been grateful for the compiler pointing out every place I need to change my struct to include a new member because I could tell it was a "tuple-like" struct that should always be initialized with all the fields. You can argue with the putative "best practice" or agree with it, but fortunately it doesn't really matter because you can do the one you want regardless of what the community thinks. There is no chance either method is ever going to be removed.


The problem with using the positional method is that

1) It's much more difficult to read since you have to know the position of elements of the struct

2) You swap one compile time error for another: in the case you add and remove a field of the same type, it's entirely possible the program will compile fine if you use the positional initialization instead of the named one.

#2 makes me think that the author would still be unsatisfied


> It's much more difficult to read since you have to know the position of elements of the struct

At least the Go editor I use (GoLand by JetBrains, one of the more popular ones) will display the names of all the fields in positional initializers inline with the code (just as they would be in a named initializer), so it's pretty much the same readability-wise. There's a special color for these automatic labels that indicates they aren't actually part of the document.

> in the case you add and remove a field of the same type, it's entirely possible the program will compile fine

True although I think it's easier to work around this (by fixing the compile errors from removing the old field before adding the new one).


Initializing through explicit constructors e.g. `func NewSample(a, b int) Sample {...}` can also add a compile-time check. Of course the onus is once again on the developer to remember to use the `New` funcs instead of instantiating directly.

Edit: spelling


I agree, I use positional initializers pretty often to get behavior you describe. Not sure about VSCode but if you use GoLand it will even prefix the field names right in the editor, so there isn't really any readability difference.

One other reasonable alternative for OP in Go might be to simply use initialization functions (NewSample() or sample.InitWithX()) for certain structs. It's a bit more flexible with the ability to have multiple initializers and non-zero default field values (and of course also solves the issue of generating compile errors in the rest of the code when new fields/parameters are added).


I wouldn't have left Go just because of these but before you write this off as minor thing, I would suggest you look this as one of the core language design decision. It reveals two important language philosophies:

1. Go doesn't follow "you pay for what you use" model.

2. Go isn't too deeply invested in compilation as means for catching errors.

Above are not binary language decisions. If you go in one direction strongly then there are consequences in other. Therefore all languages chose some balance point (aka compromises). Go has chosen a balance point that is weaker than Rust (and may be even C++) but stronger than Python.


> 2. Go isn't too deeply invested in compilation as means for catching errors.

Except when you've imported a package you're not currently using, that is a mortal sin :)


I believe Go is weaker than Python. Python dataclasses/attrs (effectively structs) raise an error instead of silently doing the wrong thing, if you omit a constructor keyword parameter. You can still supply default values, which makes the corresponding constructor parameters optional.

Keep in mind that dataclass defaults suffer from the same "mutable default arguments" issue as function parameters.


> Go doesn't follow "you pay for what you use" model.

To be fair, isn't that implied since Go uses a GC? The "pay for what you use" model is only really viable for languages that don't rely on a fixed GC runtime. (You could have "pluggable" GC libraries like the Boehm collector for C/C++, but those imply very different tradeoffs.)


Go doesn't follow "you pay for what you use" model.

It's not "you" in the traditional formulation. It's "CPU." You, the programmers, often have to pay continuously for "you pay for what you use."


You're focusing on the wrong detail. The lack of compiler message isn't the actual problem, it's just a symptom of the fact that in Go, everything has an implicit zero value, and this leads to bugs.

Hell, just the other day I complained to my backend team that the PubSub event was passing null for a non-optional array, and it was traced to the Go code declaring a map variable without ever initializing it. The compiler never caught that, it wasn't until it hit my Swift code that it became apparent.


> I’ve heard a lot of valid criticisms of the language but this is a new one for me.

We've had bugs in prod because of this "feature". It's another bad design decision in golang.


Actually is pretty good. The world have a lot of bugs in prod because C doesn't zero newly allocated memory (heap or stack).

There are APIs with autogenerated structs that span a lot of fields (even thousands)... Imagine having to initialize each field when you just want... the default zero value...


There are more options in the design space than those two extremes. Rust does not do either of these two things, for example. It will not let you use something you haven’t initialized, but it also doesn’t force you to declare a default value for each type. If a default value makes sense, you can make one, but you still have to explicitly ask for a default, rather than it implicitly happening.

I think each of these three languages have made the correct choice, given their design ideals.


I appreciate your work on Rust, I find it to be a very interesting language.

> I think each of these three languages have made the correct choice, given their design ideals.

I feel that's kind of side-stepping the issue though. The approach that golang took could match their design goals, but it doesn't mean that those goals aren't a bad idea in the first place.


For any set of correctness-improving primitives in a language, you can practically always come up with some disjoint set of additional primitives, and then make the argument that the lack of those additional primitives constitute "bad design". Sometimes you'll even be right (it's not clear to me in this case). But it will almost always be a boring argument.


In this case though, it's the addition of a "feature" that causes bugs, so lack of said feature will improve correctness.


Zeroing out uninitialized memory is a feature, one which significantly improves correctness over C/C++. Meanwhile, the scenario described in this blog post is shared by virtually every mainstream implementation language other than Rust.


I don't see it in Java or C# for instance. You have to explicitly opt into it by (1) declaring default (parameter-less constructors), and (2) exposing fields you want mutated via setters.

On the other hand, golang does not have the ability to prevent "default constructors", and the fact that it has no ability to make types file private further makes this an issue.


Java initializes variables to their zero values. If you want to hide Go struct members from other "files", you can do that trivially by putting them in subdirectories.


> Java initializes variables to their zero values

I know, but that's not what I'm referring to here. I'm referring to someone writing:

    type A struct {
        A int
        B int
    }

    a := A {A: 1, B: 2}

then modifying `A` by adding another field `C int`

The code continues to compile, with `C` being initialized to 0, which can be incorrect.

In Java or C#, you'd add a third value to the constructor, resulting in a compile time error for all constructor invocations.

As I said, I've seen this issue come up in production resulting in bugs.


I don’t doubt it causes bugs. I wish Go had option types, as well. I’m just saying that it’s not unusual behavior, and you’re going to single languages out for the bugs they enable, you’ve a long future of boring arguments ahead of you.


> Meanwhile, the scenario described in this blog post is shared by virtually every mainstream implementation language other than Rust.

I don't believe that TypeScript, which is mainstream, has zero values if strict mode is enabled. (I could be wrong though; plugging all the "undefined" leaks is hard in JS.)


It's a pretty big annoyance with Go though. I hate that all struct fields are optional.


I think there is a wider issue. Go simply does not provide a way to restrict the state of public struct fields.

Enforcing the initialization of all fields achieves relatively little given that public fields could easily be modified elsewhere. Requiring initialization to anything more specific than an automatic zero value is only useful in languages that have read-only fields.

So the only way to handle this in Go is to make struct fields private or accept that all combinations of public field values are valid (including the zero value) and deal with it at the point of use (or rather at all points of use).


there are times when a nil struct field is useful, like for trees and json. very easy to require struct fields by using a constructor


Absolutely, and Option<T>s are great for modelling that. It just shouldn't be the default.


If it's useful, then the person creating the struct can explicitly set it to nil.


FWIW, I never had that problem, but I also tend to use constructors for my structs.


When you use constructors you lose named arguments though.

One use-case where this is really painful: I like to encode sum types in Go using an interface with a private dummy method + structs that implement that. I then have a function "match" that takes one function per struct. It's nice to be able to pass these functions as a record since it gets ugly, but due to Go allowing omitted functions to be nil, if I add a case to the sum, I open myself up to NPEs everywhere. If Go had a warning for missing struct fields, it would tell me all the places I'd need to handle a new case.


Isn't this something you could catch with a linter, if you wanted to?


Definitely but I'm not sure if there's a linter for this. If so, I'd love to use it.


Yes, this would be pretty easy to solve with an initialization function and using that everywhere. Change the arguments in the function prototype and the compiler will definitely complain!


I was confused too. At the end they say they'd rather do python than Go. And yet it was going so well until this issue which is not any better in Python.


The criticism is that a struct literal allows you to not specify some (or all!) of the field. The argument is that this is brittle since, as with what was described in the post, if you add a new field, you might forget to use the field in all instances of the struct being constructed.


I've yet to try Rust. But Go is the best and worst thing to happen in my career. Best because it's amazing, worst because now I don't like working in other languages.

I'm trying elixir currently, and it makes me want to rip my hair out. No for loops, no typed parameters, implied returns. Who in the world though any of this was a good idea? IO.puts() refuses to print certain objects (list of ints) and just prints a newline, making it useless for debugging.

I'm very very excited to try Rust, and I'm glad I've been able to choose the languages I work with up to this point in my career.


Just wanted to correct some things, as a fan of both Elixir and Rust.

> No for loops

But we do have for loops? That's what comprehensions are: https://elixir-lang.org/getting-started/comprehensions.html

> no typed parameters

But if you need types on your parameters, you have Guards, which... check for type? Sure, you don't type every last parameter, but if you want that you can upgrade to Dializer and get that feature too. It's just not as useful as you likely treat it.

> implied returns

I understand that some may not like this, but it comes out of the idea that generally, you want to avoid "side effects"- ie, things in code that happen without anyone but the original function causing them being able to tell. Elixir doesn't completely lack them, as that would be pretty unreasonable, but it does at least ask that you're clear about the purpose of functions by having them always return something.

> IO.puts() refuses to print certain objects (list of ints) and just prints a newline, making it useless for debugging.

This seems like something must be going wrong. `IO.puts` certainly prints lists of ints, however, it might also be mistaking them for a charlist, which is definitely a concern. That's why generally, you want to use `puts` for text, and `IO.inspect` for data structures. As an added convenience, if you want to annotate your data structure with text, you can modify `IO.puts` like so: `IO.puts "List: #{inspect(list_of_numbers)}"`. That will parse the list to the inspect version and insert it into your `puts` line.

For the record, I'm not saying any of these necessarily shouldn't be a reason to not like Elixir- we all have our preferences, but the reason behind many of these choices is philosophical, and I'd be happy to explain anything else that has puzzled you, if you're still interested in learning more about Elixir.


This is really interesting to read. I've been writing Go for about ~4 years professionally, and recently (<6 months) jumped into Elixir (and a bit of Rust). They've been nothing but a breath of fresh air, and I can't wait to write more as I'm starting my new thing. I'm definitely more excited about Elixir as it's really, really pleasant to write and in some sense, a major increase in productivity.

Agree about typed parameters + implied returns, but you can get some of that back using guards + dialixer/dialyzer.


I also write Go for a living, and feel sort of the same way about other languages. But then I have some good news for you with Rust, you'd like it (at least I did/do). It's different from Go, for sure, and in my head fulfils different purposes from Go, but it's nice to have a choice between two things I like.

Fair warning, though, before Go, I wrote Delphi for a living.


I went from python to go to nim. Actually I still like go - but nim is an easy transition from python.

I think nim is the best thing out there for building tiny CLI utilities. It’s a fun language to code in.


Nim is very nicel Not that it matters much in most cases, but the compiled binary sizes are also quite low compared to Rust.


Do you ever regret choosing "coffeescripter" as your handle? I loved CS and contributed to the CoffeeScript Redux compiler but the language is definitely pretty hated these days...


I wonder if there's any performance difference between Cython and Nim.


Nim is generally considered to be faster. Being statically typed allows more optimization. Nim is usually quite competitive with even pure C, especially if the C is non-trivial (e.g. has to do actual memory management)


That's a bizarre reason to stop using Go. I can't really think of many realistic instances where that would bite you.


I was working on a simple Rust CLI program to parse JSON from the Github API, but I got turned off by some of the unfriendliness of the available http libs. One http library pulled in dozens of other crates and turned my 10 line program into a 50MB binary [0], while another had an unpleasant api [1].

[0] https://docs.rs/reqwest/0.9.18/reqwest/

[1] https://docs.rs/hyper/0.12.29/hyper/client/index.html

Maybe I have too much desire for instant gratification, but it sucked the fun out of the project.


"Crates" are just compilation units. Do you feel "turned off" if a C/C++ library includes "dozens" of source code files, some of which might come from outside projects? (A sibling comment mentioned --release already, which is obviously something that one should be familiar with when discussing Rust's performance.)


Yes, unfortunately reqwest pulls in lots of dependencies, but I think you should be able to get a binary under 10MB if you compile in release mode using the --release flag.


The “zero value of a type should be useful” concept is pretty fundamental to Go’s design. You can imagine compiler tooling that didn’t alter the language allowing you to catch unintended uninitialized values, but a language that changed the underlying default would no longer be Go.


rust is great if you have enough experience to pick it up without dying trying. and you need to start reading the book. the enforced safety helps even experienced devs with a formal background to reinforce and see some concepts more clearly.

all that said, when you have to do advanced things and go unsafe, it can feel really annoying (fighting the compiler) and limiting. most common problems can be modeled in rust without issue, but if you have a clear idea of something that you want to do that's more advanced... the language will actively work against you, that's its goal. that has both a good and a bad side. maybe it will get better in the future. still an awesome language today.


I went through the same path as OP. But instead of discarding them I am still using all three of them. And at least for me Python and Rust don't even compete with each other.


Biggest issue for me is the compile time. Especially in comparison to Go it's a huge difference. At work we have a huge codebase with over 1M LOC. I guess for bigger projects you need proper interfaces so you can split up your code into multiple libraries.


I would suggest F# to the op. Even though I have limited practical experience with it, from what I've read and see it checks op's checkboxes.


> This was the show stopper for me. I realized that I couldn’t rely on the compiler to get my back when I was doing mistakes.

No! One shouldn't mindlessly rely on the compiler "to get your back." Instead, one should code (or alter code) so that you've manipulated the situation, such that the compiler will flag any errors. With a language like golang, this means that you should make sure that the "zero values" are always copacetic. (Analogous to crash-only software.)


[flagged]


> > I realized that I couldn’t rely on the compiler to get my back when I was doing mistakes

> That's not how engineers work.

Engineers don't leverage automated error checking whenever possible? I think you're confusing engineers with cowboys.

Yes you should have a good idea of what's going on, and strive to make minimal mistakes, and take responsibility for continual improvement. That does not take away from the fact that you should use tools that guide you to the right answer, that catch your mistakes early, and that fail safely.

Safety is not a set of training wheels that a good engineer drops when they're no longer needed.


> > I realized that I couldn’t rely on the compiler to get my back when I was doing mistakes

> That's not how engineers work.

Maybe "hackers" work that way, but actual "engineers" seek and use the best tools available. Finding & fixing bugs after-the-fact is usually more expensive than writing things correctly the first time, so serious engineering organizations look for ways to eliminate problems as early and as automatically as possible.

Linters, unit-tests, and type systems are all valid approaches. By looking for a language that can help with this goal, the original author is demonstrating a true engineer's mindset.


Maybe you should try the dev experience of Elm or ReasonML before shooting down the author’s comments.

I get what you are saying and I also get what the author is saying it might be that you are missing something truly great in our engineering field.


If the author had explained a real reason for abandoning Python in favor of Go, it would've made a lot more sense to write this article. "Not feeling it right" isn't how engineers choose their tools. He could've said that he wanted to try something other than Python, and I would have respected that.

On top of it, his reason to switch from Go to Rust is laughable at best.


> If the author had explained a real reason for abandoning Python in favor of Go, it would've made a lot more sense to write this article. "Not feeling it right" isn't how engineers choose their tools. He could've said that he wanted to try something other than Python, and I would have respected that.

The sentence immediately following the phrase "it didn’t feel right anymore":

> Well, to be honest, it’s not really at “some totally random point” that it started not to feel right anymore, it was when I started to enjoy programming with a strongly typed language: Elm.


I also can’t believe no one mentioned one of the things Go does best... Concurrency. When it comes to its concurrency and parallelism model. Go is just awesome.


Making concurrency/parallelism easy is not necessarily a good thing. You need guard rails in place to reduce the risk of bugs.

Node.js does that by only having concurrency, not parallelism. Rust does that by having very robust memory safety checking.


[flagged]


I took a look at your comment history and it seems clear that the downvoted comments were the ones that broke the site guidelines, while your thoughtful and substantive comments have positive scores.

It's true that a fine comment sometimes gets an errant downvote (if nothing else, remember that people misclick). But most of those get corrected by other users who will notice the unfairness and give a corrective upvote. They won't do that, though, if you get upset by downvotes and lash out like this—because then you've broken the guidelines, and that will result in downvotes and flags.

https://news.ycombinator.com/newsguidelines.html


Would you still use rust for your CRUD apps? I think of it as a specialized, safe, performant system language. I wouldn’t think of turning to it for the random rest services we constantly churn out.


I would. I feel more performant (as a dev I mean, I'm not referring to code) with Rust after a year than with Go after 6 years. Which to me indicates that at least for myself, Rust is a language that allows me to get things done more quickly. Iterators especially.

With that said, I definitely wouldn't throw Rust at the Python/Go devs in my shop expecting them to be better/faster at it. It can have significant spin up time and, most importantly, if you're used to doing things unsafely by using bad patterns Rust will be hell for you.

I commented recently that I think Rust can be far more simple than people give it credit for. Namely, that you can write Rust in the same manner as Go, pretending Rust was Go. However going far beyond that too quickly is a world of hurt, from personal experience at least.


I don't write REST services, but I use rust on AWS Lambda, and it works great. I can interact with AWS resources easily through Rusoto, I use Dynamo and S3, etc.


I enjoy coding in Rust as well, I don't understand why some people try a forced comparison with C++ arguing that Rust is younger in contrast with C++. Sure, but Rust is very promise language and it's not restricted only at system programming scope. It goes a bit more further. In few years I see Rust taking a place on top and sharing it that place with C++.


python




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: