Skip to content

Instantly share code, notes, and snippets.

@danhodge
Last active November 13, 2024 14:39
Show Gist options
  • Save danhodge/f234a6baf9da695c9ddd17f831d9dc2a to your computer and use it in GitHub Desktop.
Save danhodge/f234a6baf9da695c9ddd17f831d9dc2a to your computer and use it in GitHub Desktop.
Go Notes
// types and literals
// defines a new type (note - this is not a type alias, type aliases are a different thing)
type Thing int32
// this function only accepts arguments of type Thing, int32 arguments are not allowed by the compiler
func justThings(t Thing) {
// do stuff
}
var v Thing = 2
justThings(v) // compiles
var i int32 = 7
justThings(i) // does not compile because i is an int32
justThings(9) // compiles, because of default typing: https://go.dev/blog/constants
// arrays of interfaces
// 1. Define an interface
type Thingable interface {
DoIt(s string) int
}
// 2. Define a function that operates on an array of the interface defined in step 1
func TakesThingables(t []Thingable) int {
total := 0
for i, thing := range t {
total += thing.DoIt("things") - i
}
return total
}
type Impl struct {
Value int
}
// 3. There exists an impl somewhere that conforms for the interface defined in step 1
func (i *Impl) DoIt(s string) int {
return len(s) + i.Value
}
func main() {
// 4. Here's an array of those Impls
a := []Impl{{Value: 12}, {Value: 3}}
// 5. This doesn't compile
fmt.Printf("Result 1 = %v\n", TakesThingables(a))
// 6. You can't pass an arrray of Impls into TakesThingables, you need to convert it into an array of Thingable first
aPrime := make([]Thingable, len(a))
for i, v := range a {
aPrime[i] = &v
}
fmt.Printf("Result 2 = %v\n", TakesThingables(aPrime))
}
// Get an io.ReadCloser for a string
import (
"io"
"strings"
)
rc := io.NopCloser(strings.NewReader("string"))
// implement a one-function interface with a function
// Here's the interface
type Doer interface {
DoIt(v int) string
}
// 1. Add a function type
type DoerFunc func(v int) string
// 2. Implement the interface function on the function type
func (f DoerFunc) DoIt(v int) string {
// f is a DoerFunc, so the only thing that you can do with it is call it - the purpose of this
// method is to turn an arbitrary DoerFunc function into an implementation of the Doer interface
return f(v)
}
// 3. Some function that takes the original interface
func TakesDoer(d Doer, i int) string {
return d.DoIt(i)
}
// 4a. Create a function of type DoerFunc, or...
var fn DoerFunc = func(v int) string {
return "VAL"
}
// 4b. just reference an existing function that matches the type
func TakesIntReturnsString(i int) string {
return fmt.Sprintf("%d", i)
}
var fn DoerFunc
fn = TakesIntReturnsString
// 5. Pass it into the function from step 3
TakesDoer(fn, 8)
// Shorthand for passing an error (or any other local variable) into a deferred function
func somefunc() (_ int, err error) { // use an _ for the unnamed return value - all values have to be named when using named values
// this declaration is required if not using named return values
// var err error
// need to wrap the deferred call in a function because defer evaluates the arguments to the deferred call immediately
defer func() {
OnCompletion(err)
}()
// do stuff that may or may not result in err being set
}
// struct embedding + methods & "inheritance"
type Base struct {
field string
}
type Derived struct {
Base
other int
}
// method 1
func (b *Base) basefn() string {
return fmt.Sprintf("Base+%s", b.field)
}
// method 2
func (d *Derived) basefn() string {
return fmt.Sprintf("Override+%s", d.field)
}
// method 3
func (d *Derived) derivedfn() string {
return fmt.Sprintf("Derived+%s", d.field)
}
func main() {
b := Base{field: "basic"}
d := Derived{Base: Base{field: "advanced"}, other: 2}
fmt.Printf("%s\n", b.basefn()) // works, calls method 1
fmt.Printf("%s\n", d.basefn()) // works, calls method 2
fmt.Printf("%s\n", b.derivedfn()) // does not compile
fmt.Printf("%s\n", d.derivedfn()) // works, calls method 3
}
// Check to see if an error chain includes a particular error
var p *SomeErrorType
errors.As(err, &p)
// You can pass the results of a multiple return function directly into another function call (as long as the types line up)
takesIntStrErr(returnsIntStrErr(x, y))
// Returning a "zero" value from a generic function
func genericFunction[T any]() (T, error) {
var zero T
return zero, nil
}
// Setting a client-side connection timeout (at least in gRPC) can be done via the context
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Duration(deadline) * time.Millisecond))
defer cancel()
conn.Operation(ctx, ...)
// when setting deadlines, the earliest deadline takes precedence
root := context.Background()
ctx1, cancel1 := context.WithDeadline(root, time.Now().Add(time.Duration(10)*time.Millisecond))
defer cancel1()
// the deadline for ctx2 will not override the parent context's deadline because 10ms < 10s
ctx2, cancel2 := context.WithDeadline(ctx1, time.Now().Add(time.Duration(10)*time.Second))
defer cancel2()
// the deadline for ctx1a will override the parent context's deadline because 5ms < 10ms
ctx1a, cancel1a := context.WithDeadline(ctx1, time.Now().Add(time.Duration(5)*time.Millisecond))
defer cancel1a()
// json.NewDecoder() ignores garbage at the end of the stream
type Thing struct {
Name string `json:"name"`
}
func parse1(stream io.ReadCloser) (Thing, error) {
t := Thing{}
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(stream); err != nil {
return t, err
}
err := json.Unmarshal(buf.Bytes(), &t)
return t, err
}
func parse2(stream io.ReadCloser) (Thing, error) {
t := Thing{}
err := json.NewDecoder(stream).Decode(&t)
return t, err
}
func main() {
// parse1 fails, parse2 succeeds
val, err := parse1(io.NopCloser(strings.NewReader(`{"name":"Bob"}PLUS GARBAGE`)))
if err != nil {
fmt.Printf("Error happened: %v\n", err)
} else {
fmt.Printf("OK: %v\n", val)
}
}
// force a compile-time error if a struct doesn't fully implement a given interface
type InterfaceName {
Func1(s string) string
}
type structName struct {}
// fails to compile due to no: func (s *structName) Func1(s string) string { return "val" }
// what is this doing?
// declares a variable of type Interface name (that we don't care about so it's assigned to _)
// and tries assigning a variable of type pointer to structName to it and the assignment will fail
// if structName does not implement all of the methods defined on InterfaceName
// - uses nil because nil can be assigned to any pointer so no need to declare a structName variable
// - uses a pointer because it is idiomatic to use pointer receivers when defining methods, if you could
// guarantee that structName would never need to use pointer receivers, you could achieve the same thing
// by assinging a zero value of structName to an InterfaceName variable.
var _ InterfaceName = (*structName)(nil)
// JSON marshaling omit attributes if not set
type Thing struct {
Value1 string `json:"value1"` // marshals "value1": "" if not set
Value2 string `json:"value2,omitempty"` // omits "value1" if not set or set to ""
Value3 *string `json:"value3"` // marshals "value1": null if not set
Value4 *string `json:"value4,omitempty"` // omits "value1" if not set or set to nil
}
// Custom JSON unmarshaling with validation: https://go.dev/play/p/jYPARNjmhVn
type Outer struct {
Name string `json:"name"`
Things []Inner `json:"things"`
}
func (o *Outer) UnmarshalJSON(b []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if err := json.Unmarshal(raw["name"], &o.Name); err != nil {
return err
}
if err := json.Unmarshal(raw["things"], &o.Things); err != nil {
return err
}
return nil
}
type Inner struct {
Value int32 `json:"val"`
}
func (i *Inner) UnmarshalJSON(b []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if err := json.Unmarshal(raw["val"], &i.Value); err != nil {
return err
}
if i.Value < 100 {
return errors.New("Too Low")
}
return nil
}
func main() {
var x Outer
str := `{"name": "Me", "things": [{ "val": 120 }, { "val": 30 }]}`
err := json.Unmarshal([]byte(str), &x)
if err != nil {
// fails because val=30 is < 100
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Value: %+v\n", x)
}
}
// custom JSON unmarshaling using an embedded struct to separate custom & default parsing
// https://ukiahsmith.com/blog/improved-golang-unmarshal-json-with-time-and-url/
type Descriptor struct {
Name string `json:"name"`
Key string `json:"key"`
Description string `json:"description"`
BaseURL url.URL `json:"baseUrl"`
}
func (a *Descriptor) UnmarshalJSON(data []byte) error {
// need to use a different type definition here (desc2 instead of Descriptor) to avoid infinite recursion during unmarshaling
type desc2 Descriptor
tmp := struct {
BaseURL string `json:"baseUrl"`
*desc2
}{
// embeds a pointer to the receiver in this anonymous struct to receive the other attributes as is
desc2: (*desc2)(a),
}
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}
baseURL, err := url.Parse(tmp.BaseURL)
if err != nil {
return err
}
a.BaseURL = *baseURL
return nil
}
// embedding an interface in a struct can be used for delegation
// https://go.dev/play/p/xnmxLdCXjqL
// the common interface
type Thing interface {
Increase(i int32) int32
Decrease(i int32) int32
}
// the base implementation
type thing struct{}
func (t thing) Increase(i int32) int32 {
return i + 10
}
func (t thing) Decrease(i int32) int32 {
return i - 10
}
// the wrapped implementation (embeds the common interface)
type Wrapped struct {
Thing
Amount int32
}
// changes the Increase method to delegate to the base
// implementation and then do something else
func (w Wrapped) Increase(i int32) int32 {
return w.Thing.Increase(i) + w.Amount
}
// wraps a base implementation in the wrapped implementation
func WrappedThing(thing Thing, amt int32) Thing {
return &Wrapped{thing, amt}
}
func main() {
var n int32 = 20
t := thing{}
fmt.Printf("%d, %d\n", t.Increase(n), t.Decrease(n))
wt := WrappedThing(t, 17)
fmt.Printf("%d, %d\n", wt.Increase(n), wt.Decrease(n))
}
// print full struct info (FQN & field names/values)
fmt.Printf("%#v", val)
// channels and go routines
// https://go.dev/play/p/SwLwKziIb_A
func work(num int) string {
return fmt.Sprintf("Job: %d", num)
}
func main() {
ct := 10
ch := make(chan string, ct)
var wg sync.WaitGroup
for i := range ct {
wg.Add(1)
go func() error {
defer wg.Done()
ch <- work(i)
return nil
}()
}
wg.Wait() // wait for all goroutines to finish
close(ch) // close the results channel (otherwise, range loop will never terminate)
for s := range ch {
fmt.Printf("Result: %s\n", s)
}
}
// Deserialize JSON string as int
payload = `{"identifier": "1234"}`
type Thing struct {
Id int `json:identifier,string`
}
// Closures are able to capture local variables
type Thing struct {
Val string
}
func Run(fn func()) {
fn()
}
func NewThing(v string) Thing {
return Thing{Val: v}
}
func main() {
t1 := Thing{Val: "123"}
Run(func() {
// can modify locals from a closure
t1.Val = "456"
})
fmt.Printf("t1 = %v\n", t1)
t2 := Thing{}
Run(func() {
// can reassign locals from a closure
t2 = NewThing("789")
})
fmt.Printf("t2 = %v\n", t2)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment