Skip to main content

Fun comparing V with Go and Rust

I thought of doing a fun exercise of walking down the V documentation and comparing its features with the equivalent in Go and Rust. I will be comparing the languages syntactically and where circumstances permit, I may also delve a bit into the semantics around features. Ok, let's take it from the top

Creating a 'Hello World' project#

V

#initialize projectmkdir hellocd hellov init
#run programv run hello.v
#or
#create projectmkdir learn-vcd learn-vtouch hello.v
#add contentfn main() {  println('hello world')}
#run programv run hello.v

Rust

#initialize projectcargo new learn-rust --bin
#or
mkdir learn-rustcd learn-rustcargo init --bin
#run programcargo run
#or
#create projectmkdir learn-rustcd learn-rusttouch hello.rs
#add contentfn main() {    println!("Hello World!");}
#build apprustc hello.rs
#run program./hello

Go

#initialize projectmkdir learn-gocd learn-go
#create a modulego mod init github.com/[github-username]/learn-gomkdir -p learn/basicstouch learn/basics/hello.go
#add contentpackage main
import "fmt"
func main() {    fmt.Println("Hello, world.")}
#run programgo run learn\basics\hello.go

Comments in source files#

They all support single line and multi-line comments in the same way

Single line comment

// This is a single line comment.

Multi-line comment

/*This is a multiline comment.   /* It can be nested. */*/

However, Rust has an additional dimension when it comes to comments, which pertains to generating documentation

/// Generate library docs for the following item.//! Generate library docs for the enclosing item.

Formatted print#

V

print(s string) // print anything on sdtoutprintln(s string) // print anything and a newline on sdtout
eprint(s string) // same as print(), but use stderreprintln(s string) // same as println(), but use stderr
exit(code int) // terminate the program with a custom error codepanic(s string) // print a message and backtraces on stderr, and terminate the program with error code 1print_backtrace() // print backtraces on stderr

Rust

  • Printing is handled by a series of macros defined in std::fmt some of which include:
format!: write formatted text to Stringprint!: same as format! but the text is printed to the console (io::stdout).println!: same as print! but a newline is appended.eprint!: same as format! but the text is printed to the standard error (io::stderr).eprintln!: same as eprint!but a newline is appended.

Go

fmt.Println(args)

Primitive Types#

V

  • Please note that unlike C and Go, int is always a 32 bit integer.
bool
string
i8    i16  int  i64      i128byte  u16  u32  u64      u128
rune // represents a Unicode code point
f32 f64
voidptr, size_t // these are mostly used for C interoperability
any // similar to C's void* and Go's interface{}

Rust

signed integers: i8, i16, i32, i64, i128 and isize (pointer size)
unsigned integers: u8, u16, u32, u64, u128 and usize (pointer size)
floating point: f32, f64
char Unicode scalar values like 'a', 'α' and '∞' (4 bytes each)
bool either true or false
and the unit type (), whose only possible value is an empty tuple: ()

Go

string  string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable.
numeric  uint8 the set of all unsigned 8-bit integers (0 to 255)  uint16 the set of all unsigned 16-bit integers (0 to 65535)  uint32 the set of all unsigned 32-bit integers (0 to 4294967295)  uint64 the set of all unsigned 64-bit integers (0 to 18446744073709551615)  int8 the set of all signed 8-bit integers (-128 to 127)  int16 the set of all signed 16-bit integers (-32768 to 32767)  int32 the set of all signed 32-bit integers (-2147483648 to 2147483647)  int64 the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
  float32 the set of all IEEE-754 32-bit floating-point numbers  float64 the set of all IEEE-754 64-bit floating-point numbers
  complex64 the set of all complex numbers with float32 real and imaginary parts  complex128 the set of all complex numbers with float64 real and imaginary parts
  byte alias for uint8 rune alias for int32bool  true, false
error  an interface type, which wrappers around the string type

Custom Types#

V

