Go Programming Language Syntax Summary

In this post, I'll sum up Go syntax and make some comparisons with other programming languages at the same time.

1 Hello world


package main

import "fmt"

func main() {
    fmt.Println("Hello, world")
}

Code snippet Hello world of Go

If the Go compile has been installed in your computer, you can run it with the command go run hello.go.
This minimal program reveals a few insights of Go.

1.1 Every Go program is made up of packages

Programs start running in package main. The keywordpackage declares the current package and the other keyword import indicates packages that will be used.

It's really confusing that the package name is quoted after import but unquoted after package.

1.2 main function takes no arguments and has no return value

command-line arguments is obtained through the os and flag package.

1.3 Capital P in Println

Function naming often conform to the camel case rule which usually starts with a low case letter. However, in Go, if you want a function to be accessed from the other package, its name must begin with a capital letter. That's the so called exported identifiers.

So Println begins with a Capital P.

1.3 Semicolon

There's no semicolon in the sample code, although the formal syntax uses semicolon ";" as terminators, because the compiler will insert semicolons automatically according to simple rules[2]. We'll talk about these more in the end.

1.4 Limitations of code styles

The automatic semicolon insertions make the code cleaner but also lead to consequences, one of which is the limitation of code styles. For example, the following code snippet is invalid.


package main

import "fmt"

func main() 
{  /* can not be putted in a new line*/   
    fmt.Println("Hello, world")
}

Code snippet Invalid hello world due to semicolon insertions

The reason is obvious if we look at the code after automatic semicolon insertions.


package main;

import "fmt";

func main(); 
{   
    fmt.Println("Hello, world");
};

Code snippet Invalid hello world after semicolon insertions

2 Basic types

data type Go type The zero value
number int int8 int16 int32 int64 float32 float64 ... 0
string string empty string
boolean true false false

Table 1 Basic types

Variables declared without an explicit initial value are given their zero value.

2.1 Variable declaration

Let's take a close look at variable declaration which is very different from C, Java, Python, JavaScript, etc.


var x int
 ^  ^  ^
 |  |  |
 |  | type is int
 | name is x
We'll define a variable

Figure 1 Anatomy of variable declaration

This peculiar declaration syntax applies to declarations of all types, the basic idea here is the important and fixed part come first. It don't show advantages in the declaration of basic types, but will do in more complicated examples. Rob Pike, one of creators of Go, has written an essay to illustrate that[3].

2.2 Short variable declaration

We can also declare a variable without specifying an explicit type, the variable's type is inferred from the value on the right hand side.


x := 3

Code snippet Short variable declaration

However, this syntax is only available inside a function.

2.3 Only explicit type conversion is allowed

The expression T(v) converts the value v to the type T.


var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

Code snippet Type conversion

Unlike in C, in Go assignment between items of different type always requires an explicit conversion.


var i int = 42
/* cannot use x (variable of type int) as float64 value 
 * in variable declaration */
var f float64 = i  

Code snippet Invalid type conversion

3 Control flow

3.1 if statements


x := 3
if x < 0 {
    fmt.Println("x is negative.")
} else if x == 0 {
    fmt.Println("x is equal to zero.")
} else {
    fmt.Println("x is positive")
}

Code snippet if statements

The syntax of if statement is similar to C, except the expression need not be surrounded by parentheses ( ) but the braces { } are required.

As explained in the hello world example, the staring brace "{" can not be putted in a new line.

3.2 for statements


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

/* empty initial and post statement is ok */
j := 5
for ; j > 0; {
    fmt.Println(j)
    j--;
}

Code snippet Basic for statements

The syntax of for statement is also similar to C, except there are no parentheses surrounding the three components of for statements and the braces "{" "}" are always required.

In the form of empty initial and post statement, semicolons can be dropped, this gives us the while like syntax in Go.


j := 5
for j > 0 {
    fmt.Println(j)
    j--;
}

Code snippet While style loop in Go

4 Function

4.1 Function declarations


func add(x int, y int) int {
    return x + y
}

Code snippet A simple function takes two int and return their sum

The function declaration syntax adheres to the same rule shared by variable declarations.


