Go Language Tutorial: Fundamentals of Golang Syntax and Data Types
A beginner-friendly Go (Golang) tutorial covering language features like easy deployment, built-in concurrency, and strong performance. Learn Go program structure, basic syntax, data types, variables, constants, operators, control flow, functions, pointers, structs, slices, maps, interfaces, error handling, goroutines, and channels.
3733  Words
2019-01-25

Introduction
Go (also known as Golang) is an open-source programming language developed by Google in 2007. It was publicly announced on November 10, 2009, and Go 1.0 was released in early 2012. Today, Go development is fully open source with a thriving community behind it.
Go stands out for its simple deployment, excellent concurrency support, clean language design, and strong runtime performance.
1. Go Program Structure
A Go program is made up of the following building blocks:
- Package declaration
- Import statements
- Functions
- Variables
- Statements and expressions
- Comments
Here is a minimal example:
// Declare the package name
package main
// Import the fmt package
import "fmt"
// Define a function; main is the entry point
func main() {
/* This is my first simple program */
fmt.Println("Hello, World!")
}
2. Basic Syntax
- Line separator: Each line represents a complete statement; no semicolons needed
- Comments: Single-line with
//, multi-line with/* */ - Identifiers: Used to name variables, types, and other entities. Must start with a letter or underscore, not a digit
- Keywords: Go has 25 reserved keywords
And 36 predefined identifiers:

- Whitespace: Use spaces between declarations and around operators for readability
2.1 Data Types
Data types determine how much memory a variable occupies and what operations are allowed on it.
Go organizes its data types into several categories:
- Boolean (
bool)
A boolean can only be true or false. Example: var b bool = true
- Numeric types
Integer types (int) and floating-point types (float32, float64).
Integer types include:

Floating-point types:

Other numeric types:

- String type
A string is a sequence of bytes connected end to end. In Go, strings are composed of individual bytes using UTF-8 encoding to represent Unicode text.
- Derived types
- Pointer
- Array
- Struct
- Channel
- Function
- Slice
- Interface
- Map
2.2 Variables
Variable names consist of letters, digits, and underscores. The first character cannot be a digit.
There are three ways to declare a variable:
- Specify the type explicitly; if no value is assigned, the zero value is used:
var v_name v_type
v_name = value
- Let Go infer the type from the assigned value:
var v_name = value - Use the short declaration operator
:=(the variable on the left must not have been declared previously):
v_name := value
// For example:
x := 10
Multiple variable declarations:
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3
// Type inference (similar to Python)
var vname1, vname2, vname3 = v1, v2, v3
// Short declaration (variables must not already be declared)
vname1, vname2, vname3 := v1, v2, v3
// Factored declaration, commonly used for global variables
var (
vname1 v_type1
vname2 v_type2
)
Note: A declared local variable that is never used will cause a compile error.
2.3 Constants
Constants are identifiers whose values cannot change at runtime. Only boolean, numeric, and string types are allowed.
- Declaration syntax:
const identifier [type] = value
// Explicit type
const b string = "abc"
// Implicit type (inferred)
const b = "abc"
// Examples
const LENGTH int = 10
const WIDTH = 10
- Constants can serve as enumerations:
const (
Unknown = 0
Female = 1
Male = 2
)
- Constants can be computed using built-in functions such as
len(),cap(), andunsafe.Sizeof():
package main
import "unsafe"
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)
func main(){
println(a, b, c)
}
- The
iotaconstant
iota is a special constant that the compiler increments automatically. It resets to 0 whenever the const keyword appears, and increases by 1 for each subsequent line within the const block. Think of it as the line index within a const block.
Using iota for enumerations:
const (
a = iota // 0
b = iota // 1
c = iota // 2
)
// Shorthand (iota is implicit after the first usage)
const (
a = iota
b
c
)
2.4 Operators
Arithmetic operators (assuming A = 10, B = 20):

Relational operators (assuming A = 10, B = 20):

Logical operators (assuming A = true, B = false):

Bitwise operators (assuming A = 60, B = 13):

Note: Right-shifting by n bits is equivalent to dividing by 2^n, but this does not hold for negative numbers.
Truth table for &, |, and ^:

