The standard library
There are a lot of "batteries included" in Go.
The standard library is really huge, let's check some of those packages to see, how they were implemented.
io
Input and Output Data into files is probably one of the main things, what a programm should can.
The package io
provide this functionality and two most used interfaces from this package are:
| type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
|
Reader
and Writer
are very simple and practical interfaces.
They allow to read or write len(p)
byte to a specific destination.
This allows also to read buffered data, lets check an example here:
| func countLetters(r io.Reader) (map[string]int, error) {
buf := make([]byte, 2048)
out := map[string]int{}
for {
n, err := r.Read(buf)
for _, r range bug[:n] {
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
out[string(r)]++
}
}
if err == io.EOF {
return out, nil
}
if err != nil {
return nil, nil
}
}
}
|
Let's see, how we can read a simple file:
| import (
"fmt"
"io"
"log"
"os"
)
func main() {
f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
log.Fatal(err)
}
defer func() {
if err := f.Close(); err != nil {
log.Fatal(err)
}
}()
data, err := io.ReadAll(f)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}
|
The ioutil.ReadAll
reads a Reader
interface until an error or io.EOF
is read.
time
The time
package is for working with dates, times and datetimes.
The package contains two main types: time.Duration
and time.Time
.
A period of time is represented as an int64
.
The smallest amount of time is one nanosecond.
But there are some constants, which can be used to calculate a minute, two seconds and so on:
| h := 2 * time.Hour // 2 hours
m := 3 * time.Minute // 3 minutes
hm := h + m // 2 hours and 3 minutes
|
time.ParseDuration
can parse strings like 300ms
or 2.3h
:
| import (
"fmt"
"time"
)
func main() {
hours, _ := time.ParseDuration("10h")
complex, _ := time.ParseDuration("1h10m10s")
micro, _ := time.ParseDuration("1µs")
// The package also accepts the incorrect but common prefix u for micro.
micro2, _ := time.ParseDuration("1us")
fmt.Println(hours)
fmt.Println(complex)
fmt.Printf("There are %.0f seconds in %v.\n", complex.Seconds(), complex)
fmt.Printf("There are %d nanoseconds in %v.\n", micro.Nanoseconds(), micro)
fmt.Printf("There are %6.2e seconds in %v.\n", micro2.Seconds(), micro)
}
|
Now we come to a really unusual thing... parsing and formatting datetimes.
Go follows the idea to use a format depending on the position in the string: 01/02 03:04:05PM 06 -0700
.
So for example:
| func main () {
t, err := time.Parse("2006-01-02 15:04:05 -0700", "2016-01-13 00:12:43 +0000")
if err != nil {
fmt.Println(err)
}
fmt.Println(t.Format("January 2, 2006 at 03:04:05PM MST"))
}
|
output would be:
January 13, 2016 at 12:12:43AM UTC
There are a lot of more functions to manipulate time.Time
.
Jus see some of the documentation
gostradamus
If you want to work with datetimes and to have a in-replacement for time you can use my Go package: gostradamus.
It's a more "convenient" way to work with time.Time
variables:
| import (
"fmt"
"github.com/bykof/gostradamus"
)
func main() {
dateTime, err := gostradamus.Parse("14.07.2017 02:40:00", "DD.MM.YYYY HH:mm:ss")
if err != nil {
panic(err)
}
// Easy manipulation
dateTime = dateTime.ShiftMonths(-5).ShiftDays(2)
// Easy formatting
fmt.Println(dateTime.Format("DD.MM.YYYY HH:mm:ss"))
// 16.02.2017 02:40:00
// Easy helper functions
start, end := dateTime.SpanWeek()
fmt.Println(start.String(), end.String())
}
|
output:
16.02.2017 02:40:00
2017-02-13T00:00:00.000000Z 2017-02-19T23:59:59.999999Z
encoding/json
Go has a builtin json parser.
It uses the word Marshal
(Parse
) and Unmarshal
(Format
) for this library.
Also Go uses structs or types to represent JSON object, arrays and so on.
You can "map" json object fields onto your struct, even when the field names are different.
Let's check an example:
| {
"id": 123,
"first_name": "Test",
"last_name": "Tester",
"birthday": "1994-01-19T00:00:00Z",
"skills": [
{ "id": 1, "name": "Running" },
{ "id": 1, "name": "Jumping" }
]
}
|
now we define a struct to map the data:
| import (
"encoding/json"
"fmt"
"time"
)
type Skill struct {
ID int `json:"id"`
Name string `json:"name"`
}
type Skills []Skill
type Person struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Birthday time.Time `json:"birthday"`
Skills Skills `json:"skills"`
}
func main() {
s := `
{
"id": 123,
"first_name": "Test",
"last_name": "Tester",
"birthday": "1994-01-19T00:00:00Z",
"skills": [
{"id": 1, "name": "Running"},
{"id": 1, "name": "Jumping"}
]
}`
var person Person
err := json.Unmarshal([]byte(s), &person)
if err != nil {
panic(err)
}
fmt.Printf("%#v", person)
}
|
output:
main.Person{ID:123, FirstName:"Test", LastName:"Tester", Birthday:time.Date(1994, time.January, 19, 0, 0, 0, 0, time.UTC), Skills:main.Skills{main.Skill{ID:1, Name:"Running"}, main.Skill{ID:1, Name:"Jumping"}}}
net/http
The net/http
library has a client
and a server
.
So you can send and receive http requests.
client
You can create a http.Client
instance with a default timeout:
| import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
func main() {
client := &http.Client{
Timeout: 30 * time.Second,
}
request, err := http.NewRequestWithContext(
context.Background(),
http.MethodGet,
"https://jsonplaceholder.typicode.com/todos/1",
nil,
)
if err != nil {
panic(err)
}
response, err := client.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
panic(fmt.Sprintf("got unexpected status: %v", response.StatusCode))
}
var data struct {
ID int `json:"id"`
UserID int `json:"userId"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
err = json.NewDecoder(response.Body).Decode(&data)
if err != nil {
panic(err)
}
fmt.Printf("%+v", data)
}
|
output would be:
{ID:1 UserID:1 Title:delectus aut autem Completed:false}%
server
The server http.ServeMux
handles multiple paths.
You can combine multiple ServeMux Handlers, but be aware, that you have to strip the path before the request will be given to any underlying handler.
Every incoming request will be handled by it's own goroutine.
| import (
"fmt"
"net/http"
"time"
)
func main() {
apiMux := http.NewServeMux()
apiMux.HandleFunc("/todo/", func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("todo"))
})
apiMux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("api"))
})
mux := http.NewServeMux()
mux.Handle("/api/", http.StripPrefix("/api", apiMux))
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// The "/" pattern matches everything, so we need to check
// that we're at the root here.
if req.URL.Path != "/" {
http.NotFound(w, req)
return
}
fmt.Fprintf(w, "Welcome to the home page!")
})
server := http.Server{
Addr: ":8000",
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
Handler: mux,
}
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
panic(err)
}
}
|
Sometimes you need a middleware to check if the user is allowed to access an endpoint:
main.go |
---|
| const Password = "notsecurepassword"
func securityMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Header.Get("Authorization") != Password {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("no access"))
return
}
h.ServeHTTP(w, req)
})
}
func requestTimer(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
h.ServeHTTP(w, req)
end := time.Now()
log.Printf("request took: %s", end.Sub(start))
})
}
// ...
mux.Handle(
"/",
requestTimer(
securityMiddleware(
http.HandlerFunc(
func(w http.ResponseWriter, req *http.Request) {
// The "/" pattern matches everything, so we need to check
// that we're at the root here.
if req.URL.Path != "/" {
http.NotFound(w, req)
return
}
fmt.Fprintf(w, "Welcome to the home page!")
},
),
),
),
)
|