Arrays  collections of data elements of the same type. They can be represented by a list of elements surrounded by brackets. The elements can be accessed by appending an index (starting with 0) in brackets to the array variable
Maps  association of key-value pairs. Maps can have keys of type string, rune, integer, float or voidptr.
Struct  grouping of data elements of mixed types. Structs are allocated on the stack. To allocate a struct on the heap and get a reference to it, use the & prefix:
Channel   Channels are the preferred way to communicate between coroutines. V's channels work basically like those in Go. You can push objects into a channel on one end and pop objects from the other end. Channels can be buffered or unbuffered and it is possible to select from multiple channels.
Function  named or anonymous, functions are treated just like data (higher order functions) in the same way variables are treated
Interface  A type implements an interface by implementing its methods and fields. There is no explicit declaration of intent, no "implements" keyword.
Sum Type  A sum type instance can hold a value of several different types. Use the type keyword to declare a sum type:
Generic Type  A type that is resolved during runtime
Enum  Grouping of related attributes. Enum match must be exhaustive or have an else branch. This ensures that if a new enum field is added, it's handled everywhere in the code.
Alias  To define a new type NewType as an alias for ExistingType, do type NewType = ExistingType. This is a special case of a sum type declaration.
Thread  A concurrency construct for performing different tasks concurrently
Reference  In general, V's references are similar to Go pointers and C++ references.
Shared  Data can be exchanged between a coroutine and the calling thread via a shared variable. Such variables should be created as shared and passed to the coroutine as such, too. 

Rust

struct: define a structure
enum: define an enumeration

Go

  • Go supports 8 different composite types as specified by the language specification, namely array, struct, pointer, function, interface, slice, map, and channel types.
  Array    An array is a numbered sequence of elements of a single type, called the element type. The number of elements is called the length and is never negative.  Struct    A struct is a sequence of named elements, called fields, each of which has a name and a type. Field names may be specified explicitly (IdentifierList) or implicitly (AnonymousField). Within a struct, non-blank field names must be unique.  Pointer    A pointer type denotes the set of all pointers to variables of a given type, called the base type of the pointer. The value of an uninitialized pointer is nil.  Function    A function type denotes the set of all functions with the same parameter and result types. The value of an uninitialized variable of function type is nil.  Interface    An interface type specifies a method set called its interface. A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface. The value of an uninitialized variable of interface type is nil.  Slice    A slice is a descriptor for a contiguous segment of an underlying array and provides access to a numbered sequence of elements from that array. A slice type denotes the set of all slices of arrays of its element type. The value of an uninitialized slice is nil.  Map    A map is an unordered group of elements of one type, called the element type, indexed by a set of unique keys of another type, called the key type. The value of an uninitialized map is nil.  Channels    A channel provides a mechanism for concurrently executing functions to communicate by sending and receiving values of a specified element type. The value of an uninitialized channel is nil.

Variables - initialize, assign, mutate#

V

  • Variables are declared and initialized with :=. This is the only way to declare variables in V.
  • The variable's type is inferred from the value on the right hand side. To choose a different type, use type conversion:
  • Unlike most other languages, V only allows defining variables in functions. Global (module level) variables are not allowed.
  • In V, variables are immutable by default. To be able to change the value of the variable, you have to declare it with mut.
  • Note the (important) difference between := and =. := is used for declaring and initializing, = is used for assigning.
  • Unlike most languages, variable shadowing is not allowed
  • In development mode the compiler will warn you that you haven't used the variable. In production mode it will not compile at all (like in Go).
name := 'Bob'age := 20large_number := i64(9999999999) //using type conversionmut num := 20

Rust

  • Values (like literals) can be bound to variables, using the let binding
  • The compiler will be able to infer the type of the variable from the context if the variable is not annotated with a type
  • Variable bindings are immutable by default, but this can be overridden using the mut modifier
  • Variable bindings have a scope, and are constrained to live in a block. A block is a collection of statements enclosed by braces {}
  • Also, variable shadowing is allowed.
  • It's possible to declare variable bindings first, and initialize them later (this may lead to the use of uninitialized variables)
  • The compiler forbids use of uninitialized variables, as this would lead to undefined behavior.
let an_integer = 1u32;let a_boolean = true;let unit = ();let mut mutable_binding = 1

Go

Not yet completed

Multi-dimensional Arrays#

Arrays are a huge enough topic to dedicate an entire page to, so I'll just be very brief here

V

# 2-dimensional examplemut a := [][]int{len: 2, init: []int{len: 3}}a[0][1] = 2
# 3-dimensional examplemut a := [][][]int{len: 2, init: [][]int{len: 3, init: []int{len: 2}}}a[0][1][1] = 2

Rust

Watch this space

Go

Watch this space

Functions#

V

  • The type comes after the argument's name.
  • Just like in Go and C, functions cannot be overloaded
  • Functions can be used before their declaration: add and sub are declared after main, but can still be called from main
  • Functions can return multiple values
  • Functions are private (not exported) by default. To allow other modules to use them, prepend pub. The same applies to constants and types.
