Skip to content

Instantly share code, notes, and snippets.

@samuelfvlcastro
Last active February 26, 2024 07:28
Show Gist options
  • Save samuelfvlcastro/ca2598665726c12da6ff28036520f839 to your computer and use it in GitHub Desktop.
Save samuelfvlcastro/ca2598665726c12da6ff28036520f839 to your computer and use it in GitHub Desktop.
GoLang Tips and Tricks

FAQ

Q: pointers vs values (in general) ?

A:

Don't pass pointers as function arguments just to save a few bytes.

Don't pass a pointer to a string (*string) or a pointer to an interface value (*io.Reader). In both cases the value itself is a fixed size and can be passed directly.

Use pointes on large structs, or even small structs that might grow.


Q: Return the zero value vs validate and return a confirmation var ?

A:

Avoid returning values like -1 or null to signal errors or missing results

Instead require client to check an in-band error value.

A function should return an additional value to indicate whether its other return values are valid. This return value may be an error, or a boolean when no explanation is needed. It should be the final return value.

// Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)

/* ---- */

value, ok := Lookup(key)
if !ok  {
    return fmt.Errorf("no value for %q", key)
}
return Parse(value)

Q: Where to put interfaces ? A:

Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values.

The implementing package should return a concrete (usually pointer or struct) type:

That way, new methods can be added to implementations without requiring extensive refactoring.


Q: Small vs descritive variabels?

A:

Variable names in Go should be short rather than long. This is especially true for local variables with limited scope.

Prefer c to lineCount. Prefer i to sliceIndex.

More unusual things and global variables need more descriptive names.

Q: Do switch cases have fall through?

A: No but cases can be presented in comma-separated lists.

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

Q: new vs make

A:

new:

It's a built-in function that allocates memory

new(T) allocates zeroed storage for a new item of type T and returns its address, a value of type *T

In Go terminology, it returns a pointer to a newly allocated zero value of type T.

p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  SyncedBuffer

make:

Creates slices, maps, and channels only, and it returns an initialized (not zeroed) value of type T (not *T)

Under the covers, references to data structures that must be initialized before use

make([]int, 10, 100)

Allocates an array of 100 ints and then creates a slice structure with length 10 and a capacity of 100 pointing at the first 10 elements of the array

