dark corners of go

Arash Cordi
6 min readMay 22, 2021
gopher

go is an easy to learn language with purposefully limited features. but there are some lesser known features which you may not know about. i have tried to list a few of them here.

number literals

For readability, an underscore character _ may appear after a base prefix or between successive digits; such underscores do not change the literal's value.

1_000_000
0_600
0x_42_af_3d

prevent unkeyed literals

type X struct {
A, B int
_ struct{}
}

x := X{A: 1, B: 2} // OK
x := X{1, 2} // Error: too few values

note that go vet also complains about unkeyed literals.

to prevent extra padding we can move the zero field to the top of the struct:

type X1 struct {
A, B int
_ struct{}
}

type X2 struct {
_ struct{}
A, B int
}

var (
x1 *X1
x2 *X2
)

fmt.Printf(
"sizeof(X1) = %d sizeof(X2) = %d\n",
unsafe.Sizeof(*x1), unsafe.Sizeof(*x2)
) // sizeof(X1) = 24 sizeof(X2) = 16

type grouping

group syntax can also be used for types:

type (
T1 struct{}
T2 struct{}
)

pass multiple return value to another function

generally you can’t do this except for this special case:

if the return values of a function or method g are equal in number and individually assignable to the parameters of another function or method f, then the call f(g(parameters_of_g)) will invoke f after binding the return values of g to the parameters of f in order. The call of f must contain no parameters other than the call of g, and g must have at least one return value.

func add2(x, y int) (int, int) {
return x+2, y+2
}
func add4(x, y int) (int, int) {
return x+4, y+4
}
func main() {
// OK x = 7, y = 8
x, y := add4(add2(1, 2))
// ERROR multiple-value add2() in single-value context
fmt.Printf("x = %d y = %d", add2(1, 2))
}

interface composition

interfaces can be union of other interfaces just like struct composition.

type I1 interface {
X() int
}

type I2 interface {
Y()
}

type I3 interface {
I1
I2
Z() int
}

methods with same name must have similar signatures.

type I1 interface {
X() int
}

type I2 interface {
X()
}

type I3 interface {
I1
I2 // ERROR: duplicate method 'X'
}

typed iota

you can specify a type for iota

const (
_ = iota
testvar // will be int
)

vs

type myType int
const (
_ myType = iota
testvar // will be myType
)

type alias vs type definition

this is a type alias:

type SameMutex = sync.Mutex

this is a new type:

type NewMutex sync.Mutex

SameMutex is just another name for Mutex and will have all the functionality of Mutex, but NewMutex being a different type, does not inherit any methods bound to the given type:

m1 := SameMutex{}
m2 := NewMutex{}
m1.Lock() // OK
m2.Lock() // ERROR: Unresolved reference 'Lock'

package init()

a package may have one or multiple init() functions which are called during package initialization after all package-level variables are initialized.

importing a package for side effects runs this function:

import _ "net/http/pprof"

there are a few rules about the order of variable evaluation; in the sample code below, x2 is initialized before init() so the output will be 0.

package mainvar X intfunc init() {
X = 1
}
var x2 = 2 * Xfunc main() {
println(x2)
}

special packages and directories

/internal
internal package is used to make specific packages unimportable. a package .../a/b/c/internal/d/e/f can be imported only by code in the directory tree rooted at .../a/b/c. It cannot be imported by code in .../a/b/g or in any other repository. this layout pattern is enforced on all repositories by go compiler since go 1.5

/vendor
you can use /vendor to put external package dependencies inside application directory. these dependencies can be managed manually or by go mod.

The go mod vendor command will create the /vendor directory for you. Note that you might need to add the -mod=vendor flag to your go build command if you are not using Go 1.14 where it's on by default.

some go tools like go list ignore this directory. others like gofmt and golint don’t. you can use a command like this if you wish to exclude this directory:

go list ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status

testdata

The go tool will ignore a directory named “testdata”, making it available to hold ancillary data needed by the tests.