fn main() {    println(add(77, 33))    println(sub(100, 50))}
fn add(x int, y int) int {    return x + y}
fn sub(x int, y int) int {    return x - y}
# returning multiple valuesfn foo() (int, int) {    return 2, 3}
a, b := foo()

Rust

Watch this space

Go

Watch this space

IF statements#

V

  • Unlike other C-like languages, there are no parentheses surrounding the condition and the braces are always required.
  • if can be used as an expression - there is no tenary operator
  • if can be used to check the current type of a sum type using is and its negated form !is - the same outcome can be achieved using match.
# if expressionnum := 777s := if num % 2 == 0 { 'even' } else { 'odd' }
# check sum typetype Alphabet = Abc | Xyz
x := Alphabet(Abc{'test'}) // sum typeif x is Abc {    // x is automatically casted to Abc and can be used here    println(x)}if x !is Abc {    println('Not Abc')}
# using match insteadmatch x {    Abc {        // x is automatically casted to Abc and can be used here        println(x)    }    Xyz {        // x is automatically casted to Xyz and can be used here        println(x)    }}

Rust

Watch this space

Go

Watch this space

IN operator#

V

in allows to check whether an array or a map contains an element. To do the opposite, use !in.

nums := [1, 2, 3]println(1 in nums) // trueprintln(4 !in nums) // truem := {    'one': 1    'two': 2}println('one' in m) // trueprintln('three' !in m) // true# simplifying boolean expressionsif parser.token in [.plus, .minus, .div, .mult] {  ...}

Rust

Watch this space

Go

Watch this space

For Loop#

V has only one looping keyword: for, with several forms.

for/in#

Used with an array, map or numeric range. Types that implement a next method returning an Option can be iterated with a for loop.

V

# array fornames := ['Sam', 'Peter']for i, name in names {    println('$i) $name')}# map form := {    'one': 1    'two': 2}for key, value in m {    println('$key -> $value')    // Output: one -> 1    //         two -> 2}# range forfor i in 0 .. 5 {    print(i)}

Rust

Watch this space

Go

Watch this space

Condition for#

This form of the loop is similar to while loops in other languages. The loop will stop iterating once the boolean condition evaluates to false.

Again, there are no parentheses surrounding the condition, and the braces are always required.

mut sum := 0mut i := 0for i <= 100 {    sum += i    i++}println(sum) // "5050"  # The condition can be omitted, resulting in an infinite loop.mut num := 0for {    num += 2    if num >= 10 {        break    }}println(num) // "10"

Rust

Watch this space

Go

Watch this space

C-like for#

There's the traditional C style for loop. It's safer than the while form because with the latter it's easy to forget to update the counter and get stuck in an infinite loop.

Here i doesn't need to be declared with mut since it's always going to be mutable by definition.

V

for i := 0; i < 10; i += 2 {    // Don't print 6    if i == 6 {        continue    }    println(i)}

Rust

Watch this space

Go

Watch this space

Labelled break & continue#

break and continue control the innermost for loop by default. You can also use break and continue followed by a label name to refer to an outer for loop:

V

outer: for i := 4; true; i++ {    println(i)    for {        if i < 7 {            continue outer        } else {            break outer        }    }}

Match#

A match statement is a shorter way to write a sequence of if - else statements. When a matching branch is found, the following statement block will be run. The else branch will be run when no other branches match.

A match statement can also be used to branch on the variants of an enum by using the shorthand .variant_here syntax. An else branch is not allowed when all the branches are exhaustive.

V

os := 'windows'print('V is running on ')match os {    'darwin' { println('macOS.') }    'linux' { println('Linux.') }    else { println(os) }}# branch on the variants of an enumc := `v`typ := match c {    `0`...`9` { 'digit' }    `A`...`Z` { 'uppercase' }    `a`...`z` { 'lowercase' }    else { 'other' }}println(typ)// 'lowercase'

Rust

Watch this space

Go

Watch this space

Defer#

A defer statement defers the execution of a block of statements until the surrounding function returns.

V

fn read_log() {    mut ok := false    mut f := os.open('log.txt') or { panic(err.msg) }    defer {        f.close()    }    // ...    if !ok {        // defer statement will be called here, the file will be closed        return    }    // ...    // defer statement will be called here, the file will be closed}

Rust

Watch this space

Go

Watch this space

Struct#