# README
= Golang Boot Camp :toc: manual
== Getting started
=== Check go version
[source, go]
go version
go version go1.21.5 linux/arm64
=== package, import, main
[source, go]
package main
import "fmt"
func main() { fmt.Println("Hello, World!") }
=== Run main methods
[source, go]
$ go run hello.go Hello, World!
=== Call External Methods
[source, go]
package main
import "fmt"
import "rsc.io/quote"
func main() { fmt.Println("Hello, World!") fmt.Println(quote.Glass()) fmt.Println(quote.Go()) fmt.Println(quote.Hello()) fmt.Println(quote.Opt()) }
== Getting started from real project
link:k8s-bigip-ctlr.adoc[project k8s-bigip-ctlr]
== Work with Go standard project
=== Project Structure
[source, go]
tree greetings/
greetings/ ├── cmd │ └── greetings │ └── main.go └── pkg └── greetings ├── greetings.go └── greetings_test.go
A standard Go project structure can vary depending on the size and nature of the project, but there are some common conventions that many Go developers follow:
- cmd: The
cmd
directory is for the main applications of your project. Each application can have its own subdirectory. For example, myapp could be the main entry point for your application - internal: The
internal
directory is for packages that are only used within your project, not meant for external use. - pkg: The pkg directory contains libraries and packages that are meant to be used by other projects. Each package within pkg can have its own subdirectory.
=== Init and Setup Dependencies
[source, go]
go mod init github.com/kylinsoong/golang/greetings go mod tidy
=== Run
[source, go]
go run cmd/greetings/main.go
=== How main module call pkg module
[source, go]
package main
import ( "github.com/kylinsoong/golang/greetings/pkg/greetings" )
func main() { names := []string{"Gladys", "Samantha", "Darrin", "Kylin"} messages, err := greetings.Hellos(names) }
=== Run Unit Test
[source, go]
go test ./pkg/greetings/
=== Build
[source, go]
go build -o a.out cmd/greetings/*.go
=== Run Binary File
[source, go]
./a.out
== Data Struct
=== simple map
[source, go]
processedResources := make(map[string]bool)
processedResources["foo.yaml"] = true
processedResources["bar.yaml"] = false
processedResources["zoo.yaml"] = false
for key, value := range processedResources {
fmt.Printf("%s: %v\n", key, value)
}
fmt.Println(processedResources["zoo.yaml"])
value, exists := processedResources["coo.yaml"]
if exists {
fmt.Printf("coo.yaml: %v\n", value)
} else {
fmt.Println("coo.yaml not exist")
}
=== simple struct
[source, go]
type WatchedNamespaces struct { Namespaces []string NamespaceLabel string }
func main() { watchedNamespaces := WatchedNamespaces{ Namespaces: []string{"namespace1", "namespace2"}, NamespaceLabel: "watched", }
fmt.Println(watchedNamespaces.Namespaces)
fmt.Println(watchedNamespaces.NamespaceLabel)
}
=== struct with func field
Using a Go struct with a function field offers flexibility and allows you to encapsulate behavior within the struct while enabling dynamic customization.
[source, go]
type Manager struct { queueLen int processAgentLabels func(map[string]string, string, string) bool }
func customProcessAgentLabels(labels map[string]string, namespace string, name string) bool { fmt.Printf("Custom Processing Agent Labels: %v, Namespace: %s, Name: %s\n", labels, namespace, name) return true }
func main() { appMgr := Manager{ queueLen: 10, processAgentLabels: customProcessAgentLabels, } appMgr.processAgentLabels(map[string]string{"key": "value"}, "exampleNamespace", "exampleName") }
== Fundamentals
=== object-oriented programming: interface composition
Go does not support traditional interface inheritance like some other object-oriented programming languages. Instead, Go uses a concept called "interface composition" or "embedding" to achieve similar goals without relying on classical inheritance.
In Go, you can embed one interface within another to create a new interface that includes the methods of the embedded interface.
[source, go] .Interface
type Interface interface { Add(item interface{}) Len() int Get() (item interface{}, shutdown bool) Done(item interface{}) ShutDown() ShutDownWithDrain() ShuttingDown() bool }
[source, go] .DelayingInterface
type DelayingInterface interface { Interface AddAfter(item interface{}, duration time.Duration) }
[source, go] .RateLimitingInterface
type RateLimitingInterface interface { DelayingInterface AddRateLimited(item interface{}) Forget(item interface{}) NumRequeues(item interface{}) int }
== Multi-threads
=== goroutine
The goroutine is a lightweight thread of execution managed by the Go runtime. Goroutines enable concurrent programming in a way that is more efficient and scalable compared to traditional threads.
[source, go]
package main
import ( "fmt" "time" )
func printNumbers() { for i := 1; i <= 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Printf("%d \n", i) } }
func main() { go printNumbers()
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("A%d \n", i)
}
}
=== channel: send and receive data
Channels are a typed conduit through which you can send and receive values with the channel operator <-:
- ch <- v send v to channel
- v := <-ch receive from channel, and assign value to v
[source, go]
func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // send sum to c }
func Test_Send_Receive(t *testing.T) { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c fmt.Println(x, y, x+y) }
=== channel: communication and synchronization between goroutines
In Go, channels are a powerful mechanism for communication and synchronization between goroutines. They provide a way for one goroutine to send data to another goroutine.
[source, go]
func numberGenerator(ch chan int, wg *sync.WaitGroup) { defer wg.Done() for i := 1; i <= 5; i++ { ch <- i // Send numbers 1 to 5 to the channel } close(ch) // Close the channel to signal no more data will be sent }
func squareCalculator(ch chan int, resultCh chan int, wg *sync.WaitGroup) { defer wg.Done() for num := range ch { square := num * num resultCh <- square // Send squared result to the resultCh channel } close(resultCh) // Close the resultCh channel to signal no more results will be sent }
func resultPrinter(resultCh chan int, wg *sync.WaitGroup) { defer wg.Done() for result := range resultCh { fmt.Println("Squared Result:", result) } }
func main() { numberCh := make(chan int) resultCh := make(chan int) var wg sync.WaitGroup wg.Add(3) go numberGenerator(numberCh, &wg) go squareCalculator(numberCh, resultCh, &wg) go resultPrinter(resultCh, &wg) wg.Wait() }
=== channel: multiple chanels with select case statement
The select statement in Go is used to choose from multiple communication operations. It allows a goroutine to wait on multiple communication operations, blocking until one of them can proceed.
[source, go]
func simple_worker(c chan string) { c <- fmt.Sprintf("Hello from Channel %v", c) }
func Test_Multiple_Chan_With_Select(t *testing.T) { ch1 := make(chan string) ch2 := make(chan string) go simple_worker(ch1) go simple_worker(ch2) select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) case <-time.After(3 * time.Second): fmt.Println("Timed out waiting for messages.") } }
=== channel: signal completion of sub goroutine
[source, go]
func worker(ch chan struct{}) { fmt.Println("Worker is starting...") time.Sleep(2 * time.Second) fmt.Println("Worker is done!") ch <- struct{}{} }
func main() { doneCh := make(chan struct{}) go worker(doneCh) <-doneCh fmt.Println("Main function exiting.") }
=== using signal to control application interruptiong and termination
In Go, the os/signal
package provides a way to intercept signals sent to the program, such as termination signals (SIGINT for interrupt and SIGTERM for terminate). The signal usually wrapped with a channel that can be used to control application interruptiong and termination.
[source, go]
func main() { fmt.Println("Started to run tasks...") signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) sig := <-signals fmt.Printf("Received signal: %v\n", sig) }
=== defer statements a Last In, First Out (LIFO) execution order
[source, go]
func main() { defer fmt.Println("This will be executed third.") defer fmt.Println("This will be executed second.") defer fmt.Println("This will be executed first.") fmt.Println("Hello, Go!") }
=== thread-safe via defer and sync
[source, go]
type Counter struct { value int mu sync.Mutex }
func (c *Counter) increment() { c.mu.Lock() defer c.mu.Unlock() c.value++ }
func (c *Counter) getValue() int { c.mu.Lock() defer c.mu.Unlock() return c.value }
== K8S
=== How to repeatedly calls a provided function
The k8s.io/apimachinery/pkg/util/wait
provides utilities for waiting and timing operations. Specifically, wait.Until
is a function that repeatedly calls a provided function until the stop channel is closed or a timeout is reached.
[source, go] .wait.Until
func exampleWork() { fmt.Println("Doing some work...") time.Sleep(2 * time.Second) }
func main() { stopCh := make(chan struct{}) go wait.Until(exampleWork, time.Second, stopCh) time.Sleep(5 * time.Second) close(stopCh) time.Sleep(1 * time.Second) fmt.Println("Main goroutine exiting...") }
=== How to use kubernetes rate limited workqueue
Refer to link:#object-oriented-programming-interface-composition[object-oriented programming: interface composition] for more details about k8s.io/client-go/util/workqueue
and RateLimitingInterface
implemenration.
=== How to create kubernetes client
There are 2 stps necessary to create a kubeClient:
- Create Kubernetes Rest Config, If your application run in Kubernetes, the use the certifications keys in Namespace default ServiceAccount, if your application run outside Kubernetes, then you need pass
~/.kube/config
file to create Rest Config - Create Kubernetes Client via Kubernetes Rest Config
[source, go]
import "k8s.io/client-go/kubernetes" import "k8s.io/client-go/rest" import "k8s.io/client-go/tools/clientcmd"
var kubeClient kubernetes.Interface var config *rest.Config var err error
if *inCluster { config, err = rest.InClusterConfig() } else { config, err = clientcmd.BuildConfigFromFlags("", *kubeConfig) } if err != nil { log.Fatalf("[INIT] error creating configuration: %v", err) }
kubeClient, err = kubernetes.NewForConfig(config) if err != nil { log.Fatalf("[INIT] error connecting to the client: %v", err) }
=== How to use kubernetes client-side caching mechanism and cache api create a configmap watcher
k8s.io/client-go/tools/cache
is a client-side caching mechanism. It is useful for reducing the number of server calls you'd otherwise need to make. Reflector watches a server and updates a Store. Two stores are provided; one that simply caches objects (for example, to allow a scheduler to list currently available nodes), and one that additionally acts as a FIFO queue (for example, to allow a scheduler to process incoming pods).
[source, go]
cfgMapInformer = cache.NewSharedIndexInformer(
cache.NewFilteredListWatchFromClient(
restClientv1,
Configmaps,
namespace,
everything,
),
&v1.ConfigMap{},
resyncPeriod,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
)
cfgMapInformer.AddEventHandlerWithResyncPeriod(
&cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { enqueueConfigMap(obj, OprTypeCreate) },
UpdateFunc: func(old, cur interface{}) { enqueueConfigMap(cur, OprTypeUpdate) },
DeleteFunc: func(obj interface{}) { enqueueConfigMap(obj, OprTypeDelete) },
},
resyncPeriod,
)