https://en.wikipedia.org/wiki/Kiss_(band)

“Simple statement” notion in Go

Michał Łowicki
golangspec

--

In Golang specification there is a term simple statement which maybe isn’t used frequently throughout the document but language’s grammar allows to use only such statements in a couple of important places. The goal of this story is to introduce what simple statements are and where they can be used.

Photography: Ursula Coyote / Netflix

In spec it’s defined by SimpleStmt production (using EBNF):

SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .

Let’s start then with introducing 6 types of simple statements…

1. Empty statement

It’s quite natural that statement doing absolutely nothing sits in jar labeled “simple statements”.

2. Increment or decrement statement

x++  // semantically equivalent to assignment x += 1
x-- // x -= 1

In Go, construct denoted by “ ” or “++” after expression operand isn’t an expression so you won’t see in source files with .go extension tangled compositions like:

foo = bar++ + 10

It’s a statement instead but since it’s simple statement can be used in places where makes sense and doesn’t introduce unnecessary burden where it shouldn’t (by making code unreadable and hard to maintain).

In Golang there is no prefix version i.e. where “ — “ or “++” are placed before operand.

3. Send statement

Lexical token “<-” denotes sending value through channel. It doesn’t return any value so no need to make expression out of it.

4. Expression statement

Certain expressions can be put into places where statement is expected.

Block is a sequence of statements enclosed by curly brackets so e.g. expression statements are totally valid in such context.

Allowed options are:

  • function calls (except some built-ins i.e. append, cap, complex, imag, len, make, new, real, unsafe.Alignof, unsafe.Offsetof and unsafe.sizeOf)
  • method calls
  • receive operations
func (s S) m() {
fmt.Printf("Hi %s\n", s.name)
}
func f(n int) {
fmt.Printf("f says %d\n", n)
}

func main() {
f(1)
s := S{"Michał"}
s.m()
c := make(chan int)
go func() {
c <- 1
}()
<-c numbers := []int{1, 2, 3}
len(numbers) // error: len(numbers) evaluated but not used
}

5. Assignment

Assignment in the most basic form should be well known. Declared variable is needed and right-hand expression must be assignable to a variable:

var population int64
population = 8000000 // assignment
var city, country string
// assignment of two single-value expressions to two variables
city, country = "New York", "USA"

When it goes to assigning more than one value to a list of variables there’re two forms. In the first one there is a single expression returning multiple values like e.g. function call:

f := func() (int64, string) {
return 2000000, "Paris"
}
var population int64
var city string
population, city = f()

Second form encompasses many expressions on the right side but each one is single-valued:

population, city = 13000000, "Tokyo"

These two forms cannot be mixed:

f := func() (int64, string) {
return 4000000, "Sydney"
}
var population int64
var city, country string
population, city, country = f(), "Australia"

as it throws “multiple-value f() in single-value context” while compilation.

An assignment operation is a construct assembled from “=” sign preceded by one of the binary operators:

  • addition (+)
  • subtraction (-)
  • bitwise OR (|)
  • bitwise XOR (^)
  • multiplication (*)
  • division (/)
  • modulo (%)
  • bitwise left and right shift (<< and >>)
  • bitwise AND (&)
  • bit clear (&^)

a op= b is semantically equivalent to a = a op b but a is evaluated only once. Assignment operation is also valid assignment statement.

6. Simple variable declaration

It’s a shorthand for regular variable declaration where initializer is required. Moreover type which isn’t explicitly specified is inferred from initializer’s type:

s := S{}
a := [...]int{1,2,3}
one, two := f(), g()

It’s clear now what constitutes set of valid simple statements. But where are they applicable?

If statement

Simple statement can be optionally placed before condition expression. It’s used quite often to declare variable (using short variable declaration), used later inside expression:

gen := func() int8 {
return 10
}
if num := gen(); num > 5 {
fmt.Printf("%d is greater than 5\n", num)
}

For statement

Init or post statements in for clause can be only simple statements. Common use case is to have short variable declaration as init statement and either assignment operation or increment / decrement statement:

for i := 0; i < 10; i += 1 {
fmt.Println(i)
}

Nothing stops programmer though from using other simple statements there.

Switch statement

In a similar way as with if statement it’s allowed to put optional simple statement before expression in expression switch:

switch aux := 2; letter {
case 'a':
fmt.Println(aux)
case 'b':
fmt.Println(aux << 1)
case 'c':
fmt.Println(aux << 2)
default:
fmt.Println("no match")
}

or in front of guard in type switch:

type I interface {
f(num int8) int8
}
type T1 struct{}func (t T1) f(num int8) int8 {
return 1
}
type T2 struct{}func (t T2) f(num int8) int8 {
return 2
}
...var i I = T1{}switch aux := 10; val := i.(type) {
case nil:
fmt.Printf("nil %T %d\n", val, aux)
case T1:
fmt.Printf("T1 %T %d\n", val, aux)
case T2:
fmt.Printf("T2 %T %d\n", val, aux)
}

which outputs:

T1 main.T1 10

--

--

Michał Łowicki
golangspec

Software engineer at Datadog, previously at Facebook and Opera, never satisfied.