This script will get the lastest version of Go and install it in /usr/local
:
#!/bin/bash
# Upgrades the Go binary to the version specified as the first argument passed
# to this script.
# Check if the version argument was passed
if [ -z "$1" ]; then
echo "Usage: $0 <go-version>"
echo "Example: $0 1.24.4"
exit 1
fi
VERSION="$1"
TARBALL="go${VERSION}.linux-amd64.tar.gz"
URL="https://go.dev/dl/${TARBALL}"
echo "Removing previous installation from /usr/local/go..."
rm -rf /usr/local/go
echo "Downloading go tarball from $URL..."
wget "$URL"
echo "Extracting the tarball to /usr/local..."
tar -C /usr/local -xzf "${TARBALL}"
echo "Cleaning up (deleting the tarball)..."
rm -v ${TARBALL}
echo
echo
echo "Verify the installation with 'go version'"
-bench
and -benchmem
go test -cover
to check your test coverage.https://go.dev/doc/modules/gomod-ref
If you plan to distribute your application, you need to tell others where your code is available for download. Thats what go mod
does–it gives the name of the module and the download URL:
go mod init <path/to/module-name.com>
When you write tests, you are using the compiler as a feedback mechanism. Here is the feedback loop:
This makes sure that you are writing tested code with relative tests that are easier to debug when they fail.
https://pkg.go.dev/fmt#hdr-Printing
A method is a function with a reciever. Its declaration binds the method name (the identifier) to a method and associates that method with the receiver’s base type.
A method has to be invoked on an instance of this base type. When you call the method on an instance, you get a reference to the instance’s data through the receiver variable. The receiver variable is similar to this
in other programming languages. By convention, the receiver variable is the first letter of the Type.
// base type
type Rectangle struct {
Width float64
Height float64
}
// receiver gives access to its data
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
You can also make type from an existing type. This helps make the code more domain-speific. You can also add methods and interfaces to these new types:
type Bitcoin int
bc := Bitcoin(10)
// Stringer method
func (b Bitcoin) String() string {
return fmt.Sprintf("%d BTC", b)
}
The
Stringer
method lets you define what your type looks like when its output as a string.
An interface decouples functions from concrete types. By decoupling types from behavior, an interface helps you declare the behavior that you need rather than the type you need.
Table tests are useful for testing intefaces. For example, when you implement an interface with a new type, you can add the new type as a test case to the table test.
When you create a table test, name the fields in the anonymous struct, and include a name
field so you can name each test. This helps identify specific tests in the output. The anonymous struct should include the following:
When you run the tests with a for...range
loop, use name
field as the test name for each t.Run
subtest:
func TestArea(t *testing.T) {
areaTests := []struct {
name string
shape Shape
hasArea float64
}{
{name: "Rectangle", shape: Rectangle{12, 6}, hasArea: 72.0},
{name: "Circle", shape: Circle{10}, hasArea: 314.1592653589793},
{name: "Triangle", shape: Triangle{12, 6}, hasArea: 36.0},
}
for _, tt := range areaTests {
t.Run(tt.name, func(t *testing.T) {
got := tt.shape.Area()
if got != tt.hasArea {
t.Errorf("#%#v got %g want %g", tt.shape, got, tt.hasArea)
}
})
}
}
When you use pointers, you don’t have to dereference the pointer in the function. For example:
func (w *Wallet) Balance() int {
return w.balance // not return (*w).balance
}
Here, you can return the correct wallet instance without dereferencing. (you can also dereference the pointer, but it is not necessary.) The creators of Go didn’t like the syntax, so they don’t make us dereference (they are automatically dereferenced).
Helpful error linter:
go install github.com/kisielk/errcheck@latest
// run in working dir
errcheck .
Link: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
This creates an error with your custom error message:
errors.New("error msg")
You can also convert an error to a string message to confirm that it is the error that you want:
// tested function
func (w *Wallet) Withdraw(amount Bitcoin) error {
if amount > w.balance {
return errors.New("cannot withdraw, insufficient funds")
}
w.balance -= amount
return nil
}
assertError := func(t testing.TB, got error, want string) {
t.Helper()
if got.Error() != want { // compare got string to want string
t.Errorf("got %q, want %q", got, want)
}
}
Handling errors like this is tedious. If you want to change the error message, you have to change it in multiple places.
Its much easier to define a meaningful error value (errors are values in Go) that you can reference throughout your codebase:
var ErrInsufficientFunds = errors.New("cannot withdraw, insufficient funds")
func (w *Wallet) Withdraw(amount Bitcoin) error {
if amount > w.balance {
return ErrInsufficientFunds
}
...
}
Create a type for your errors and implement the error
interface. Then, you can create constant
errors, which makes them more reusable and immutable:
const (
ErrNotFound = DictionaryErr("could not find the word you were looking for")
ErrWordExists = DictionaryErr("cannot add word because it already exists")
)
type DictionaryErr string
func (e DictionaryErr) Error() string {
return string(e)
}
An idiomatic way to check your errors is with the .Is()
or .As()
error methods, but you can also use a switch
statement:
func (d Dictionary) Delete(word string) error {
_, err := d.Search(word)
switch err {
case ErrNotFound:
return ErrWordDoesNotExist
case nil:
delete(d, word)
default:
return err
}
return nil
}
Maps can return two values. Idiomatically, you can check if a map contains a value with the ok
keyword:
func (d Dictionary) Search(word string) (string, error) {
definition, ok := d[word]
if !ok {
return "", errors.New("could not find the word you were looking for")
}
return definition, nil
}
You can mutate a map without passing their address. This is because a map is a pointer to a runtime.hmap structure. When you copy a map, you aren’t copying the data structure, you’re copying the pointer to the data structure.
All this means that you can initialize a nil
map, but you DO NOT want to do that because it results in a runtime panic. Initialize an empty map or use the make
keyword:
var dictionary = map[string]string{}
// OR
var dictionary = make(map[string]string)
If you add a value with a key that already exists, the map does not create duplicate entries. It overwrites the old value with the new value.
You can delete items from a map with the built-in function delete
. It takes the map and the key to remove, and it returns nothing:
delete(mapName, key)
Dependency injection means thta you can inject (pass in) a dependency at runtime. The dependencies–like a DB connection or printing to STDOUT–is passed into the function rather than being hardcoded. Dependency injection lets you write general-purpose functions, and it faciliates testing.
One way to do this is to pass an interface rather than a concrete type:
func Greet(writer io.Writer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}
Use bytes.Buffer{}
to test anything with the io.Writer
interface.
A testing spy is a test double (like a mock or stub) that records information about how it’s used, so your test can make assertions about:
What makes it a spy:
Unlike mocks, spies typically don’t enforce expectations upfront (e.g., “this method must be called once”). You test after the fact.
Use a spy to test side effects and interactions.
Use concurrency for the part of the code that you want to run faster. The other parts should run linearly.
A race condition is a bug that occurs when software is dependent on the timing and sequence of events. Go has a built-in race detector:
go test -race
Channels can help solve data race condiitons.
// send expression
resultChannel <- result{u, wc(u)}
// receive expression
r := <-resultChannel