README
¶
CRUD Go & Postgres
Overview
Purpose of this application is for studying: golang + postgres, golang testing, postgres+docker for testing.
Step:
- Create base scaffold for the whole program. Model + App + base main and main_test
- Create .env
- Test connect with db. In this case, I created a docker postgres container.
- Create all test case
- Run test case to see if all return 404.
Dependencies
-
gorilla/mux: Package
gorilla/mux
implements a request router and dispatcher for matching incoming requests to their respective handler. -
lib\pq: Go postgres driver for Go's database/sql package
Run test
go test -v
Structure
Main -> App -> Model -> DB
- Instead of DB return Model, methods in model will access db and get db then directly modify the model.
p := product{ID: id}
p.getProduct(a.DB) // p right now will by modify with all the information returned from db
Learnt
Postgres + Docker for testing
-
Use postgres for testing: How to run PostgreSQL in Docker on Mac (for local development)
-
Summary:
-
Running in Docker allows keeping my database environment isolated from the rest of my system and allows running multiple versions and instances
-
For this application, I used the first option
-
docker run --name postgres -e POSTGRES_PASSWORD=password -d -p 5432:5432 postgres
: This command will find an image of postgres and install it, if local doens't has it.- -it: allocate pseudo-TTY
- -d: Run container in background and print container ID
- -p: Publish a container's port(s) to the host
-
To connect to postgres inside docker: Github
-
underscore in front of import package
-
What does an underscore in front of an import statement mean?
-
Summary:
- It's for importing a package solely for its side-effects.
- In our case
pd
is used as driver
Pointer
Little demonstration:
package main
import "fmt"
func main() {
i := 42
fmt.Printf("i: %[1]T %[1]d\n", i)
p := &i
fmt.Printf("p: %[1]T %[1]p\n", p)
j := *p
fmt.Printf("j: %[1]T %[1]d\n", j)
q := &p
fmt.Printf("q: %[1]T %[1]p\n", q)
k := **q
fmt.Printf("k: %[1]T %[1]d\n", k)
}
// Output
i: int 42
p: *int 0x10410020
j: int 42
q: **int 0x1040c130
k: int 42
- Summary:
- If datatype of variable or paramater contains
*
-> ask for address -> pass in with&
--> Use for change value. - Inside function, if the param has
*
, need to use*
to dereference.
- If datatype of variable or paramater contains
Create Scaffolding a Minimal Application
-
Before we can write tests, we need to create a minimal application that can be used as the basis for the tests.
-
In this application, I used
app.go
to create minimal application. By doing this,main.go
(dev, prod) andmain_test.go
(test) can both use theApp
to run the application. -
For model, create all functions 1 model need. Example:
type human struct {
ID int `json:"id"`
Name string `json:"name"`
}
func (h *human) getHuman(db *sql.DB) error {
return errors.New("Not implemented")
}
func (h *human) updateHuman(db *sql.DB) error {
return errors.New("Not implemented")
}
func (h *human) deleteHuman(db *sql.DB) error {
return errors.New("Not implemented")
}
func (h *human) createHuman(db *sql.DB) error {
return errors.New("Not implemented")
}
--> By doing this, We have everything layout. This method also helpful in testing
Should I define methods on values or pointers?
func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct) valueMethod() { } // method on value
-
First, and most important, does the method need to modify the receiver? If it does, the receiver must be a pointer. (Slices and maps act as references, so their story is a little more subtle, but for instance to change the length of a slice in a method the receiver must still be a pointer.)
-
In the examples above, if pointerMethod modifies the fields of s, the caller will see those changes, but valueMethod is called with a copy of the caller's argument (that's the definition of passing a value), so changes it makes will be invisible to the caller.
Env variables
-
Use
os
+godotenv
package
// Load the .env file in the current directory
godotenv.Load()
// or
godotenv.Load(".env")
// Work with os
err := godotenv.Load(".env")
os.Getenv(key)
Testing with TestMain
- Summary: By using
TestMain
, setup and tear down included in the test.
Testing with fake request (make request and see how handler handle it):
-
using
"net/http/httptest"
package -
Summary:
- Create new request with
req, err := http.NewRequest(<request>)
- Create new recorder (
*httptest.ResponseRecorder
type -> record the response so we can check later) withrr := httptest.NewRecorder()
- Create handler with
handler :=http.HandlerFunc(<function>)
- Run request with
handler.ServeHTTP(rr, req)
- Check if the response match what we expected by check the
recorder
:
if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } // Check the response body is what we expect. expected := `{"alive": true}` if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) }
- Create new request with
-
In this application, run request step and check http status code will be resuable functions.
How to use JSON with Go -> Testing
-
However, in this application, I also used
[]byte()
to create function (TestCreateProduct
) -
Summary:
- Encode (marshal) struct to JSON -> use
json.Marshal(<struct>)
struct to json - Decode (unmarshal) JSON to struct or map -> use
json.Unmarshal(<data []byte>, <interface{}>)
. In this application, I converted json to map and used map to check if error exist.
- Encode (marshal) struct to JSON -> use
Unmarshal vs newDecoder.Decode
-
It really depends on what your input is. If you look at the implementation of the
Decode
method ofjson.Decoder
, it buffers the entire JSON value in memory before unmarshalling it into a Go value. So in most cases it won't be any more memory efficient (although this could easily change in a future version of the language). -
So a better rule of thumb is this:
- Use
json.Decoder
if your data is coming from an io.Reader stream, or you need to decode multiple values from a stream of data. res.Body is stream (need to close()) therefore need to use Decoder stead of Unmarshal. - Use
json.Unmarshal
if you already have the JSON data in memory.
- Use
For the case of reading from an HTTP request, I'd pick json.Decoder
since you're obviously reading from a stream. However, in the testing, we already has JSON data in the memeory.
Query with postgres
- To get or create ->
db.QueryRow()
: only 1 row ordb.Query()
: multiple rows - To delete or update ->
db.Exec()
- Scan(), to get more info go to
model.go
:- When get results back from get requests, use
Scan()
to modify current variable with the result db.Query()
will returnsql.rows
:- Always
defer rows.Close()
- To loop over rows:
for rows.Next()
- Inside the for loop, rows variable now will point over each row. To get the value out of row. Use
Scan
- Always
- When get results back from get requests, use
Request Payload vs Form Data
-
Summary:
- Request payload or payload body is sent using
PUT
orPOST
POST /some-path HTTP/1.1 Content-Type: application/json { "foo" : "bar", "name" : "John" }
- Form data is sent using submit form with
POST
POST /some-path HTTP/1.1 Content-Type: application/x-www-form-urlencoded foo=bar&name=John
- Request payload or payload body is sent using
dockerfile
-
FROM golang...
line specifies the base image to start with (this contains the Go tools and libraries, ready to build your program) -
WORKDIR
line like acd
inside the container. If the folder hasn't created, it will be created automatically by docker -
The
COPY
command tells Docker to copy the Golang source code from the current directory into the container. -
Then the
RUN ...
command tells Docker how to build it. -
a second
FROM SCRATCH
line in this Dockerfile, which tells Docker to start again with a fresh, completely empty container image (called a scratch container), and copy our compiled demo program into it. This is the container image that we'll then go on to run later. -
Using a scratch image saves a lot of space, because we don't actually need the Go tools, or anything else, in order to run our compiled program. Using one container for the build, and another for the final image, is called a multistage build.
-
This fresh container gonna copy
/bin/demo
from the previous container.
Documentation
¶
There is no documentation for this package.