Get started with Go
Andrew Gerrand
Andrew Gerrand
Go is a new, general-purpose programming language.
"Go is a wise, clean, insightful, fresh thinking approach to the greatest-hits subset of the well understood."
- Michael T. Jones
gccgo
Put this code into hello.go
:
package main import "fmt" func main() { fmt.Println("Greetings, fellow gopher") }
Run the program:
$ go run hello.go
The go
tool is the standard tool for building, testing, and installing Go programs.
Compile and run hello.go
:
$ go run hello.go
Run zip
tests:
$ go test archive/zip
Build and format the files in the current directory:
$ go build $ go fmt
Fetch and install websocket
:
$ go get code.google.com/p/go.net/websocket
The go
tool derives build instructions from Go source code.
There's no need to write and maintain build scripts.
For this to work, some prescribed directory structure, known as a workspace, is required.
workspace/ bin # executable binaries pkg # compiled object files src # source code
Create your workspace now.
$ mkdir -p $HOME/gocode/src
(The bin
and pkg
sub-directories will be created by the go
tool.)
Tell the go
tool where your workspace is by setting the GOPATH
environment variable:
export GOPATH=$HOME/gocode
You may also want to add the bin
sub-directory of your workspace to your PATH
:
export PATH=$PATH:$GOPATH/bin
This lets you run your Go programs without specifying their full path.
(You may want to put these export
commands in the .bash_profile
file in your home directory.)
Choose a special place for your Go code.
I use "github.com/nf"
, the root of my GitHub account (useful with `go get`).
$ mkdir -p $GOPATH/src/github.com/nf
Create a hello
directory in your namespace and copy hello.go
there:
$ mkdir $GOPATH/src/github.com/nf/hello $ cp hello.go $GOPATH/src/github.com/nf/hello
Now you can build install the hello program with the go
tool:
$ go install github.com/nf/hello
This builds an executable named hello
, and installs it to the bin
directory of your workspace.
$ $GOPATH/bin/hello Hello, fellow gopher
A command-line program that fetches and displays the latest headlines from the golang
page on Reddit.
The program will:
- make an HTTP request to the Reddit API,
- decode the JSON response into a Go data structure, and
- print each link's title, URL, and number of comments.
To get started, create directory inside your namespace called reddit
:
$ mkdir $GOPATH/src/github.com/nf/reddit
This is where you will put your Go source files.
13
This program makes an HTTP request to the Reddit API and copies its response to standard output. Put this in a file named main.go
inside your reddit
directory.
package main import ( "io" "log" "net/http" "os" ) func main() { resp, err := http.Get("http://reddit.com/r/golang.json") if err != nil { log.Fatal(err) } if resp.StatusCode != http.StatusOK { log.Fatal(resp.Status) } _, err = io.Copy(os.Stdout, resp.Body) if err != nil { log.Fatal(err) } }
All Go code belongs to a package.
package main
Go programs begin with function main
inside package main
.
The import declaration specifies the file's dependencies.
import ( "io" "log" "net/http" "os" )
Each string is an import path. It tells the Go tools where to find the package.
These packages are all from the Go standard library.
16func main() { resp, err := http.Get("http://reddit.com/r/golang.json") if err != nil { log.Fatal(err) } if resp.StatusCode != http.StatusOK { log.Fatal(resp.Status) } _, err = io.Copy(os.Stdout, resp.Body) if err != nil { log.Fatal(err) } }
This is a function declaration. The main function takes no arguments and has no return values.
17func main() { resp, err := http.Get("http://reddit.com/r/golang.json") if err != nil { log.Fatal(err) } if resp.StatusCode != http.StatusOK { log.Fatal(resp.Status) } _, err = io.Copy(os.Stdout, resp.Body) if err != nil { log.Fatal(err) } }
Call the Get
function from the http
package, passing the URL of the Reddit API as its only argument.
Declare two variables (resp
and err
) and give them the return values of the function call. (Yes, Go functions can return multiple values.) The Get
function returns *http.Response
and an error
values.
func main() { resp, err := http.Get("http://reddit.com/r/golang.json") if err != nil { log.Fatal(err) } if resp.StatusCode != http.StatusOK { log.Fatal(resp.Status) } _, err = io.Copy(os.Stdout, resp.Body) if err != nil { log.Fatal(err) } }
Compare err
against nil
, the zero-value for the built-in error
type.
The err
variable will be nil if the request was successful.
If not, call the log.Fatal
function to print the error message and exit the program.
func main() { resp, err := http.Get("http://reddit.com/r/golang.json") if err != nil { log.Fatal(err) } if resp.StatusCode != http.StatusOK { log.Fatal(resp.Status) } _, err = io.Copy(os.Stdout, resp.Body) if err != nil { log.Fatal(err) } }
Test that the HTTP server returned a "200 OK" response.
If not, bail, printing the HTTP status message ("500 Internal Server Error", for example).
20func main() { resp, err := http.Get("http://reddit.com/r/golang.json") if err != nil { log.Fatal(err) } if resp.StatusCode != http.StatusOK { log.Fatal(resp.Status) } _, err = io.Copy(os.Stdout, resp.Body) if err != nil { log.Fatal(err) } }
Use io.Copy
to copy the HTTP response body to standard output (os.Stdout
).
package io func Copy(dst Writer, src Reader) (written int64, err error)
The resp.Body
type implements io.Reader
and os.Stdout
implements io.Writer
.
The Reddit API returns JSON data like this:
{"data": {"children": [ {"data": { "title": "The Go homepage", "url": "https://go.dev/", ... }}, ... ]}}
Go's json
package decodes JSON-encoded data into native Go data structures. To decode the API response, declare some types that reflect the structure of the JSON data:
type Item struct { Title string URL string } type Response struct { Data struct { Children []struct { Data Item } } }
Instead of copying the HTTP response body to standard output
_, err = io.Copy(os.Stdout, resp.Body)
we use the json package to decode the response into our Response data structure.
r := new(Response) err = json.NewDecoder(resp.Body).Decode(r)
Initialize a new Response
value, store a pointer to it in the new variable r
.
Create a new json.Decoder
object and decode the response body into r
.
As the decoder parses the JSON data it looks for corresponding fields of the same names in the Response
struct. The "data"
field of the top-level JSON object is decoded into the Response
struct's Data
field, and JSON array "children"
is decoded into the Children
slice, and so on.
for _, child := range r.Data.Children { fmt.Println(child.Data.Title) }
Iterate over the Children
slice, assigning the slice value to child
on each iteration.
The Println
call prints the item's Title
followed by a newline.
So far, all the action happens in the main function.
As the program grows, structure and modularity become important.
What if we want to check several subreddits? Or share this functionality with another program?
Create a function named Get
that takes the name of subreddit, makes the API call, and returns the items from that subreddit.
func Get(reddit string) ([]Item, error) {
Get
takes a string, reddit
, and returns a slice of Item
and an error
value.
func Get(reddit string) ([]Item, error) { url := fmt.Sprintf("http://reddit.com/r/%s.json", reddit) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } r := new(Response) err = json.NewDecoder(resp.Body).Decode(r) if err != nil { return nil, err } items := make([]Item, len(r.Data.Children)) for i, child := range r.Data.Children { items[i] = child.Data } return items, nil }
Use fmt.Sprintf
to construct the request URL from the provided reddit
string.
func Get(reddit string) ([]Item, error) { url := fmt.Sprintf("http://reddit.com/r/%s.json", reddit) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } r := new(Response) err = json.NewDecoder(resp.Body).Decode(r) if err != nil { return nil, err } items := make([]Item, len(r.Data.Children)) for i, child := range r.Data.Children { items[i] = child.Data } return items, nil }
Exiting the function, return a nil slice and a non-nil error value, or vice versa.
29func Get(reddit string) ([]Item, error) { url := fmt.Sprintf("http://reddit.com/r/%s.json", reddit) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } r := new(Response) err = json.NewDecoder(resp.Body).Decode(r) if err != nil { return nil, err } items := make([]Item, len(r.Data.Children)) for i, child := range r.Data.Children { items[i] = child.Data } return items, nil }
The response's Status
field is just a string; use the errors.New
function to convert it to an error
value.
func Get(reddit string) ([]Item, error) { url := fmt.Sprintf("http://reddit.com/r/%s.json", reddit) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } r := new(Response) err = json.NewDecoder(resp.Body).Decode(r) if err != nil { return nil, err } items := make([]Item, len(r.Data.Children)) for i, child := range r.Data.Children { items[i] = child.Data } return items, nil }
Defer a call to the response body's Close
method, to guarantee that we clean up after the HTTP request. The call will be executed after the function returns.
func Get(reddit string) ([]Item, error) { url := fmt.Sprintf("http://reddit.com/r/%s.json", reddit) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } r := new(Response) err = json.NewDecoder(resp.Body).Decode(r) if err != nil { return nil, err } items := make([]Item, len(r.Data.Children)) for i, child := range r.Data.Children { items[i] = child.Data } return items, nil }
Use the make function to allocate an Item
slice big enough to store the response data.
func Get(reddit string) ([]Item, error) { url := fmt.Sprintf("http://reddit.com/r/%s.json", reddit) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } r := new(Response) err = json.NewDecoder(resp.Body).Decode(r) if err != nil { return nil, err } items := make([]Item, len(r.Data.Children)) for i, child := range r.Data.Children { items[i] = child.Data } return items, nil }
Iterate over the response's Children
slice, assigning each child's Data
element to the corresponding element in the items slice.
In the main
function, replace the http request and JSON decoding code with a single call to Get
.
func main() { items, err := Get("golang") if err != nil { log.Fatal(err) } for _, item := range items { fmt.Println(item.Title) } }
The print loop becomes clearer, too.
However, it's not very useful to print only the title of the items. Let's address that.
34
The fmt
package knows how to format the built-in types, but it can be told how to format user-defined types, too.
When you pass a value to the fmt.Print
functions, it checks to see if it implements the fmt.Stringer
interface:
type Stringer interface { String() string }
Any type that implements a `String() string` method is a Stringer
, and the fmt
package will use that method to format values of that type.
A method declaration is just like a function declaration, but the receiver comes first.
Here's a String
method for the Item
type that returns the title, a newline, and the URL:
func (i Item) String() string { return fmt.Sprintf("%s\n%s", i.Title, i.URL) }
To print the item we just pass it to Println, which uses the provided String
method to format the Item
.
fmt.Println(item)
Let's go a step further. One way to judge how interesting a link might be is by the discussion surrounding it. Let's display the number of comments for each Item
as well.
{ "title": "The Go homepage", "url": "https://go.dev/", "num_comments": 10 }
Update the Item
type to include a Comments
field:
type Item struct { Title string URL string Comments int `json:"num_comments"` }
The new Comments
field has a "struct tag", a string that annotates the field. Go code can use the reflect
package to inspect this information at runtime.
This tag, json:"num_comments"
, tells the json
package to decode the "num_comments"
field of the JSON object into the Comments
field (and the reverse, when encoding).
Now the String
method can be a little more complex:
func (i Item) String() string { com := "" switch i.Comments { case 0: // nothing case 1: com = " (1 comment)" default: com = fmt.Sprintf(" (%d comments)", i.Comments) } return fmt.Sprintf("%s%s\n%s", i.Title, com, i.URL) }
Observe that, unlike some languages, Go's switch statements do not fall through by default.
Now when we run our program we should see a nicely formatted list of links.
39This is useful code. Let's organize it to make it more accessible to others by putting it in an importable package.
Create a new directory inside your reddit
directory named geddit
, and copy your main.go
file there.
reddit
is the name of the library and geddit
as that of the command-line client.
$ cd $GOPATH/src/github.com/nf/reddit $ mkdir geddit $ cp main.go geddit/
Rename the main.go
inside the reddit
directory to reddit.go
. (Not necessary; just a convention.)
$ mv main.go reddit.go
Change the package statement at the top of reddit.go
from `package main` to `package reddit`.
It is convention that the package name be the same as the last element of the import path.
The convention makes packages predictable to use:
import "github.com/nf/reddit" func foo() { r, err := reddit.Get("golang") // "reddit" here is the package name // ... }
The only strict requirement is that it must not be `package main`.
Also remove the main
function from reddit.go
, and any unused package imports. (The compiler will tell you which packages are unused.)
The reddit.go
file now looks like this:
package reddit import ( // omitted ) type Response struct { // omitted } type Item struct { // omitted } func (i Item) String() string { // omitted } func Get(reddit string) ([]Item, error) { // omitted }
Edit the geddit/main.go
file to remove the Get
, Item
, and Response
declarations, import the reddit
package, and use the reddit.
prefix before the Get
invocation:
package main import ( "fmt" "github.com/nf/reddit" "log" ) func main() { items, err := reddit.Get("golang") if err != nil { log.Fatal(err) } for _, item := range items { fmt.Println(item) } }
Godoc
is the Go documentation tool. It reads documentation directly from Go source files. It's easy to keep documentation and code in sync when they live together in the same place.
Here's our reddit package when viewed from godoc
:
$ godoc github.com/nf/reddit PACKAGE package reddit import "github.com/nf/reddit" FUNCTIONS func Get(reddit string) ([]Item, error) TYPES type Item struct { Title string URL string Comments int `json:"num_comments"` } func (i Item) String() string type Response struct { // etc
First, hide the Response
type by renaming it to response
.
In Go, top-level declarations beginning with an uppercase letter are "exported" (visible outside the package). All other names are private and inaccessible to code outside the package.
To document the remaining visible names, add a comment directly above their declarations:
// Item describes a Reddit item. type Item struct {
// Get fetches the most recent Items posted to the specified subreddit. func Get(reddit string) ([]Item, error) {
Most importantly, document the package itself by adding a comment to the package clause:
// Package reddit implements a basic client for the Reddit API. package reddit
Don't worry about documenting the String
method, as all Go programmers should be familiar with it and its purpose.
The godoc
output for our revised package:
PACKAGE package reddit import "github.com/nf/reddit" Package reddit implements a basic client for the Reddit API. FUNCTIONS func Get(reddit string) ([]Item, error) Get fetches the most recent Items posted to the specified subreddit. TYPES type Item struct { Title string URL string Comments int `json:"num_comments"` } Item describes a Reddit item. func (i Item) String() string
Use the go tool to automatically check out and install Go code:
$ go get github.com/nf/reddit/geddit
This checks out the repository to $GOPATH/src/github.com/nf/reddit
and installs the binary to $GOPATH/bin/geddit
. The bin
directory is in my PATH, so I can now run:
$ geddit
The `go get` command can fetch code from
as well as arbitrary Git, Mercurial, Subversion, and Bazaar repositories.
51Some ideas:
Learn Go:
Documentation and articles:
Standard library reference:
53