compile, run
types
fmt
pointers
loops
Error Handling
Goroutines
OOP in Go
Testing
Benchmarking
-
- If you have experience with compiling with python virtual environments with requirements.txt or java gradle / maven, you most definately know how complicated and hard it is just to execute a file in a server.
-
- No language needs to be installed to execute.
- The result file is a simple executable.
- simple package management files with .mod files.
- Can be configured to be built for different OS and CPU architecture.
-
-
-
-
go mod init moduleName
- this command creates a go.mod file.
The go.mod file contains the module name, dependencies and versions.
-
-
-
go get github.com/pkg/errors
- If you run
go get
, this code creates a package in thego env GOPATH
directory.# chekc this path if the package is installed go env GOPATH
- The go.sum file includes the hash information of the dependency to match the exact version, branch, content.
- You can also use
go get
to change the version of the used dependency.go get github.com/pkg/errors@v0.8.1
-
-
-
go list -m all
-
-
-
If you upgrade or downgrade by specifying the version with
go mod tidy
go get
, the go.sum file will not remove the previous version used. This command will remove the previous versions and unused dependencies.
-
-
-
-
go build mygofile.go
-
-
-
go run mygofile.go
-
-
-
- uint (unsigned 64bit or 32 bit by operating system)
- uint8 (0 ~ 255)
- uint16 (0 ~ 65535)
- uint32 (0 ~ 4294967295)
- uint64 (0 ~ 18446744073709551615)
-
- int (64bit or 32 bit by operating system)
- int8 (-128 ~ 127)
- int16 (-32768 ~ 32767)
- int32 (-2147483648 ~ 2147483647)
- int64 (-9223372036854775808 ~ 9223372036854775807)
-
- float (64bit or 32 bit by operating system)
- float32 (precision of IEEE-754 32 bit)
- float64 (precision of IEEE-754 64 bit)
-
fmt stands for "format", this package in go scans and prints I/O values by the given format.
-
-
package main import "fmt" func main() { var printingValue int8 = 32 // print fmt.Print(printingValue) // println (print with next line) fmt.Println(printingValue) // printf (print format) (mostly used) fmt.Printf("%d", printingValue) }
-
-
-
package main import "fmt" func main() { var printingValue int8 = 32 // sprint (set value string from print) var settingValue = fmt.Sprintf("%d", printingValue) fmt.Println(settingValue) }
-
-
-
package main import "fmt" func main() { // scanln (scan line) var input string fmt.Scanln(&input) // scanf (scan by format) var integerInput int64 fmt.Scanf("%d", &integerInput) }
-
-
-
%v
: value%+v
: struct value with field names%T
: type
-
%t
: bool
-
%b
: binary%o
: oxadecimal%d
: decimal%x
: hexadecimal
-
%e
: adds the scientific notation e%f
: e is removed but lesser decimals%.2f
: %f with the precision of 0.XX, rounded%g
: e is removed, for large exponents
-
-
-
Every function in go creates a new memory stack in use.
In this memory stack, all memory usage of variables are considered "immutable".
They can only be accessed within the stack and cannot be modified from outside the scope.package main import "fmt" func main() var IntegerValue int64 = 256 WithoutPointer(IntegerValue) fmt.Println(IntegerValue) } func WithoutPointer(IntegerValue int64) { IntegerValue += 10 }
This code will create 2 memory stacks. main and WithoutPointer.
Modifying the value of IntegerValue inWithoutPointer
will not modify the value inmain
.package main import "fmt" func main() { var IntegerValue int64 = 256 WithPointer(&IntegerValue) fmt.Println(IntegerValue) } func WithPointer(IntegerValue *int64) { *IntegerValue += 10 }
However if you send the variable to the function as a pointer, you can modify memory within different stacks.
This is because theWithPointer
memory stack is given the address ofIntegerValue
.
-
-
-
&
returns the address of of the variables memory. (ex:0x14000110018
) -
*
is used to modify, change or read the variable of the given address.var Variable int64 = 0 var VariableMemoryAddress = &Variable *VariableMemoryAddress += 1
-
The
*
sign is also used to tell that the type itself is a pointer by adding it in front of a type.var Variable int64 = 0 var VariableMemoryAddress *int64 = &Variable
-
- golang has a similar but different way of defining loops.
-
- Golang
for
keyword replaces thewhile
keyword unlike most programming languages.// while (condition) for variable > 10 { } // while true for true { }
- Golang
-
-
Golang has a unified method for looping elements from iterations compared to other languages.
for i, element := range array { }
for key, value := range map { }
// available in java 5 for (Type element : array) { }
// 1. using map entryset for (Map.Entry<String, Integer> entry : map.entrySet()) { entry.getKey(); entry.getValue(); } // 2. using map iterator Iterator<Integer> iterator = map.values().iterator(); while (iterator.hasNext()) { Integer value = iterator.next(); } // 3. using map foreach map.forEach((key, value) -> ...);
as you can see, Golang does have a more unified and simple method compared to java.
-
- golang has quite a unique way of handling errors compared to other languages.
-
- Conventional programming languages throw exceptions, in Go, we throw panics.
But as funny as it seems, we cannot catch a panic like a try catch method. -
- The goroutines where the panic occurred terminates
- It does not affect the execution of other goroutines
- It is a unrecoverable state and terminate immediately
- Conventional programming languages throw exceptions, in Go, we throw panics.
-
- I wouldn't call this "handling" because
defer
will not stop the termination and keep the goroutine running. But still we should be able to log the panic and change our code to return it as a custom error afterwards.Here, thefunc main() { defer func() { if r := recover(); r != nil { fmt.Println("defer check for from panic:", r) } }() panic("panic attack") }
r:=recover();
returns the string "panic attack".
Remember that panic has a parameter of typeany
, sorecover
returns whatever type that is given.
- I wouldn't call this "handling" because
-
- When we use goroutines, we use
WaitGroup
in order to wait for the routine to be finished.
If the routine panics withoutwg.Done()
, the program will wait forever. In order to block such behaviour we mustdefer
. -
var wg sync.WaitGroup func waitRoutine() { defer wg.Done() panic("panic attack") } func main() { wg.Add(1) go waitRoutine() wg.Wait() }
- When we use goroutines, we use
-
- If you check the
errors.go
package, you can see that it is shockingly simple.
But because of it's "short" code, it does lack some functionality that other languages offer as a given.
One of the features that lack I find annoying is getting the stack trace. -
In java, the exception class itself has a
printStackTrace
.Creating custom errors might me sufficient enough, but if you use that custom error raised in two different places, how would you debug just using a the string message?try { throw new RuntimeException("oops, error."); } catch (RuntimeException e) { e.printStackTrace(); }
So our best bet would be to create a custom error struct that contains a stack trace.If you use custom logging services like datadog or logstash, you should customize your error struct to be as specific as possible , such as the name of the gorutinepackage main import ( "fmt" "runtime/debug" ) type CustomError struct { errorMessage string stack string } func (e CustomError) Error() string { return e.errorMessage } func NewCustomError(message string) CustomError { return CustomError{errorMessage: message, stack: string(debug.Stack())} } func main() { err := NewCustomError("oops, error.") fmt.Println(err.stack) }
runtime.GoID()
, the current executing code file withruntime.Caller(1)
etc.
Example of error logging in datadog
- If you check the
- "Goroutines are lightweight thread managed by the Go runtime." - official go.dev
In most programming languages, you have the option to create a thread or process for concurrency.
In Go, we can only create goroutines. -
WaitGroup is a tool in the "sync" package in order to wait for goroutines.
package main import ( "sync" "time" ) var wg sync.WaitGroup func waitRoutine() { defer wg.Done() time.Sleep(1000) } func main() { for i := 0; i < 1000; i++ { wg.Add(1) go waitRoutine() } wg.Wait() }
-
-
this function adds the parameter
delta
in most case 1, meaning as a incrementation of goroutines to be waited.This code has no functional difference with the previous code.func main() { wg.Add(1000) for i := 0; i < 1000; i++ { // wg.Add(1) go waitRoutine() } wg.Wait() }
-
This tells the
wg
waitGroup that 1 goroutine is finished. -
This waits until enough
wg.Done()
is called as much as thedelta
added bywg.Add()
-
this function adds the parameter
-
-
In Go, waitGroups are not the only way to wait for goroutines, you can also use channels in order to receive and e wait.
package main import ( "fmt" ) func sendData(ch chan int) { for i := 0; i < 5; i++ { ch <- i } close(ch) } func main() { ch := make(chan int) go sendData(ch) for { data, running := <-ch if !running { break } fmt.Println(data) } }
-
-
This line declares a channel variable. In order to specify a channel type,
chan int
is used.
You can also specify the number of how much values can be sent to this channel bysize
.ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 // The next send will block and result in a panic ch <- 4
- This line is used to send variables to the channel.
-
This line makes the
running
boolean indata, running := <-ch
return as false.
Which means that the channel is closed and no longer for use. -
This line is used for receiving data in the channel.
data
is sent bych <- i
,running
is changed to false byclose(ch)
.
-
This line declares a channel variable. In order to specify a channel type,
-
Go is quite different than other languages even in implementing OOP.
Go does not have inheritance, and it has embedding. -
In inheritance, a dog is a mammal, which is a animal.
A mammal can run, and a animal can breath.
In embedding, a dog has legs, and lungs.
Legs can run, and lungs can breath.
Great conference in GopherUK -
"For a value x of type T or *T where T is not a pointer or interface type, x.f denotes the field or method at the shallowest depth in T where there is such an f. If there is not exactly one f with shallowest depth, the selector expression is illegal." - Go
package main import "fmt" type Depth0 struct { Depth1 } type Depth1 struct { Depth2 seq int } type Depth2 struct { seq int } func main() { depth := Depth0{Depth1: Depth1{seq: 1, Depth2: Depth2{seq: 2}}} fmt.Println(depth.seq) }
The difference with java and other conventional code is that the
seq
can be accessed within of typeDepth0
. This is because of golang uses embedding not inheritance.
If a dog has a leg, you can say that a dog can walk. :depth.seq
You can also say that the dogs right leg is walking. :depth.Depth1.seq
You can also say that the dogs left leg is walking. :depth.Depth2.seq
Another thing to note is that the code above will returnseq
ofdepth1
notdepth2
. As instructed from Go as "shallowest depth."
If theseq
is in the same depth, golang returns a compile error "ambiguous selector". -
"If it walks like a duck, and it quacks like a duck. It must be a duck."
In Go, this is allowed in interfaces, but not in structs.package main type Duck interface { quack() walk() } // class to class duck typing does not work // type Duck struct { // } type Something struct { } func (Something) quack() { } func (Something) walk() { } func isThisADuck(duck Duck) { } func main() { something := Something{} isThisADuck(something) }
Note that the
Duck
interface was not mentioned anywhere on classSomething
.
But still passes as the parameter as the interfaceDuck
. -
package main import ( "encoding/json" "fmt" ) type Account struct { email string `json:"email"` firstName string `json:"first_name,omitempty"` lastName string `json:"last_name,omitempty"` password string age uint8 `json:"age,omitempty"` } type AccountView struct { Account password string `json:"omitempty"` } func main() { account := Account{ email: "dohyung97022@gmail.com", password: "my super secret password" } jsonData, _ := json.Marshal(account) fmt.Println(jsonData) accountView := AccountView{Account: account} jsonData, _ = json.Marshal(accountView) fmt.Println(jsonData) }
Structs are the "Class" of golang. So easy, everybody knows that.
So what I wanted to talk about is, handling json within Go.
In modern programming, we use the termmodel
,view
,controller
,service
to separate structures within the backend.
You will need to createmodel
asview(json)
and communicate with the frontend.
In Go, you can use keywords such asjson
,omitempty
.
The example above,account
is set with nofirstname
lastname
orage
.
omitempty
will remove the field that is not specified.{"first_name":"dohyung97022@gmail.com", "password":"my super secret password"}
There will be situations when you need to remove the values like
password
.
In this case, you can use "Promotion" in order to remove unwanted json values.
Remember, embedding in Go always promote the "shallowest depth".{"first_name":"dohyung97022@gmail.com"}
-
I have heard some people that TDD is tedious, and time-consuming, but I totally disagree with this statement. It is a tradeoff for stability and speed over time, with current cost and speed of development.
It might seem tedious now, but in the long run, it will be even faster.Lets say that you have developed a function that has a complicated business logic.
When you come back to your code after a year, how would you remember such logic?
Well you could dig up your documentation, as documentation is a must for development.
But test code is also be a great point for reference. It has information of what values should go in, and what should go out, even with the edge cases you have to worry about.type hashAuthorization struct { hashType string key string value string } type test struct { name string in http.Header out hashAuthorization } var cases = []test{ {"parse hashAuthorization", http.Header{"authorization": []string{"SAPISIDHASH 1704335617_e72d02a68243c61b04537b1165b33b55c3f77224"}}, hashAuthorization{hashType: "SAPISIDHASH", key: "1704335617", value: "e72d02a68243c61b04537b1165b33b55c3f77224"}}, }
Just by looking at this testCase, you can know that the function we are testing
- has an input of
http.Header
, - has a output of
hashAuthorization
, - parses
SAPISIDHASH 1704335617_e72d02a68243c61b04537b1165b33b55c3f77224
intohashType
,key
andvalue
.
Lets say that your company has employed a new jr developer.
The jr has no idea of checking out documentations or any point of reference.
Or does read the documentation but misunderstood it.
This Jr dev has been assigned to add some business logic into your function.
If you have written your test code will great edge cases,
The Jr devs code will not even build to production.
The architecture of your CI/CD should checkgo test
and stop if it fails test cases. - has an input of
-
-
src payment payment.go payment_service.go payment_service_test.go user user.go user_service.go user_service_test.go main.go
- The test file needs to end with
_test
- The test file needs to be within the same folder (package)
You should not place your test files in a separate folder (package)
src test payment_service_test.go user_service_test.go
The reason being is that 2. This will create the test files in a separate package of
test
- The function to be tested should not be public just to be accessed by test files
- placing test files besides the code file enforces the dev to create a test file.
If it is in a separate file, devs can miss its location, or miss some of the services.
- The test file needs to end with
-
-
-
testing.T
contains most of the testing utilities.
When you add a test function you should add atesting.T
as a pointer.func TestAdd(t *testing.T) {}
-
The
Run
method runs thefunction
in a separate goroutine.
Thefunction
receives atesting.T
as a parameter.func TestBusinessFunction(t *testing.T) { t.Run("case input1,input2", func(t *testing.T) { if businessFunction("input1", "input2") != "output" { t.Error("business logic has failed") } }) }
-
Fail()
and theFailNow()
does not take any arguments.
TheFail()
function will tell that the test has failed, but keep testing other functions.
TheFailNow()
function will tell that the test has failed and exit the program. -
Error()
andFatal()
takes an arguments or...any
this results infmt.Sprintln(args...)
.
Errorf()
andFatalf()
takes an additional string offormat
and format printsargs
TheError()
function will tell that the test failed and continue.
TheFatal()
function will tell that the test failed and exit the program. -
type in struct { a int b int } type test struct { name string in in out int } var cases = []test{ {"all zero", in{0, 0}, 0}, {"left zero", in{0, 3}, 3}, {"right zero", in{3, 0}, 3}, } func TestAdd(t *testing.T) { for _, test := range cases { t.Run(test.name, func(t *testing.T) { if add(test.in.a, test.in.b) != test.out { t.Errorf("%d added with %d is not the result %d", test.in.a, test.in.b, test.out) } }) } }
-
-
Golang has its own benchmark cli that can be run by
go test ./... -bench=.
.
Benchmarking cpu and memory usage is crucial for server management.
If you can identify how much time a function takes, and how much memory it uses, you can identify and improve such problems.
Example output forgo test ./.. -bench -benchtime=10000x -benchmem
-
In go, the program identifies a benchmark function by naming "Benchmark" at the beginning.
func BenchmarkStringsBuilder(b *testing.B) { var builder strings.Builder for n := 0; n < b.N; n++ { builder.WriteString("a") } }
You can then specify that function by
go test ./... -bench=BenchmarkStringsBuilder
The -bench takes a regex that specifies function names.
For examplego test ./... -bench=String
will run all benchmarks with the wordString
. -
The go benchmark function takes a
testing.B
as an argument.
This class contains multiple parameters that is set for benchmarking. -
The
testing.B
class contains a variable ofN
.
ThisN
value is set when you configure it with-benchtime=10x
.for n := 0; n < b.N; n++ { builder.WriteString("a") }
So it would be ideal to keep your benchmark functions to loop through
testing.B.N
for this command to function. You can also set the-benchtime
as seconds.
This would result your benchmark to run for that specified seconds. -
This command is added to
go test ./... bench=. -benchmem
to print out the memory usage. -
This command is added to benchmark with given number of cpu cores.