In this post, I'll sum up Go syntax and make some comparisons with other programming languages at the same time.
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.
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
.
main
function takes no arguments and has no return value
command-line arguments is obtained through the os
and flag
package.
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
.
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.
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
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.
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].
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.
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
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.
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
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.
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
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
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.
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
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
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.
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
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.
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.
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.
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.
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
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
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
In this section, I'll list some concise syntax that's widely used in practice.
var (i int; s string)
var i , s = 1, "hello"
or i, s := 1, "hello"
var i, j, k int = 1, 2, 3
a := [6]int{1, 2, 3, 11, 21, 23}
s := []int{1, 2, 3}
a[low:high]
, both low
and high
can
be omitted. a[:5]
is equivalent to a[0:5]
, a[1:]
is equivalent to
a[1:len(a)]
, or you can even omit both a[:]
that's equivalent to
a[0:len(a)]
map
initialization: m := map[string]int{"song":2003, "yu":17}
struct
initialization
type rectangle struct {
name string
width int
height int
}
rect := rectangle {"rect1", 4, 3}
struct
type and a variable of that struct
type at the
same time
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)
for { .... }
is equivalent to for true {....}
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
for i := range c
receives values from the channel repeatedly until
it is closed.
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
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
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;
m := make(map[string]int)
var (k int; s string)
var (
k int;
s string
)
var (
k int
s string
)
var (k int; s string;)
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.
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