Rust/Go notes
-
Rustacean and Gopher
- quirky mascot, also Deno?
-
Rust has borrow, Go has GC
- this is like comparing apples to oranges
- I won't talk about it here
-
similar things
- type inference
- no auto/implicit conversion
- no parentheses on condition expression (
if
,for
)- but braces are required for the code block
- implicit auto (de)reference when calling methods
- calling methods defined on ref on base type, and vice versa
- no C++ style
->
- doesn't work for regular parameters
p
for pointer in format string, Rust:"{:p}"
, Go:"%p"
- they work for slice
-
variable initialization/default
- Go:
- every type has a zero value
0
for numeric types,nil
for pointer types, etc- user defined types have automatic zero values
- variables defaults to zero
- every type has a zero value
- Rust:
- there is a similar thing called std::default
- user defined types could
#[derive(Default)]
or implement their own default behavior
- user defined types could
- it's not called when variable declaration doesn't include initialization
- compiler error if used before initialization
- there is a similar thing called std::default
- Go:
-
slice
- similar
- fat pointer
- functions/methods are mostly (and encouraged to be) implemented against slice instead of array/Vec
- low/high bound could be omitted in slicing syntax:
[..]
or[:]
- Rust: slice is borrow/reference by definition
- it's always
&[T]
, never[T]
- there's always another variable acting as owner
slice.join
returnsVec
, which ironically is not a primitive type
- it's always
- Go slice = Rust Vec + Rust slice
- you can create slice by make:
s := make([]int, 10)
- then the slice itself is the sole owner of that data
- you can expand slices in place
- example: https://go.dev/play/p/fjMJprLkOgg
- like a Rust Vec where capacity > len
- interestingly only beyond the end, not start
- you can create slice by make:
- Go slice can be nil
- for Rust, slice (like other references) has no nil/null value
- default value does not point to 0: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=395bcfecb113757ed140bccd1401c093
- a rough equivalent would be
Option<&[T]>
- for Rust, slice (like other references) has no nil/null value
- Go slice can only be compared(
==
) to (literal) nil- BTW, it's a bit different than "slice can only equal to nil", which is not true
- when two slices are compared it's compiler error, not true or false
- Rust does deep equal instead
- go has
reflect.DeepEqual
- BTW, it's a bit different than "slice can only equal to nil", which is not true
- similar
-
constants
- Rust: must be type annotated, even when type inference obviously would have worked
// error: missing type for `const` item const C = 0isize; // must be annotated const C: isize = 0;
- Go: constants are bizarre
- they can be implicitly converted
// this is OK const n = 0 var a int = n var b uint = n // not OK since implicit type conversions are not allowed a = b
- really a different beast
// arbitrary precision const n = 1 << 100 var a float64 = n // won't compile since it overflows int64 var b int64 = n
- they can be implicitly converted
- Rust: must be type annotated, even when type inference obviously would have worked
-
Go doesn't have tuple
- but functions can return multiple values
- which (I believe) in most other languages, is implemented as tuple
- and multiple assignments, like
a, b = x, y
- again usually implemented as destructuring assignment from tuple
- unit type is
struct{}
- rust uses
()
, a 0-tuple
- rust uses
- but functions can return multiple values
-
Go claims to be statically-typed but it's practically dynamically-typed when interface is involved:
- example: https://go.dev/tour/methods/16
-
type inference quirks
- Rust: array length won't work
- this works:
let a = [0, 1, 2];
- this also works:
let a: [_; 3] = [0, 1, 2];
- this doesn't:
let a: [usize; _] = [0, 1, 2];
- while it's not the same thing, when you do similar thing in Go:
- ordinary array literal
a := [3]int{0, 1, 2}
- if length is omitted,
a
becomes a slice insteada := []int{0, 1, 2}
- there is no such use of
_
in Go, this doesn't compilea := [_]int{0, 1, 2}
- ordinary array literal
- this works:
- Rust: array length won't work
-
flow control
- loop constructs
- Rust has
for in
,while
andloop
- doesn't have C-style 3 clause
for
- doesn't have C-style 3 clause
- Go has
for
only but multiple flavors- (C-style 3 clause)
for ; ;
for k, v := range ... {}
(likefor in
)for condition {}
(likewhile
)for {}
(likeloop
)
- (C-style 3 clause)
- Rust has
- Go switch case, Rust match
- Go doesn't fallthrough on default
- but a
fallthrough
keyword could be used when needed
- but a
- Rust doesn't fallthrough
- Rust match must be exhaustive
- Go doesn't fallthrough on default
- loop constructs
-
Rust function:
- return at end can be omitted
- return value can be specified with an expression without
;
- return value can be specified with an expression without
- return at end can be omitted
-
Go function:
- return can only be omitted when there is no return value
- return value can be named
- even then, return at end can not be omitted
-
Go didn't have generics
- but some builtin types are like generics, example: array, slice, map, channel
- Go have some magical builtin functions for them
- like make, append, delete, len, cap
- with type checks and all
- definitely looks like generics
- users can't implement things like that
- Go have some magical builtin functions for them
- an unpleasant example:
sort.Slice
fromstd
package main import "sort" func main() { sort.Slice(0, func(_, _ int) bool { return false }) }
- this thing compiles, horrifyingly
- it panics if you run it though
- notice that it can't take
[]any
?
- but some builtin types are like generics, example: array, slice, map, channel
-
function overloading
- Rust: no function overloading
- method overloading is possible
by implementing methods for different traits with the same name
- like
try_into
andtry_from
- they can even have the same signature which isn't possible in C++ function overloading
- like
- method overloading is possible
by implementing methods for different traits with the same name
- Go: strictly speaking, no
- but it's basically dynamic so
- Rust: no function overloading
-
Go doesn't have operator overloading
-
Go doesn't have uint128
- and since there is no operator overloading, we can't implement our own https://cs.opensource.google/go/go/+/master:src/net/netip/uint128.go
- interestingly it has complex(64|128) (as a primitive type)
-
Rust trait bound vs Go interface satisfaction
- trait must be implemented
- also makes aforementioned method overloading possible
- interfaces are just declarations
- structural typing
- difference from duck-typing?
- structural typing
- function overloading and duck-typing are incompatible with each other?
- trait must be implemented
-
external type
- Rust: you can't implement external traits on external types
- type alias of external type are still considered external
- there is a workaround by wrapping it in a singleton tuple struct, like:
struct Foo(isize);
- they even have a name for it, called newtype: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
- Go: you can only define methods on your own type
- however type aliases are considered your own type
- Go is structural typed so implementing external interface on your own type isn't really a problem
- Rust: you can't implement external traits on external types
-
Go: interface can only have regular methods (as requirements)
- not "static" methods, like new/from
- Go usually have them as (package level) regular functions instead
- this is bad news for generics
- as a workaround from can be implemented on pointer type
- only the pointer type satisfy the trait then
- not "static" methods, like new/from
-
Go: can't define methods for interfaces
- i.e. receiver can't be a interface type
- very odd considering that defining functions which takes parameters of interface types is allowed
- in Rust (and some other languages), receiver is just the first parameter
-
Go 1.18 introduced generics, as type parameters
- https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
- https://tip.golang.org/doc/go1.18#generics
- using interface as constraint, like Rust trait
- introduced union constraint element in interfaces
- they can only be used as constraints
- introduced union constraint element in interfaces
- generic types can have methods
- however methods can't take type parameters
- i.e. methods can't be more generic than it's receiving type
- and this is by design
- x/exp/slices and x/exp/maps
- ironic?
- known limitations as noted in the release note
- most importantly structural typing on union constraints are not working
- golang/go#52367
- barely works for the example it used
- basically renders union constraints useless
- most importantly structural typing on union constraints are not working
- only two predefined constrains as of now: any, comparable
- some more are defined in the x/exp/constraints package
- they're defined as union constraint
- you can't make your own type satisfy them
- they're defined as union constraint
- some more are defined in the x/exp/constraints package
-
(Go) remember we can't define methods for interfaces?
- but generic types can?
- and generic types are constrained by interfaces?
- wrapping interface in a generic type can be a workaround:
a lot like Rust newtype
type Foo interface{} type Wrapper[T Foo] [1]T // we can then define methods on that wrapper func (Wrapper[T]) foo(){}
- then I thought:
but this doesn't work:
type Wrapper[U Foo] U
I feel attacked: cannot use a type parameter as RHS in type declaration
-
visibility
- Go use naming
- Rust trait methods can't be private
- there're some strange workarounds
-
closure, nested function, anonymous function
- clarify terms, since they overlap a lot
- nested
fn foo() { fn bar(x: isize) { x + 1 } }
- anonymous function
let foo = func(x int) { return x + 1 };
- shorthand anonymous function
let foo = |x| x + 1;
let var = x => x + 1
- closure
- nested
- Rust
- nested: yes, but not closure
- anonymous: no
- shorthand(closure): yes, and closure
- Go
- nested: no
- anonymous: yes, and closure
- shorthand: no
- JS
- nested: yes and closure
- anonymous: yes and closure
- shorthand(arrow): since ES6 and closure
- both rust and go closure can't be recursive
- probably due to the nature of anonymous function and order of evaluation
- there're some workarounds
- as comparison JS doesn't have this limitation
- clarify terms, since they overlap a lot