How writing in Rust made me a better Go developer

Nimrod Shneor
3 min readJul 6, 2020

A bit of background

I have been writing Go professionally for about two years now as part of a large corporation. Recently I have decided to pick up Rust; I started the way I usually approach picking up new languages — Writing a small project while reading the documentation (in this case — the Rust book).

My first impression of the language was how “large” and verbose it is compared to Go. It took me a month and a half to finish “The Book”. For comparison, “A tour of Go” took me about a week. Go is simply a “smaller” language with less features. This meant that picking up Rust was much harder and required much more cognitive load.

What do you mean by “more verbose”?

Lets look at the following code written in Go:

type Publisher interface {
Publish(s []string) error
}

For comparison here is the above interface written as a “trait” in Rust:

pub trait Publisher {
fn publish(&self, s &str);
}

Now lets take a look at the following implementations:

Go:

type NewYorkTimes struct{}
func (t *NewYorkTimes) Publish(s []string) {
fmt.Println("Publishing... %s", s)
}

Rust:

struct NewYorkTimes {}impl Publisher for NewYorkTimes {
fn publish(&self, s &str) {
println!("Publishing... {}", s);
}
}

Getting over the hurdle

After about a month of reading regularly about Rust and writing on and off, I found my self conforming to the language; the compilers strict requirements meant I had to think of each allocation and reference I made — “Who was owning that data?”, “How long does this reference remain valid?”. These types of questions didn’t occur to me coming from Go. Then I looked at old code I had written; I found my self asking the same questions: “Is this reference going to be valid by the end of this method?”, “Can this shared mutable reference lead to unexpected behavior?” etc.

Lets talk code

Here is a real world example I found in my code base:

type CidrValidator interface {
func ValidateNetworkCIDRs(cidr []string)
}
type AWSCidrValidator struct{}func (v *AWSCidrValidator) ValidateNetworkCIDRs(cidr []string) {
// Do something here ..
}
type GCPCidrValidator struct{}func (v *GCPCidrValidator) ValidateNetworkCIDRs(cidr []string) {
// Do something here ..
}
...func validateNetworkCIDRs(network models.Network, isMultiAZ bool, cloudProviderID string) error {
validator, err := getCIDRValidator(cloudProviderID)
if err != nil {
return err
}
return validator.ValidateNetworkCIDRs(network, isMultiAZ)
}

func getCIDRValidator(cloudProviderID string) (validators.CidrValidator, error) {
switch cloudProviderID {
case cloudprovider.AwsCloudProviderID:
return &validators.AWSCidrValidator{}, nil
case cloudprovider.GcpCloudProviderID:
return &validators.GCPCidrValidator{}, nil
default:
return nil, errors.Errorf("unsupported cloud provider '%s'", cloudProviderID)
}
}

These functions (originally methods) validate a REST request to one of our HTTP services containing some CIDR address. Lets try and write the getCIDRValidator function in Rust:

enum CidrValidator {
GCP
AWS
}
impl CidrValidator {
fn validate(&self) {
// Do some validation here
}
}
fn get_cidr_validator(cloud_provider_id: &str) -> Result<CidrValidator, Error> {
match cloud_provider_id {
"aws" => Ok(CidrValidator::AWS),
"gcp" => Ok(CidrValidator::GCP),
_ => Err(Error::UnexpectedCloudProvider),
}
}

Two things are strikingly different:

  1. The use of Enums and the match statement makes for a very slick and verbose code.
  2. No new allocations are done on the heap for each call! This is extremely important, specifically on a garbage collected language like Go.

Granted there are ways to get around these redundant allocations:

  1. The actual validation code is stateless thus using the validation logic as a method in a FooValidator is redundant — one can simply make the validation code pure function. Thus we can alleviate the redundant allocations.
  2. Allocate the {GCP,AWS}Validator as fields of the parent struct. As mentioned before the above functions are originally methods of some “parent” struct. This has the limitation of using a stateless struct to perform some “validation” computation — which is, again, redundant. But alleviates the heap-allocations-per-request.

Summary

My aim writing this article was to show how writing code in one language can shed light on code written in another language and enrich everyone, both camps of the battlefield.

Hope you guys enjoyed this article!

--

--

Nimrod Shneor

Software Engineer; Rock Climber; Hiker; Runner; Father; Partner.