Binary representations of A = 60 and B = 13:

Assignment operators:

Other operators (address-of
&and dereference*):
package main import "fmt" func main() { var a int = 4 var b int32 var c float32 var ptr *int fmt.Printf("Line 1 - type of a = %T\n", a) fmt.Printf("Line 2 - type of b = %T\n", b) fmt.Printf("Line 3 - type of c = %T\n", c) ptr = &a // ptr holds the address of a fmt.Printf("Value of a: %d\n", a) fmt.Printf("Value of *ptr: %d\n", *ptr) }Operator precedence
Binary operators are evaluated left to right. The table below lists operators from highest to lowest precedence:

You can use parentheses to override the default precedence.
2.5 Conditional Statements
Control flow diagram:

Go provides the following conditional constructs:

Examples with if...else and switch:
if a < 20 {
fmt.Printf("a is less than 20\n")
} else {
fmt.Printf("a is not less than 20\n")
}
switch {
case grade == "A":
fmt.Printf("Excellent!\n")
case grade == "B", grade == "C":
fmt.Printf("Good\n")
case grade == "D":
fmt.Printf("Pass\n")
case grade == "F":
fmt.Printf("Fail\n")
default:
fmt.Printf("Poor\n")
}
The select statement is a control structure similar to switch, but designed for channel communication. Each case must be a send or receive operation. Go randomly picks a runnable case; if none is ready, it blocks until one becomes available. A default clause is always runnable.
2.6 Loops
Loop flow diagram:

Go’s for loop comes in three forms:
for init; condition; post { }for condition { }for { }(infinite loop)
Where init is the initializer, condition is the loop condition, and post is the post-iteration statement.
The for...range form iterates over slices, maps, arrays, and strings:
for key, value := range oldMap {
newMap[key] = value
}
Full example:
package main
import "fmt"
func main() {
var b int = 15
var a int
numbers := [6]int{1, 2, 3, 5}
// Classic for loop
for a := 0; a < 10; a++ {
fmt.Printf("Value of a: %d\n", a)
}
// While-style loop
for a < b {
a++
fmt.Printf("Value of a: %d\n", a)
}
// Range-based loop
for i, x := range numbers {
fmt.Printf("Index %d, value = %d\n", i, x)
}
}
2.7 Functions
Functions are the basic building blocks of a Go program. Every Go program has at least one function: main().
- Function definition:
func function_name( [parameter list] ) [return_types] {
// function body
}
func introduces the declaration. The function name and parameter list together form the function signature. Parameters act as placeholders for values passed at call time. Return types specify the types of the returned values; they can be omitted if the function returns nothing.
Example:
// Returns the larger of two numbers
func max(num1, num2 int) int {
var result int
if num1 > num2 {
result = num1
} else {
result = num2
}
return result
}
- Multiple return values:
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("Mahesh", "Kumar")
fmt.Println(a, b)
}
- Function parameters
Go supports two argument-passing mechanisms:
(a) Pass by value: The function receives a copy of the argument.
func swap(x, y int) int {
var temp int
temp = x
x = y
y = temp
return temp
}
(b) Pass by reference (using pointers): The function receives a pointer to the original variable.
func swap(x *int, y *int) {
var temp int
temp = *x
*x = *y
*y = temp
}
- Advanced function usage
(a) Functions as values: Functions can be assigned to variables.
package main
import (
"fmt"
"math"
)
func main(){
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
fmt.Println(getSquareRoot(9))
}
(b) Closures: Anonymous functions that capture variables from their enclosing scope.
package main
import "fmt"
func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main(){
nextNumber := getSequence()
fmt.Println(nextNumber()) // 1
fmt.Println(nextNumber()) // 2
fmt.Println(nextNumber()) // 3
// A new closure with its own state
nextNumber1 := getSequence()
fmt.Println(nextNumber1()) // 1
fmt.Println(nextNumber1()) // 2
}
(c) Methods: Functions with a receiver argument, attached to a specific type.
Syntax:
func (variable_name variable_data_type) function_name() [return_type]{
// method body
}
Example:
package main
import "fmt"
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("Area of circle =", c1.getArea())
}
// This method belongs to the Circle type
func (c Circle) getArea() float64 {
return 3.14 * c.radius * c.radius
}
2.8 Variable Scope
Go has three levels of variable scope:
- Local variables (declared inside a function)
- Global variables (declared outside all functions)
- Formal parameters (function arguments)
2.9 Arrays
An array is a fixed-length, numbered sequence of elements of a single type. Elements are accessed by index, starting at 0.