package main
import (
"fmt"
"sync"
"time"
)
func main() {
// the main goroutine **will not wait** for this one to end
go func() {
fmt.Println("Start running premature goroutine")
time.Sleep(10 * time.Minute)
fmt.Println("End running premature goroutine")
}()
fmt.Println("passed prematurely")
// With a WaitGroup the main goroutine **will** wait for this one to end
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Start running waitGroup goroutine")
time.Sleep(2 * time.Second)
fmt.Println("End running waitGroup goroutine")
}()
wg.Wait()
fmt.Println("Ended after goroutine execution")
/* out:
Ended prematurely <-- ignored the goroutine end
Start running waitGroup goroutine <-- waitgroup goroutine started
Start running premature goroutine <-- premature goroutine started
End running waitGroup goroutine <-- waitgroup goroutine endeded
Ended after waitGroup goroutine execution <-- Waited for the waitGroup but not for the premature goroutine
*/
}
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type Foo struct {
sum int
}
func main() {
var wg sync.WaitGroup
var mutex = &sync.Mutex{}
bar := Foo{sum: 0}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(time.Duration(rand.Intn(5)) * time.Second)
mutex.Lock() // <-- Lock sum from being written by more than on goroutine at the same time
bar.sum++
mutex.Unlock() // <-- Unlocks sum so it can be written by other goroutine
}()
}
wg.Wait()
fmt.Printf("Total sum: %d", bar.sum)
}
/* out:
Total sum: 10 <-- using the mutex lock/unlock no two goroutines can add to sum at the same time
creating a race condition
*/
package main
import (
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"time"
)
type Args struct {
A int
B int
}
type Calculator int
func (calc *Calculator) Add(args Args, reply *int) error {
*reply = args.A + args.B
return nil
}
func main() {
go startHttpRpcServer()
client, err := rpc.DialHTTP("tcp", "localhost:1234") // <-- connects to an HTTP RPC server at the specified network address listening on the default HTTP RPC path.
if err != nil {
log.Fatal("dialing:", err)
}
args := Args{5, 3}
var reply int
err = client.Call("Calculator.Add", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Calculation %d+%d=%d\n", args.A, args.B, reply)
}
func startHttpRpcServer() {
func() {
calculator := new(Calculator)
rpc.Register(calculator) // <-- Register publishes the receiver's methods in the DefaultServer.
rpc.HandleHTTP() // <-- Registers an HTTP handler for RPC messages
listener, e := net.Listen("tcp", ":1234") // <-- creates an tcp listener on localhost
if e != nil {
log.Fatal("listen error:", e)
}
fmt.Println("Starting serving rpc")
time.Sleep(2 * time.Second)
go http.Serve(listener, nil) // <-- Starts accepting tcp connections to the listener
fmt.Println("Serving rpc")
}()
}
package main
import (
"fmt"
"time"
)
func main() {
codeChan := make(chan int, 1)
codeChan <- 2
fmt.Printf("Initial code: %d\n", <-codeChan)
go func() {
time.Sleep(2 * time.Second)
fmt.Println(" Done after 2 seconds...")
codeChan <- 3
}()
// The channel receiver waits until the chan receives a value to continue
fmt.Printf("Final code: %d\n", <-codeChan)
/* out:
Initial code: 2
Done after 2 seconds...
Final code: 3
*/
}
package main
import (
"fmt"
)
func main() {
/*
(1) Unbuffered channel
For unbuffered channel, the sender will block on the channel until the receiver
receives the data from the channel, whilst the receiver will also block on the channel
until sender sends data into the channel.
*/
unbufChan := make(chan bool)
go func(unbufChan chan bool) {
unbufChan <- true
unbufChan <- false
unbufChan <- true
unbufChan <- true
close(unbufChan) // <-- The channel needs to be closed to signal it to be unblocked
}(unbufChan)
for i := 0; i < 10; i++ { // <-- The closed channel can still be read from, after all passed values are read it returns the nil value of the chan type
fmt.Println(<-unbufChan)
}
fmt.Println("\n...next example...\n")
/*
(2) Buffered channel
Compared with unbuffered counterpart, the sender of buffered channel will block when
there is no empty slot of the channel, while the receiver will block on the channel when it is empty.
*/
bufChan := make(chan bool, 2)
bufChan <- true
bufChan <- false
for i := 0; i < 2; i++ {
fmt.Println(<-bufChan)
}
}
/* out:
true
false
true
true
false
false
false
false
false
false
...next example...
true
false
*/
package main
import (
"fmt"
)
type Car struct {
name string
}
//SetName set the name of the **copy** of car
func (car Car) SetName(name string) {
car.name = name
}
type Moto struct {
name string
}
//SetName set the name on the **pointer receiver** of mo
func (mo *Moto) SetName(name string) {
mo.name = name
}
func main() {
alfa := Car{
name: "Mito",
}
// method as a **value receiver**, name will not change on the struct
alfa.SetName("giulietta")
honda := Moto{
name: "Rebel",
}
// method as a **pointer receiver**, name will change on the struct
honda.SetName("Neo Cafe")
fmt.Printf("alfa: %s\nhonda: %s", alfa.name, honda.name)
/* out:
alfa: Mito <-- did not change
honda: Neo Cafe <-- did change
*/
}
package main
import (
"context"
"fmt"
"sync"
)
var jobs = map[string]string{
"Job1": "done",
"Job2": "done",
"Job3": "done",
"Job4": "error", // <-- this job will always give an "error"
"Job5": "done",
}
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
resultsChan := make(chan string, len(jobs)) // <-- buffered channels make it so
errChan := make(chan error, 1)
for jobName, jobStatus := range jobs {
wg.Add(1)
go func(j string, s string) {
defer wg.Done()
select {
case <-ctx.Done():
return
default:
}
if s == "done" {
resultsChan <- fmt.Sprintf("%s status: %s", j, s)
}
if s == "error" {
errChan <- fmt.Errorf("%s status: %s", j, s)
cancel()
return
}
}(jobName, jobStatus)
}
wg.Wait()
if ctx.Err() != nil {
err := <-errChan
fmt.Printf("Errors=> %s\n", err.Error())
return
}
for i := 0; i < len(jobs); i++ {
fmt.Printf("Results=> %s\n", <-resultsChan)
}
}
/* out:
Errors=> Job4 status: error <-- One worker gives and error and the context is canceled stopping all other workers
*/
package main
import (
"fmt"
"sync"
)
var jobs = map[string]string{
"Job1": "done",
"Job2": "error",
"Job3": "done",
"Job4": "error",
"Job5": "done",
}
func main() {
var wg sync.WaitGroup
resultsChan := make(chan string, len(jobs))
errChan := make(chan error, len(jobs))
for jobName, jobStatus := range jobs {
wg.Add(1)
go func(j string, s string) {
defer wg.Done()
if s == "done" {
resultsChan <- fmt.Sprintf("%s status: %s", j, s)
}
if s == "error" {
errChan <- fmt.Errorf("%s status: %s", j, s)
return
}
}(jobName, jobStatus)
}
wg.Wait()
close(errChan) <-- Channels need to be closed
close(resultsChan) <-- Channels need to be closed
for i := 0; i < len(jobs); i++ {
if err := <-errChan; err != nil {
fmt.Printf("Errors=> %s\n", err)
}
if results := <-resultsChan; results != "" {
fmt.Printf("Results=> %s\n", results)
}
}
}
/* out:
Errors=> Job4 status: error
Results=> Job3 status: done
Errors=> Job2 status: error
Results=> Job5 status: done
Results=> Job1 status: done
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment