For this unit your environemnt must be initialized as in previous units.
Extessive material can be found on official go site. Take a look, try to reproduce it, but don't dig it too much.
Go standard library offers great capabilities to build http servers.
Most of the examples with implementation of Go http servers with standard library can be reduced to code:
import (
"net/http"
)
func newRootHandler() {
// Here we define function and return it
// note that we return function and under the hood it is used like this:
// handler := newRootHandler()
// handler variable is address of function which we can call:
// handler(w, r)
return func(w http.ResponseWriter, r *http.Request) {
log.Println("request /")
io.WriteString(w, "Some data to be returned\n")
}
}
func main() {
http.HandleFunc("/", newRootHandler())
// more handlers
err := http.ListenAndServe(":3333", nil) // application execution will stop here until server will be stopped
}
Note that in example above http
is a package.
By calling HandleFunc you add handlers for particular paths in URL, for example if there is handler1 /api/
it handles all paths started from /api
: /api/v1
, /api/v1/foo
, /api/
and /api
. If additional handler2 is added to /api/v1/test
path, this partical path will be handled by handle2 while all other /api
paths will be handled by hanlder1:
/ - default handler
├── /api/ - handler1
| └── /v1 - handler1
| | ├── /foo - handler1
| | | └── /* - handler1 # any subpath of /api/v1/foo
| | ├── /test - handler2
| | | └── /* - handler1 # any subpath of /api/v1/test
| | └── /* - handler1 # any subpath of /api/v1/
| └── /* - handler1 # any other subpath of /api/
└── /* - default handler # any other subpath of /
Note that for example /api/v1/test/foo
will be managed by handler1 instead of handler2 because hanlder2 manages /api/v1/test
only. If you want handler2 to manage all subpaths of /api/v1/test
you should define path with trailing /
: /api/v1/test/
Such tree of handlers is managed by things commonly called as http multiplexers or routers.
Tearing down http
package you can find that there is (DefaultServeMux)[]. Everytime you specify nil as handler in http.ListenAndServe(":3333", nil)
http library will use DefaultServeMux defined in http package. In most production application it's better and more maintainable to create own instance of multiplexer of mux in short and configure it:
mux := http.NewServeMux()
mux.HandleFunc("/", newRootHandler())
...
Non-default mux allows you to spinup multiple different servers listen different port for different purposes, for example separate servers on different ports/addresses for web, api and metrics:
webmux := http.NewServeMux()
webmux.HandleFunc("/", newWebHandler())
apimux := http.NewServeMux()
apimux.HandleFunc("/api/v1/", newApiV1Handler())
apimux.HandleFunc("/api/v2/", newApiV2Handler())
metricsmux := http.NewServeMux()
metricsmux.HandleFunc("/metrics", newPrometheusMetricsHandler())
wg := sync.WaitGroup{}
wg.Add(1)
go func(){
defer wg.Done
log.Println(http.ListenAndServe(":80", webmux)) // goroutine execution will stop here until server will be stopped
}
wg.Add(1)
go func(){
defer wg.Done
log.Println(http.ListenAndServe(":8080", apimux)) // goroutine execution will stop here until server will be stopped
}
wg.Add(1)
go func(){
defer wg.Done
log.Println(http.ListenAndServe(":3000", metricsmux)) // goroutine execution will stop here until server will be stopped
}
wg.Wait() // application execution will stop here until aat least one server works
When we call http.ListenAndServe()
it creates http.Server
in-the-fly and calls ListenAndServe()
fucntion of created http.Server
.
By creating new http.Server
instance you can tune http server that can be also be useful in produciton code, for example if you need custom TLS or timeouts configuration.
Server listens the socket, accepts incoming connections and pass handling of the connection to mux handler.
To build exercise, being in root folder of the repo you can run:
go build ./unit4/exercises/e0
This command will build local folder with all .go
files in it and place result application to e0
file in current (repo root folder).
If you want to specify name of path of the file:
go build -o ./unit4/e0 ./unit4/exercises/e0
for oxercies of these and next unit it is handy to build and run in one command:
go run ./unit4/exercises/e0
It will build and run code in ./unit4/exercises/e0
:
./unit4/exercises/e0
Find source code of this exercise.
TBA
mux.HandleFunc("/", defaultHandler)
mux.HandleFunc("/a/", aHandler)
mux.HandleFunc("/b", bHandler)
mux.HandleFunc("/c", cHandler)
mux.HandleFunc("/a/b", abHandler)
- aHandler
- bHandler
- cHandler
- abHandler
Extend code from exercise 0: Add handler to "/echo" path which for "POST" requests will read request body and echo them back.
Note: Test verifies the output of your program by running it and sending generated request to your http server and verifies a reply. Server must listens port 8080. You may organize code as your own.
Don't add additional Prints to output. It is checked in tests.
Share your implementation unit4/exercises/e1/main.go
in github PR.
Don't hesitate to copy contents of unit4/exercises/e0/
to unit4/exercises/e1/
and modify necessary files or add new ones.
Hint
switch r.Method { // just for illustration let's read whole body
case http.MethodPost, http.MethodPut:
io.Copy(io.Discard, r.Body) // read request Body to null
}
Extend code from exercise 1: Add handler to "/ungzip" path which for "POST" requests will read request body, ungzip the input and send it back. In case of error, server must return HTTP Status Code 400 (Bad Request)
Note: Test verifies the output of your program by running it and sending generated request to your http server and verifies a reply. Server must listens port 8080. You may organize code as your own.
Note: To test your implementation you can run
echo "AAAAAAAAAA=" | gzip | curl --data-binary @- -v http://localhost:8080/ungzip
. It must return something like:
* Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> POST /ungzip HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.74.0
> Accept: */*
> Content-Length: 25
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 25 out of 25 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 27 Oct 2022 06:05:26 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
<
AAAAAAAAAA=
* Connection #0 to host localhost left intact
Key point here is that you receive ungzipped "AAAAAAAAAA=" string.
For malformed request (for example, for non gzipped request body: echo "AAAAAAAAAA=" | curl --data-binary @- -v http://localhost:8080/ungzip
), you must receive:
* Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> POST /ungzip HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.74.0
> Accept: */*
> Content-Length: 12
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 12 out of 12 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Date: Thu, 27 Oct 2022 06:07:47 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
Key point here is to get HTTP/1.1 400 Bad Request
response.
Don't add additional Prints to output. It is checked in tests.
Share your implementation unit4/exercises/e2/main.go
in github PR.
Don't hesitate to copy contents of unit4/exercises/e1/
to unit4/exercises/e2/
and modify necessary files or add new ones.
Hints
- ResponseWriter.WriteHeader(statusCode int)
http.StatusBadRequest
- constant defined in http package for HTTP status code 400- gzip.Reader