func add(x int, y int) int {
 ^    ^       ^         ^  ^
 |    |       |         |  |
 |    |       |         | starting { must be in the same line
 |    |    arguments return type
 |   name      
declare a function

Figure 2 Anatomy of a function declaration

The arguments and return value is also variable declaration, why not use the exactly same syntax with a var keyword like this: func add(var x int, var y int) var int {.

I guess the reason is that the part after the starting parenthesis must be arguments declarations, so omitting var can make syntax cleaner without any ambiguity.

4.2 Multiple return values

A function in Go is able to return multiple values.


package main

import "fmt"

func swap(x, y string) (string, string) {
    return y, x
}

func main() {
    /* receive multiple return values */
    a, b := swap("hello", "world")
    fmt.Println(a, b)
}

Code snippet A function that return two values

4.3 Function closures

Go functions may be closures. A closure is a function value that references variables from outside its body. The function may access and assign to the referenced variables; in this sense the function is "bound" to the variables.

For example, the adder function returns a closure. Each closure is bound to its own sum variable.


package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

Code snippet Function closures

5 Advanced types

5.1 Array


var a [5]int
a[0]=10
a[1]=9
fmt.Println(a[0])

Code snippet Array example

The type [n]T is an array of n values of type T, the declaration follows the same principles.


var a [3]int
 ^  ^  ^  ^
 |  |  |  |
 |  |  | element in array is int
 |  | array of size 3
 | name
define a variable

Figure 3 Anatomy of array declaration.

5.2 Slice

An array has a fixed size. A slice, on the other hand, is a dynamically-sized. The type []T is a slice with elements of type T.

Every slice has an underlying array, so you can create a slice from an array by specifying two indices, a low(inclusive) and high(exclusive) bound.


var a [5]int
a[0]=10
a[1]=9
var s []int = a[1:4]

Code snippet Create slice from an existing array

Slices are like references to array, changing the elements of a slice modifies the corresponding elements of its underlying array. You can think internals of the example slice as follows:


             cap of s
            |<----->|
         +----------+
Array a  |10|9|0|0|0|
         +----------+
             ^   ^
             |___|
            slice s

Figure 4 The internals of the slice s

This leads to two important concepts of a slice: length and capacity.

The length and capacity of a slice s can be obtained using the built-in len(s) and cap(s).

You can create a slice with the standard function make([]int, 1, 5), the first argument is type, the second is the length and the last is the capacity

Finally, we can append new elements to extend slice: s = append(s, 11). If the backing array of s is too small to fit all given values a bigger array will be allocated.


         +----------+
Array a  |10|9|0|0|0|
         +----------+
             ^   ^
             |___|
           slice s

s = append(s, 11)
         +-----------+
Array a  |10|9|0|0|11|
         +-----------+
             ^     ^
             |_____|
            slice s

s = append(s, 12, 13)
         +-----------+
Array a  |10|9|0|0|11|
         +-----------+
            +------------------+
New backing |9|0|11|12|13|0|0|0|
  array     +------------------+
             ^         ^
             |_________|
               slice s

Figure 4 Appending new element to a slice s

5.3 map

A map is a key-value dictionary.


/* define a map with make */
var m map[string]int = make(map[string]int)
/* add key-value pairs */
m["songziyu"] = 2003
m["blog"] = 2001
/* delete a key value pair*/
delete(m, "blog")

The same declaration principle applies to map too.


var m map[string]int
 ^  ^  ^    ^     ^
 |  |  |    |     |
 |  |  |    | value type
 |  |  | key type
 |  | type is map
 | name
declare a variable

Figure 5 Anatomy of map declaration

5.4 Pointers

Go has pointer. The type *T is a pointer to a T value. A pointer holds the memory address of a value, it's like a reference to that value.


var p *int

i := 42
p = &i

fmt.Println(*p) // read i through the pointer p
*p = 21         // set i through the pointer p

Code snippet Basic pointer usage

The & operator generates a pointer to its operand. The * operator denotes the pointer's underlying value.


var p *int
 ^  ^ ^ ^
 |  | | |
 |  | | point to int
 |  | type is pointer
 | name 
declare a variable

Figure 6 Anatomy of a pointer declaration

Unlike C, Go has no pointer arithmetic, so expressions like p+1 is illegal.

5.5 Structures

A structure is a user-defined type, it is a collection of fields, the field can be any type: int, string, boolean, slice, array or even structure itself.


type rectangle struct {
    name string
    width int
    height int
}

var rect rectangle
rect.name = "name"
rect.width = 4
rect.height = 3
fmt.Println(rect.name, rect.width, rect.height)

Code snippet Basic structure usage

Again, structure as a user-defined type, its declaration syntax is aligned with other type declaration.


type rectangle struct {
 ^       ^       ^    ^
 |       |       |    |
 |       |       | The beginning { of field declarations block
 |     name   a structure
declare a customized type

Figure 7 Anatomy of a structure declaration

In fact, type in Go can also be used to give alias to existing types, for example, type bigint int64 declares a new type name bigint which is an alias for int64, you can use it like a basic type.


type bigint int64
var population bigint = 1e9
fmt.Println(population)

Code snippet type alias

5.6 Methods: special member functions

Go does not have classes. But it can implement the paradigm of classes, namely a collection of member variables and member functions.

Structure is a collection of variables, we can attach functions to structures.


func circumference(rect rectangle) int {
    return rect.height * 2 + rect.height * 2
}

/* output 12 */
fmt.Println(circumference(rect))

Code snippet A function for the rectangle structure

This function works but Go provide a better way that's more similar to classes.


func (rect rectangle) circumference() int {
    return rect.height * 2 + rect.height * 2
}

/* output 12 */
fmt.Println(rect.circumference())

Code snippet A method of the structure rectangle

The additional part (rect rectangle) between the func keyword and the function name is called a receiver argument.

A Function with a receiver argument is a special function called method, you can treat it as a member function of that receiver type.

5.7 Pointers to structures

A pointer can point to a structure.


var pRect *rectangle = &rect
fmt.Println((*pRect).width)
fmt.Println(pRect.height)

Code snippet Pointers to structures

As you can see, Both (*pRect).width and pRect.height are legal in Go, that's not true in C.

A pointer can also be a receiver argument.


func (pRect *rectangle) circumference() int {
    return pRect.height * 2 + pRect.height * 2
}

/* output 12 */
fmt.Println(pRect.circumference())

Code snippet A pointer receiver

In general, we prefer a pointer receiver to value receiver for two reasons:

1. The method with pointer receiver can modify the value that the receiver points to.
2. Avoid copying the value on each method call.
In the other words, every time you call a method, you need a deep copy of the argument and pass it to the method. A pointer store address, all you need to copy is the address. By contrast, if the receiver is a large structure, for example, you need copy lots of fields of that structure.

5.8 interface

An interface is a also a user-defined type that's made up of a set of method signatures. A value of interface type can hold any value that implements those methods.


type IShape interface {
    circumference() int
}

type rectangle struct {
    name   string
    width  int
    height int
}

func (pRect *rectangle) circumference() int {
    return pRect.height * 2 + pRect.height * 2
}

/* a *rect implements IShape */
var shape IShap = &rect
fmt.Println(shape.circumference())

Code snippet Interface

A type implements an interface by implementing its methods. There is no explicit declaration of intent, no "implements" keyword.

Note the difference between pointer and value here.

In the above example, the method func (pRect *rectangle) circumference() takes a pointer receiver, so we it's *rect implements IShape, rect does not implements IShape. As a result, var shape IShape = rect is invalid.

6 Concurrency and Goroutines

I would be remiss if I didn't introduce concurrency in Go. Because Go is famous for high concurrency performance and has some built-in mechanisms for concurrency programming.

6.1 Goroutine

A goroutine is a lightweight thread(compared with the operation system's thread) managed by the Go runtime. It's easy to start a new goroutine with go keyword.


package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    /* say("world") is running in another goroutine */
    go say("world")
    say("hello")
}

Code Snippet 21 A Goroutine example

6.2 Channel

A channel is a conduit through which you can send and receive values between different goroutines.

A channel has a type and capacity. The type means only values of the same type can be transported. The capacity means a receiver blocks when reading from an empty channel and a sender blocks when writing to a full channel.


package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for i := 0; i < len(s); i++ {
        sum += s[i]
    }
    c <- sum // send sum to c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    /* create channel with make, the capacity is 0*/
    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
}

Code snippet A Channel example

A sender can close a channel to indicate no more values will be sent.
A receiver can test whether a channel has been closed by assigning a second parameter to the receive expression.


package main

import "fmt"

func main() {
    c := make(chan int, 1)
    c <- 1
    close(c)
    x, more := <-c
    fmt.Println(x)
    if more {
        fmt.Println("more to be read...")
    } else {
        fmt.Println("channel is closed")
    }
}

Code snippet close a channel

6.3 select

The select statement lets a goroutine wait on multiple communication operations.

A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.


package main

import "fmt"
import "time"

func add(c chan int, quit chan int) {
    sum := 0
    for true {
        select {
        case x := <-c:
            fmt.Printf("receive %d\n", x)
            sum += x
        case <-quit:
            fmt.Println(sum)
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go add(c, quit)
    for i := 0; i < 5; i++ {
        fmt.Printf("send %d\n", i)
        c <- i
    }
    quit <- 0
    time.Sleep(50 * time.Millisecond)
}

Code snippet A select example

7 Syntactic sugar

In this section, I'll list some concise syntax that's widely used in practice.

7.1 declaration and initialization


type rectangle struct {
    name string
    width int
    height int
}

rect := rectangle {"rect1", 4, 3}

s := struct {
    name string
    width int
    height int
}{ "rect1", 4, 3 }

i, j := 3, 4
sum := func(x, y int) int {
    return x + y
}(3, 4)

7.2 range

The range form of the for loop iterates over a slice, map or channel.


a := [6]int{1, 2, 3, 11, 21, 23}

for i, v := range a{
    fmt.Println(i, v)
}

for _, v := range a{
    fmt.Println(v)
}

for i, _ := range a{
    fmt.Println(i)
}

for i := range a {
    fmt.Println(i)
}

Code snippet Range over a slice


m := map[string]int{"song":2003, "yu":17}
for k, v := range m {
    fmt.Println(k, v)
}

for _, v := range m {
    fmt.Println(v)
}

Code snippet Range over a map


func count(n int, c chan int) {
    for i := 1; i <= n; i++ {
        c <- i
    }
    close(c)
}

c := make(chan int, 5)
go count(cap(c), c)
for i := range c {
    fmt.Println(i)
}

Code snippet Range over a channel

7.3 if with a initial statement

Like for, the if statement can start with a short statement to execute before the condition.


count := 10 
if count++; count < 10 {
    fmt.Println("litter than 10")
} else {
    fmt.Println("greater than 10")
}

Code snippet if with a initial statement

8 semicolon

8.1 rules

According to the Go specification[2]:

The formal syntax uses semicolons ";" as terminators in a number of productions. Go programs may omit most of these semicolons using the following two rules:

1. When the input is broken into tokens, a semicolon is automatically inserted into the token stream immediately after a line's final token if that token is:
1.1 an identifier
1.2 an integer, floating-point, imaginary, rune, or string literal
1.3 one of the keywords break, continue, fallthrough, or return
1.4 one of the operators and punctuation ++, --, ), ], or }

2. To allow complex statements to occupy a single line, a semicolon may be omitted before a closing ")" or "}".

These rules are used for different purposes. Let's give simple examples for them.


i := 10
p := &i

since 10 is an integer and i is an identifier, so ; will be inserted.


i := 10;
p := &i;

var (k int; s string)
var (
     k int; 
     s string
    )
var (
     k int
     s string
    )
var (k int; s string;)

8.2 Consequences

Due to the semicolon insertions, some unexpected consequences may occur.

For example, the following code is invalid.


a := 0
// The following two lines both fail to compile.
println(a++) // unexpected ++, expecting comma or )
println(a--) // unexpected --, expecting comma or )

The reason why the above code is invalid is compilers will view it as


a := 0;
println(a++;);
println(a--;);

And the seemingly invalid code is valid.


for
i := 0
i < 6
i++ {
    fmt.Println(i)
}

the code after semicolon insertions:


for
i := 0;  /* the initial statement */
i < 6;   /* the conditional statement */
i++ {
    fmt.Println(i)
}

You can read more in this article.

9 What's missing

10 Reference

1 A tour of Go
2 The Go Programming Language Specification. Semicolons
3 Go's Declaration Syntax
4 Go101 website. line break rules

Written by Songziyu @China Nov. 2023