← Blog

Go Programming Language — Syntax and Data Types

2020-01-15 · 9 min read

goprogramming

Go Programming Language — Syntax and Data Types

Go Programming Language

In this post I’ll cover the fundamental building blocks of Go: syntax, types, keywords, variable declarations, constants, and UTF-8 support — with examples throughout.

Go has exactly 25 keywords — one of the fewest of any mainstream language. For comparison:

  • C++: 82
  • C#: 79
  • Java: 50
  • JavaScript: 38
  • Python 3.7: 35
  • Dart: 54

The complete Go keyword list:

break    default     func   interface select
case     defer       go     map      struct
chan     else        goto   package  switch
const    fallthrough if     range    type
continue for         import

There are also 47 operators and punctuation marks:

+   &   +=  &=   &&  ==  !=  (  )
-   |   -=  |=   ||  <   <=  [  ]
*   ^   *=  ^=   <-  >   >=  {  }
/   <<  /=  <<=  ++  =   :=  ,  ;
%   >>  %=  >>=  --  !   ...  .  :
&^  &^=

Variable Declaration

Go offers several ways to declare variables. The first is declaring without an initial value — the variable is automatically initialized to the zero value for its type:

var t int

You can also assign a value at declaration time using the = operator:

var t int = 10

The short variable declaration (:=) is a concise form that requires an initial value and infers the type. It creates a local variable scoped to the enclosing block — a function, loop, or other block:

t := 10

To explicitly specify a type during short declaration, wrap the value in a type conversion:

t := int64(10)

If a variable is declared without an initial value, Go assigns the zero value for its type:

  • Numeric types: 0
  • Boolean: false
  • String: "" (empty string)

Unlike some languages, Go has no undefined value. A bool, for instance, is always either true or false.

package main

import "fmt"

func main() {
  var name string
  fmt.Printf("var name %T = %+v\n", name, name)

  var payment float64
  fmt.Printf("var payment %T = %q\n", payment, payment)

  var isActive bool
  fmt.Printf("var isActive %T = %+v\n", isActive, isActive)
}

Output:

var name string = ""
var payment float64 = 0
var isActive bool = false

Variable Naming

Go does not have private, public, or protected access modifiers. Instead, visibility is determined by the case of the first letter of an identifier:

If the name starts with an uppercase letter, it is exported (accessible from other packages). If it starts with a lowercase letter, it is unexported (package-private).

This makes Go identifiers case-sensitive by design.

var FirstName string  // exported
var password string   // unexported

Additional naming rules:

  • Use only letters, digits, and underscores:
// Valid:   user_name, userName, signal4
// Invalid: best.offer, user-name
  • Must not start with a digit:
// Valid:   best4Game, house62
// Invalid: 10cars
  • No symbols:
// Valid:   product
// Invalid: $product

Multiple Assignment

Go allows assigning values to multiple variables of different types in a single statement — a feature that greatly improves code readability:

a, b, c := 12, true, "Google"

fmt.Println(a)
fmt.Println(c)
fmt.Println(b)

Output:

12
Google
true

Numeric Types

Go has two categories of numeric types: architecture-independent (fixed-size) and implementation-specific (size depends on the target platform).

Architecture-independent types:

uint8  unsigned  8-bit integer (0255)
uint16 unsigned 16-bit integer (065535)
uint32 unsigned 32-bit integer (04294967295)
uint64 unsigned 64-bit integer (018446744073709551615)
int8   signed    8-bit integer (-128127)
int16  signed   16-bit integer (-3276832767)
int32  signed   32-bit integer (-21474836482147483647)
int64  signed   64-bit integer (-92233720368547758089223372036854775807)
float32 IEEE-754 32-bit floating-point (+- 1e-45 -> +- 3.4e38)
float64 IEEE-754 64-bit floating-point (+- 5e-324 -> 1.7e308)
complex64
complex128
byte  ~~ uint8
rune  ~~ int32

Implementation-specific types:

uint    -> 32 or 64 bits
int     -> same size as uint
uintptr -> an unsigned integer large enough to hold any pointer value

Choosing the right fixed-size type matters for performance. If you only need values in the range 0–200, prefer uint8 over uint.

Example 1: Value within range — correct result:

package main

import (
  "fmt"
)

func main() {
  var t uint8 = 254
  fmt.Println(t + 1)
}

Output:

255

Example 2: Value overflows at print time — wraps to 0:

package main

import (
  "fmt"
)

func main() {
  var t uint8 = 254
  fmt.Println(t + 2)
}

Output:

0

Example 3: Overflow detected at compile time — compiler error:

package main

import (
  "fmt"
)

func main() {
  var t uint8 = 254 + 2
  fmt.Println(t)
}

Output:

./prog.go:8:20: constant 256 overflows uint8

Boolean

The bool type has two possible values: true and false. Its zero value is false. Booleans are commonly used in control flow:

package main

import (
  "fmt"
)

func main() {
  var isActive bool
  printed := true

  fmt.Println(isActive)
  fmt.Println(printed)
}

Output:

false
true

String