- Declaration:
var variable_name [SIZE] variable_type
For example, an array of 10 float32 values: var balance [10] float32
- Initialization:
The number of elements in {} must not exceed the number in [].
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
You can let Go count the elements automatically:
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
- Reading and writing elements:
package main
import "fmt"
func main() {
var n [10]int
var i, j int
for i = 0; i < 10; i++ {
n[i] = i + 100
}
for j = 0; j < 10; j++ {
fmt.Printf("Element[%d] = %d\n", j, n[j])
}
}
- Multidimensional arrays:
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
Example: var threedim [5][10][4]int
Initializing a 2D array:
a = [3][4]int{
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11},
}
Full example:
package main
import "fmt"
func main() {
var a = [5][2]int{{0,0}, {1,2}, {2,4}, {3,6}, {4,8}}
var i, j int
for i = 0; i < 5; i++ {
for j = 0; j < 2; j++ {
fmt.Printf("a[%d][%d] = %d\n", i, j, a[i][j])
}
}
}
2.10 Pointers
- Definition
A pointer variable stores the memory address of another variable. Declaration syntax: var var_name *var-type
var ip *int // pointer to int
var fp *float32 // pointer to float32
package main
import "fmt"
func main() {
var a int = 20
var ip *int
ip = &a // ip now holds the address of a
fmt.Printf("Address of a: %x\n", &a)
fmt.Printf("Address stored in ip: %x\n", ip)
fmt.Printf("Value at *ip: %d\n", *ip)
}
- Nil pointers
An uninitialized pointer has the value nil (also called a null pointer).
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("Value of ptr: %x\n", ptr)
}
Checking for nil:
if ptr != nil { /* ptr is not nil */ }
if ptr == nil { /* ptr is nil */ }
- Arrays of pointers:
package main
import "fmt"
const MAX int = 3
func main() {
a := []int{10, 100, 200}
var i int
var ptr [MAX]*int
for i = 0; i < MAX; i++ {
ptr[i] = &a[i]
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i, *ptr[i])
}
}
- Pointer to pointer
A pointer to a pointer stores the address of another pointer variable.