when you run go test, for each package in scope, the test binary will be executed with its working directory set to the source directory of the package under test.

putting these together, locating test data from your test code is simply:

os.Open("testdata/data.json")

type hint

you can pass a zero value of a type to a function to inform it of the type you desire:

type A struct {
X string
Y string
}

func Decode(useThisType interface{}, binaryData []byte) interface{} {
json.Unmarshal(binaryData, useThisType)
return useThisType
}

func main() {
value := Decode(&A{}, []byte(`{"X":"10", "Y":"20"}`))
fmt.Println(value)
}

detailed output with %v

use %v of printf family functions to print data with sufficient details:

type A struct {
X string
Y string
}

func main() {
fmt.Printf("%v", A{X: "10", Y:"20"})
fmt.Printf("%+v", A{X: "10", Y:"20"})
fmt.Printf("%#v", A{X: "10", Y:"20"})
}

output:

{10 20}
{X:10 Y:20}
main.A{X:"10", Y:"20"}

check this cheat sheet for other annotation verbs.

Example functions

func Example() {...}
func ExampleT() {...}

functions with these signatures can be used to place usage examples in godoc. Examples should be placed in a file with a _test suffix. you can document the output of the example by adding an output comment at its end.

func ExampleExamples_output() {
fmt.Println("Hello")
// Output: Hello
}

also, if output is provided go test will run the example and compares it’s output with the output specified in the comment section and report the example function as passed if they match. if no output is provided go test will only compile it.

error wrapping

since go 1.13 errors can wrap another error creating a hierarchy of errors.

%w directive in fmt.Errorf is used for wrapping an error:

e1 := errors.New("error one")
e2 := fmt.Errorf("e2 : %w", e1)
fmt.Println(e2)

output:

e2 : error one

use Unwrap() to unwrap errors:

e3 := errors.Unwrap(e2)
fmt.Println(e3)

output:

error one

check this post for a detailed explanation on error wrapping.

embed files

as of go 1.16 you can natively embed files using embed package

package main

import _ "embed"

func main() {
//go:embed "hello.txt"
var s string
print(s)
}

output:

Hello, Gophers!

read more about embedding here.

forcing a type to json marshal

the default type for integer values is float64 and int64 values may overflow. use this syntax to force encoding into string:

type Data struct {
ID int64 `json:"id,string"`
}

block forever

you can use an empty select to block forever:

select {
}

a common use case is when you have a single server with multiple http listeners. you can spawn each was with a goroutine in main() and block forever using select{} . this is also a good place to check os signals.

you can find other ways to block here.

comment magic

for most parts comments are just comments. but there are situations where comments may have some side effects, we have seen two of these situations so far (the embed command and example function output), here are other situations:

godoc text

godoc uses the comment block immediately before a declaration as the documentation of the declaration; the first line of the comment should start with the name of declaration:

// Object is an object
type Object struct {}

build constraints

a comment that adheres to the following rules is recognized as a build constraint by go build :

  • located at the start of the file
  • start with the prefix +build followed by one or more tags
// +build linux

this build tag tells the go compiler to use this file for linux only.

build tags can be combined with the following rules:

  • build tags starting with ! are negated
  • space-separated tags are logically OR’d
  • comma-separated tags and line-separated tags are logically AND’d

this constraint requires windows or linux :

// +build linux windows

this constraint requires both linux and 386 architecture:

// +build linux,386

a full detail of build constraints can be found here.

go generate

when you run the command go generate, the tool search for comments of the form //go:generate command arguments . this command can be anything and doesn’t have any requirements.

cgo

Cgo allows Go programs to call C code directly. comment immediately preceding #import “C” directive (AKA the preamble) will be treated as standard code and can the be accessed via the C package:

// #include <stdio.h>
//
// static void myprint(char *s) {
// printf("%s\n", s)
// }
import "C"
C.myprint(C.String("foo"))

--

--