A string is an ordered sequence of one or more characters. In Go, string literals can be written with double quotes (") or raw string literals with backticks (`):

a := "He said \"Hello\" this morning"
b := `He said "Hello" this morning`

UTF-8

Go has native UTF-8 support built into the language — no external package or library required.

You can test this with the following code:

package main

import (
  "fmt"
)

func main() {
  a := "Hello, 世界"
  for i, c := range a {
    fmt.Printf("%d: %s\n", i, string(c))
  }
  fmt.Println("length of 'Hello, 世界': ", len(a))
}

Output:

0: H
1: e
2: l
3: l
4: o
5: ,
6:
7: 世
10: 界
length of 'Hello, 世界': 13

The reported length is greater than the character count because of Go’s rune type. A rune holds a character’s Unicode code point and is represented as 1–3 int32 values.

package main

import "fmt"

func main() {
  const nihongo = "日本語"
  for index, runeValue := range nihongo {
    fmt.Printf("%#U starts at byte position %d\n", runeValue, index)
  }
}

Output:

U+65E5 '' starts at byte position 0
U+672C '' starts at byte position 3
U+8A9E '' starts at byte position 6

Constants

Constants are immutable values — once assigned, they cannot be changed:

package main

import "fmt"

func main() {
  const gopher = "Nail"
  fmt.Println(gopher)
}

Output:

Nail

Attempting to reassign a constant causes a compile-time error:

package main

import "fmt"

func main() {
  const gopher = "Nail"
  fmt.Println(gopher)
  gopher = "Can"
  fmt.Println(gopher)
}

Output:

./prog.go:10:9: cannot assign to gopher

Typed and Untyped Constants

Constants can be declared with or without an explicit type. An untyped constant takes on a type implicitly based on its value at the point of use:

package main

import "fmt"

const (
  year     = 365       // untyped
  leapYear = int32(366) // typed
)

func main() {
  hours   := 24
  minutes := int32(60)
  fmt.Println(hours * year)
  fmt.Println(minutes * year)
  fmt.Println(minutes * leapYear)
}

Output:

8760
21900
21960

Assigning a value of the wrong type to a typed constant results in a compile-time error:

package main

import "fmt"

const (
  leapYear = int32("test") // typed
)

func main() {
  minutes := 10
  fmt.Println(minutes * leapYear)
}

Output:

./prog.go:6:18: cannot convert "test" (type untyped string) to type int32

Iota

iota is a special constant generator used to create a sequence of incrementing values. Within a const block, iota starts at 0 and increments by 1 for each constant:

package main

import "fmt"

const (
  Metin int = iota
  Ali
  Feyyaz
)

func main() {
  fmt.Printf("Metin is %v\n", Metin)
  fmt.Printf("Feyyaz is %v\n", Feyyaz)
  fmt.Printf("Ali is %v\n", Ali)
}

Output:

Metin is 0
Feyyaz is 2
Ali is 1

Console Output

Go’s fmt package provides formatted I/O. After importing it, you can use Print, Printf, and Println (among others) to produce output:

  • Use Println for plain string output.
  • Use Printf when you need to format values inline using verbs.
package main

import "fmt"

func main() {
  fmt.Println("This code was already here when I arrived.")
  fmt.Printf("It's always %s fault.\n", "the previous developer's")
}

Output:

This code was already here when I arrived.
It's always the previous developer's fault.

Format Verbs

All Print functions support format verbs. Common ones:

// %v  -> prints the value without type checking
// %s  -> for string values
// %d  -> for integer values (int, int32, etc.)
// %T  -> prints the type of the value
// %f  -> for floating-point values
// \n  -> newline
// \t  -> tab character

Example:

package main

import (
  "fmt"
)

func main() {
  const name, age, isWorking, mealCost = "Nail", 32, true, 21.5

  fmt.Printf("Name: %v \n", name)
  fmt.Printf("Age: %v \n", age)
  fmt.Printf("Is working: %v \n", isWorking)
  fmt.Printf("Meal cost: %v", mealCost)
  fmt.Println("********")
  fmt.Printf("Name: %s \n", name)
  fmt.Printf("Age: %d \n", age)
  fmt.Printf("Is working: %t \n", isWorking)
  fmt.Printf("Meal cost: %f", mealCost)
  fmt.Println("********")
  fmt.Printf("Name: %T \n", name)
  fmt.Printf("\t Age: %T \n", age)
  fmt.Printf("\t \t Is working: %T \n", isWorking)
  fmt.Printf("\t \t \t Meal cost: %T", mealCost)
}

Output:

Name: Nail
Age: 32
Is working: true
Meal cost: 21.5********
Name: Nail
Age: 32
Is working: true
Meal cost: 21.500000********
Name: string
   Age: int
      Is working: bool
         Meal cost: float64

For the full fmt package reference, see the official Go documentation.

Comments

Go uses the same comment syntax as most C-family languages:

// Single-line comment

For block comments:

/*
  All of these lines
  are
  comments
*/

Inline comments are also valid:

z := x % 2 // returns the remainder of x divided by 2

Go also uses comments to generate documentation. GoDoc parses the comments immediately above functions and types to build API documentation, so writing clear and accurate comments is especially important in Go.

See the Go Blog post on GoDoc for more details.

Structs

A struct is a composite data type that groups together fields under a single name — similar to a model or record in other languages. Structs are defined using the type keyword:

type User struct {
  Name  string
  Email string
}

Fields are accessed using dot notation. You can read a field’s value or assign a new one with =.

All of the following are valid ways to instantiate a struct:

a := User{}

b := User{
  Name:  "Homer Simpson",
  Email: "homer@example.com",
}

c := User{Email: "marge@example.com"}
c.Name = "Marge Simpson"

d := User{Name: "Homer Simpson", Email: "homer@example.com"}

e := User{"Homer Simpson", "homer@example.com"}

fmt.Println(a.Name)
fmt.Printf("User's email is %s", e.Email)

However, the positional initialization used for e is discouraged. If the struct definition changes — say, a new field is added — the positional form will break:

type User struct {
  ID    int
  Name  string
  Email string
}

After this change, the positional initialization produces:

too few values in User literal

The notes shared here are primarily derived from materials used in Google’s Go workshops and from the official Go documentation and Tour.