Declaration: var ptr **int
Accessing the value requires double dereferencing:
package main
import "fmt"
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
ptr = &a
pptr = &ptr
fmt.Printf("Value of a = %d\n", a)
fmt.Printf("Value at *ptr = %d\n", *ptr)
fmt.Printf("Value at **pptr = %d\n", **pptr)
}
- Pointers as function arguments
Pass a pointer to allow the function to modify the original variable:
package main
import "fmt"
func main() {
var a int = 100
var b int = 200
fmt.Printf("Before swap: a = %d\n", a)
fmt.Printf("Before swap: b = %d\n", b)
swap(&a, &b)
fmt.Printf("After swap: a = %d\n", a)
fmt.Printf("After swap: b = %d\n", b)
}
func swap(x *int, y *int) {
var temp int
temp = *x
*x = *y
*y = temp
}
2.11 Structs
A struct is a composite data type that groups together fields of different types.
While arrays store elements of the same type, structs let you define different data types for each field.
- Defining a struct:
type struct_variable_type struct {
member1 type1
member2 type2
...
}
The struct keyword defines a new data type with one or more members. The type keyword assigns a name to it.
- Using a struct:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
// Positional initialization
fmt.Println(Books{"Go Language", "example.com", "Go Tutorial", 6495407})
// Named field initialization
fmt.Println(Books{title: "Go Language", author: "example.com", subject: "Go Tutorial", book_id: 6495407})
// Omitted fields default to zero values
fmt.Println(Books{title: "Go Language", author: "example.com"})
}
- Accessing struct members with the
.operator:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books
var Book2 Books
Book1.title = "Go Language"
Book1.author = "example.com"
Book1.subject = "Go Tutorial"
Book1.book_id = 6495407
Book2.title = "Python Tutorial"
Book2.author = "example.com"
Book2.subject = "Python Guide"
Book2.book_id = 6495700
fmt.Printf("Book 1 title: %s\n", Book1.title)
fmt.Printf("Book 1 author: %s\n", Book1.author)
fmt.Printf("Book 1 subject: %s\n", Book1.subject)
fmt.Printf("Book 1 book_id: %d\n", Book1.book_id)
fmt.Printf("Book 2 title: %s\n", Book2.title)
fmt.Printf("Book 2 author: %s\n", Book2.author)
fmt.Printf("Book 2 subject: %s\n", Book2.subject)
fmt.Printf("Book 2 book_id: %d\n", Book2.book_id)
}
- Structs as function arguments:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books
Book1.title = "Go Language"
Book1.author = "example.com"
Book1.subject = "Go Tutorial"
Book1.book_id = 6495407
printBook(Book1)
}
func printBook(book Books) {
fmt.Printf("Book title: %s\n", book.title)
fmt.Printf("Book author: %s\n", book.author)
fmt.Printf("Book subject: %s\n", book.subject)
fmt.Printf("Book book_id: %d\n", book.book_id)
}
- Struct pointers:
Declare a pointer to a struct: var struct_pointer *Books
Get the address with &: struct_pointer = &Book1
Access members through a pointer using . (same as with a value): struct_pointer.title
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books
Book1.title = "Go Language"
Book1.author = "example.com"
Book1.subject = "Go Tutorial"
Book1.book_id = 6495407
printBook(&Book1)
}
func printBook(book *Books) {
fmt.Printf("Book title: %s\n", book.title)
fmt.Printf("Book author: %s\n", book.author)
fmt.Printf("Book subject: %s\n", book.subject)
fmt.Printf("Book book_id: %d\n", book.book_id)
}
2.12 Slices
A slice is an abstraction over arrays. Unlike arrays, slices have a dynamic length and can grow using append. Think of them as Go’s version of dynamic arrays.
- Declaring a slice:
var identifier []type(no size specified)
Or use make():
var slice1 []type = make([]type, len)
// Shorthand
slice1 := make([]type, len)
You can also specify an optional capacity: make([]T, length, capacity)
Initializing a slice:
a. Direct initialization:
s := []int{1, 2, 3}creates a slice withcap == len == 3b. From an array:
s := arr[:]creates a slice referencing the entire arrays := arr[startIndex:endIndex] // elements from startIndex to endIndex-1 s := arr[startIndex:] // from startIndex to end s := arr[:endIndex] // from beginning to endIndex-1c. From another slice:
s1 := s[startIndex:endIndex]d. Using
make():s := make([]int, len, cap)Slice operations:
Use len() to get the current length and cap() to get the maximum capacity. An uninitialized slice is nil with length 0.
package main
import "fmt"
func main() {
var numbers = make([]int, 3, 5)
printSlice(numbers)
var numbers2 []int
printSlice(numbers2)
if numbers2 == nil {
fmt.Printf("slice is nil")
}
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
To grow a slice beyond its capacity, create a new larger slice and copy the contents. Use append() to add elements and copy() to duplicate a slice:
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
numbers = append(numbers, 0)
printSlice(numbers)
numbers = append(numbers, 1)
printSlice(numbers)
numbers = append(numbers, 2, 3, 4)
printSlice(numbers)
// Create a new slice with double the capacity
numbers1 := make([]int, len(numbers), cap(numbers)*2)
copy(numbers1, numbers)
printSlice(numbers1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
Sub-slicing with [lower:upper]:
package main
import "fmt"
func main() {
numbers := []int{0,1,2,3,4,5,6,7,8}
printSlice(numbers)
fmt.Println("numbers ==", numbers)
fmt.Println("numbers[1:4] ==", numbers[1:4])
fmt.Println("numbers[:3] ==", numbers[:3])
fmt.Println("numbers[4:] ==", numbers[4:])
numbers1 := make([]int, 0, 5)
printSlice(numbers1)
number2 := numbers[:2]
printSlice(number2)
number3 := numbers[2:5]
printSlice(number3)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
2.13 Range
The range keyword iterates over arrays, slices, channels, or maps in a for loop. For arrays and slices, it returns the index and value. For maps, it returns each key-value pair.
package main
import "fmt"
func main() {
// Sum a slice using range
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
// Use the index when needed
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
// Range over a map
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
// Range over a string (yields index and Unicode code point)
for i, c := range "go" {
fmt.Println(i, c)
}
}
2.14 Maps
A map is an unordered collection of key-value pairs, providing fast lookups by key.
- Declaring a map:
// Declare a map variable (nil by default, cannot store data)
var map_variable map[key_data_type]value_data_type
// Create a usable map with make
map_variable := make(map[key_data_type]value_data_type)
An uninitialized map is nil and cannot be used to store key-value pairs.
Example:
package main
import "fmt"
func main() {
var countryCapitalMap map[string]string
countryCapitalMap = make(map[string]string)
countryCapitalMap["France"] = "Paris"
countryCapitalMap["Italy"] = "Rome"
countryCapitalMap["Japan"] = "Tokyo"
countryCapitalMap["India"] = "New Delhi"
for country := range countryCapitalMap {
fmt.Println(country, "capital is", countryCapitalMap[country])
}
// Check if a key exists
capital, ok := countryCapitalMap["United States"]
if ok {
fmt.Println("Capital of United States is", capital)
} else {
fmt.Println("Capital of United States not found")
}
}
- The
delete()function removes an element from a map:
package main
import "fmt"
func main() {
countryCapitalMap := map[string]string{
"France": "Paris",
"Italy": "Rome",
"Japan": "Tokyo",
"India": "New Delhi",
}
fmt.Println("Original map:")
for country := range countryCapitalMap {
fmt.Println(country, "capital is", countryCapitalMap[country])
}
delete(countryCapitalMap, "France")
fmt.Println("\nAfter deleting France:")
for country := range countryCapitalMap {
fmt.Println(country, "capital is", countryCapitalMap[country])
}
}
2.15 Interfaces
An interface defines a set of method signatures. Any type that implements all those methods satisfies the interface.
- Defining an interface:
type interface_name interface {
method_name1() return_type
method_name2() return_type
}
Example:
package main
import "fmt"
type Phone interface {
call()
}
type NokiaPhone struct{}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct{}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
2.16 Error Handling
Go provides a simple error-handling mechanism through the built-in error interface:
type error interface {
Error() string
}
Functions typically return an error as their last return value. Use errors.New to create error values:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
Custom error example:
package main
import "fmt"
type DivideError struct {
dividee int
divider int
}
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
func main() {
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 =", result)
}
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is:", errorMsg)
}
}
2.17 Concurrency
Go has built-in concurrency support through goroutines. Simply prefix a function call with the go keyword to run it in a new goroutine.
A goroutine is a lightweight thread managed by the Go runtime. All goroutines in a program share the same address space.
Syntax: go functionName(args)
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() {
go say("world")
say("hello")
}
2.18 Channels
A channel is a typed conduit for passing data between goroutines. The <- operator specifies the channel direction (send or receive). If no direction is given, the channel is bidirectional.

Declare a channel with the chan keyword. Channels must be created before use:

By default, channels are unbuffered: the sender blocks until the receiver is ready, and vice versa.
Example – summing numbers with two goroutines:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c
fmt.Println(x, y, x+y)
}
- Buffered channels
You can create a buffered channel by passing a capacity as the second argument to make:

Buffered channels decouple the sender and receiver: the sender can write to the buffer without the receiver being immediately ready. However, once the buffer is full, the sender blocks until the receiver drains some values.
Key rule: An unbuffered channel blocks the sender until the receiver reads. A buffered channel blocks the sender only when the buffer is full. The receiver always blocks until a value is available.
package main
import "fmt"
func main() {
// Buffered channel with capacity 2
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
- Iterating and closing channels
Use range to read from a channel until it is closed:

When no more data will be sent, close the channel with close().
package main
import "fmt"
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range reads from c until it is closed
for i := range c {
fmt.Println(i)
}
}
Comments
Join the discussion — requires a GitHub account