SE-EDU
  • AB-1
  • AB-2
  • AB-3
  • AB-4
  • Collate
  • Book
  • Home
  • Contribute
  • About
  • GitHub
  • Learning Resources for Software Engineering Students ยป

    Introduction to Go

    Author(s): Cara Leong
    Reviewers: Darren Wee, Tran Tien Dat

    What Is Go?

    Go (also known as golang) is a compiled, statically-typed, garbage-collected language that has special memory safety and concurrent programming features. Born out of frustration with the available languages (e.g. C, C++, Java) and environments for systems programming, Go was conceptualized by programmers at Google who sought to create a single language that was efficient to write, build and execute. Go also supports newer developments in computing such as multicore processors and network systems.

    Why Learn Go?

    1. Go is a language built for software engineers. As it was written by software engineers at Google, Go addresses and attempts to solve some of the pain points that exist in many commonly-used languages. For instance, the language has easy dependency management, prides itself on fast builds and has many easy to use debugging, testing, and code-vetting tools. These features make Go an easy language to use for software development.

    2. Go is a useful systems-side (i.e. backend) language. As it was built with large, distributed architectures in mind, Go is useful for creating scalable server-side programs that handle multicore processors, networked systems or even large computation clusters. In other words, if you're looking to make an Android application, Go is probably not the language for you. However, if you're looking to pick up a language that is reasonably easy to learn, builds on the foundations of other common languages and creates programs that are easily scalable, then Go may be the language for you.

    3. Go is an open source project. Learning about Go and contributing to the language may be a useful experience for those interested. In addition, its source code may be useful reading for those interested in learning good practices, or simply to find out more about how the language was implemented.

    Of course, Go is not a perfect language. However, for some people, Go may be exactly the right language to pick up! If you're unconvinced about how you can learn and use Go, you can use the Go playground to write, build and execute code without installing Go on your machine.

    As it builds on the foundations set by many popular and widely-used languages such as C, C++, Java and Python, much of Go's syntax draws from existing implementations and will be familiar to programmers looking to learn an additional language. However, Go also diverges explicitly from these other languages. Listed below are some features that make Go unique!

    Benefit: Better Variable Declarations

    One immediately obvious difference between Go and other languages is that Go declares its variables in the format var variableName (variableType), with the variable's type declared to the right of the variable name, as follows:

    var name string
    name = "John Smith"
    

    This differs from many other languages, which put the variable type to the left of the variable name.

    In addition, the verbosity of a declaration statement in Go can vary. You do not need to declare the variableType of a new variable if you use an initializer, as the type of the variable will be inferred from its initialization. For instance, in the following example, the string type is optional:

    var name string = "John Smith"
    
    // alternatively
    var name = "John Smith"
    

    In addition to omitting the type when it can be inferred, you can also eliminate the keyword var when performing variable declaration by using the := short assignment statement. := acts as a shortcut to declare and immediately initialize a variable inside of a function.

    name := "John Smith"
    

    Thus, we see that there are several potential ways to declare a variable in Go. Choosing which style of variable declaration to use depends on how verbose a programmer wants to be in declaring the variable. However, the upshot of having many different degrees of verbosity is that common problems of both dynamically- and statically-typed languages can be avoided. Unlikely dynamically-typed languages, in which the type of a variable is sometimes unclear, a type in Go can always be explicitly declared to increase code clarity. On the other hand, when a variable's type can be clearly inferred, programmers can choose not to be unnecessarily verbose in their code.

    Go's syntax for more complex types such as pointers, arrays and structs is also somewhat idiosyncratic, and can be explored in this Go blog article, or with the help of this tutorial on pointers and this tutorial on structs.

    Benefit: Better Support for Concurrency

    One of Go's special features is its focus on implementing concurrency simply and well. To this end, Go's standard library comes with two features that allow for easy and maintainable concurrency.

    Goroutines

    A goroutine is a lightweight thread that executes a function concurrently with its caller. A goroutine is launched by a go statement:

    func run() {
        // does something
    }
    
    func main() {
        go run()
        executeOtherCommands()
    }
    

    In the above example, using the go keyword launches a goroutine, which executes run() concurrently with executeOtherCommands().

    Goroutines can also be started for anonymous functions:

    func foo() {
        go func(msg string) {
            fmt.Println(msg)
        }("a message") // starts a goroutine that prints "a message"
    }
    

    A goroutine is not its own thread; instead, goroutines are dynamically multiplexed onto threads as required to keep them running. In addition, goroutines start with very small stacks which makes them lightweight, so having a large number of goroutines is feasible. In practice, goroutines behave similarly to very cheap threads.

    Channels

    Channels are used to fulfill Go's philosophy on concurrent software: "don't communicate by sharing memory; share memory by communicating". In other words, Go relies on message passing between concurrently running goroutines to share information.

    Specifically, Go relies on channels to implement message passing. Channels are typed conduits that allows goroutines to communicate with each other by sending and receiving messages. Before using a channel of a specific type, we must declare and make it:

    var c chan int
    c = make(chan int)
    
    //alternatively
    c := make(chan int)
    

    Channels can transmit data of any type; thus, creating a channel that transmits channels (i.e. a chan chan) is theoretically possible and may even be useful.

    Goroutines send and receive messages through a channel using the <- operator.

    ch <- v   // Send v to channel ch.
    v := <-ch // Receive from ch, and
          // assign value to v.
    

    One useful way to think about sending and receiving data with the <- operator is that the data moves in the direction of the arrow.

    Channels can be used to synchronize execution across goroutines, since receivers block until they receive data, while senders block until the receiver or buffer receives data. In the code example below, the main goroutine waits until it receives a message from the worker goroutine that it is done before terminating.

    func worker(done chan bool) {
        fmt.Print("Working...")
        time.Sleep(time.Second)
        fmt.Println("Done working")
    
        done <- true
    }
    
    func main() {
        done := make(chan bool)
        go worker(done)
    
        <-done
        fmt.Println("Returned from work")
    }
    

    If you are interested in delving deeper into using Go's concurrency features extensively, Google developers have put out video presentations on Go's basic and advanced concurrency patterns. This code walkthrough provides an annotated example of how Go's memory-sharing principles can be applied in practice.

    Benefit: Better Error Handling

    Error handling in Go is performed using multiple returns. On any function that can fail, the function's last return type should always be of the type error. For example, the os.Open function returns a non-nil error value when it fails to open a file.

    func Open(name string) (file *File, err error)
    

    An error variable represents any value that can describe itself as a string, by implementing the following interface:

    type error interface {
        Error() string
    }
    

    When calling a method that may return an error, we check if the returns err != nil and handle the resulting error.

    func useFile() {
        f, err := os.Open("filename.ext")
        if err != nil {
            log.Fatal(err)
            return
        }
        // do something with the open *File f
    }
    

    Go's use of multiple returns for errors can be contrasted with the use of exceptions in a language like Java. Unlike exceptions, which can crash a program, errors are seen as regular values that are to be expected by programmers and handled accordingly.

    To deal with unexpected errors, Go also provides two mechanisms: panic and recover.

    • panic is similar to throwing an exception in other languages. An explicit call to panic on a function F stops the ordinary flow of execution of F at the point of the panic, executes any functions deferred by F, and returns to F's caller. To the caller, F behaves like a call to panic. It triggers a panic in F's caller, which recursively propagates up the call stack until all functions in the goroutine have returned, after which the program crashes. panic is used to fail fast on errors that cannot be handled gracefully.
    • recover regains control of a panicking goroutine. Using recover is comparable to catching an exception in C++ or Java. When used inside a deferred function, a call to recover captures the value returned by panic and resumes normal execution.

    More information on error handling can be found on the Go blog or Go wiki.

    Benefit: defer Execution

    As opposed to traditional control flow mechanisms such as if, for and switch, which execute functions immediately, Go's defer keyword pushes a function call to a list, and only executes all functions on the list after the surrounding function returns. In the following example, defer adds two print functions to the stack of deferred functions. After foo finishes executing, the deferred functions are executed in last-in-first-out order.

    func foo() {
        defer fmt.Println("This gets printed third")
        defer fmt.Println("This gets printed second")
        fmt.Println("This gets printed first")
    }
    

    defer is frequently used for clean-up actions, such as to close files. Deferred functions run on panicking goroutines as well, which makes them useful for recovering from panic.

    Benefit: Good Support for Interfaces

    Although Go has types and methods and allows pseudo-object-oriented style of programming, type hierarchy does not exist in Go. Instead, Go uses interfaces to specify methods that types should implement, favouring composition over inheritance. Types do not need to explicitly specify which interfaces are implemented. Instead, types implement interfaces by implementing the methods in the interface.

    In the example below, the Rectangle type implements the interface TwoDimensional by implementing the methods area() and perim() that are specified in the interface. Thus, instances of Rectangle can be used as arguments to price.

    Meanwhile, although Circle implements perim(), it does not implement area(). Since it does not implement all the methods in the TwoDimensional interface, Circle does not implement TwoDimensional. Thus, instances of Circle cannot be used as arguments to price.

    type TwoDimensional interface {
        area() float64
        perim() float64
    }
    
    type Rectangle struct {
        width, height float64
    }
    
    type Circle struct {
        radius float64
    }
    
    func (r Rectangle) area() float64 {
        return r.width * r.height
    }
    
    func (r Rectangle) perim() float64 {
        return r.width*2 + r.height*2
    }
    
    func (c Circle) perim() float64 {
        return 2 * math.Pi * c.radius
    }
    
    func price(t TwoDimensional) float64 {
        return t.area() * 3.5
    }
    
    func main(){
        c := Cirlce{5}
        fmt.Println(price(c))
    }
    

    Go checks that types satisfy the required interfaces at compile time. For example, the above main function specifies that Circle satisfies the TwoDimensional interface, when in actuality it does not. Thus, when you try to compile your program, you will get a compile-time error drawing your attention to the problem.

    cannot use c (type Circle) as type TwoDimensional in argument to price:
        Circle does not implement TwoDimensional (missing area method)
    

    One benefit of using a system where interface implementations need not be stated in the source code is that methods can be attached to types that you didn't write. In other words, you can extend a type to implement an interface without access to its source code by simply implementing the interface's method in your own code.

    Some resources to get started with Go interfaces include this blog post introducing Go interfaces and code examples on how interfaces (including the empty interface) are used in practice. For a more extensive look at how object-oriented programming is done in Go, you can refer to this comparison of Go's OOP style with that of other languages, Go's official FAQ on OOP, or this tutorial on OOP in Go.

    Benefit: Canonical Coding Style

    Formatting in Go is enforced by running go fmt, which will align your source code with the language-wide standard style of indentation and vertical alignment. Thus, given the following code:

    type T struct {
        name string // name of the object
        value int // its value
    }
    

    Running $ go fmt in the same directory as the source file will line up comments and correct the source code's indentation:

    type T struct {
        name  string // name of the object
        value int    // its value
    }
    

    Variations on go fmt may be of use, and can be found in the Go documentation.

    Go also enforces good coding practices, for instance, by refusing to build projects that declare of unused variables or imports. Such enforcement, along with a clear, unified and extensive treatise on coding conventions in Go, have manifested in a reasonably stable Go coding style.

    How to Get Started with Go?

    Go provides its own installation guide and an interactive tour of Go. These are useful and highly comprehensive resources for programmers looking to learn the syntax and style of Go. For those who prefer to read existing code examples, Go by Example is a collection of code samples covering a wide variety of features in Go, and includes line-by-line explanations of the code. For those looking for a quick crash course on Go syntax, the Learn X In Y Minutes Go cheatsheet may also be a good starting point.

    Where to Go from Here?

    Go's development team is heavily involved in documenting and growing the Go language and community. If you are keen to learn more about Go, here are some resources to help you get started:

    • The Go FAQ - answers common questions about the language's history, usage, design and more
    • Go's documentation - a good starting point, contains links to official information about Go
    • The Go Blog - features news and in-depth articles about Go by the Go team and guests