├── .github ├── dependbot.yml └── workflows │ └── go.yml ├── .gitignore ├── .golangci-lint.yml ├── README.md ├── code-slides ├── database.db ├── day-1-restful │ ├── handerfunc.go │ ├── httptest.go │ ├── listenandserve.go │ └── muxserver.go └── day-2-databases │ ├── connect-mysql.go │ ├── connect-postgres.go │ ├── connect-sqlite.go │ ├── go-import.sh │ ├── query-data.go │ └── update.sql ├── database ├── README.md ├── demo │ ├── database │ │ ├── db.go │ │ ├── models.go │ │ └── user.sql.go │ ├── main.go │ ├── migrations │ │ └── 0001_game_create.sql │ ├── queries │ │ └── user.sql │ └── sqlc.yaml ├── ex-1-connection │ ├── main.go │ ├── solution │ │ ├── postgres.go │ │ └── sqlite.go │ └── sqlite.db └── ex-2-abstraction │ ├── database │ └── database.go │ └── main.go ├── go.mod ├── go.sum ├── http-quiz ├── question1.go ├── question2.go ├── question3.go ├── question4.go ├── question5.go └── question6.go ├── reliable-webservice-go ├── README.md ├── ex-1-auth │ └── auth.go ├── ex-2-middleware │ └── middleware.go └── ex-3-monitoring │ └── monitoring.go ├── restful-go ├── README.md ├── ex-1-servers │ └── server.go ├── ex-2-fiber │ └── fiber.go ├── ex-2-webframeworks │ └── framework.go ├── ex-3-clients │ └── client.go └── ex-4-tests │ ├── server.go │ ├── server_test.go │ └── solution │ ├── framework.go │ └── framework_test.go └── sql-quiz ├── question1.sql ├── question2.sql ├── question3.sql ├── question4.sql └── question5.sql /.github/dependbot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: 1.23 20 | 21 | # add linting and test step 22 | - name: lint 23 | uses: golangci/golangci-lint-action@v6 24 | with: 25 | only-new-issues: true 26 | - name: test 27 | # run only valid tests 28 | run: go test -v ./resftul-go/... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.swp 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | -------------------------------------------------------------------------------- /.golangci-lint.yml: -------------------------------------------------------------------------------- 1 | # Options for analysis running. 2 | run: 3 | # Timeout for analysis, e.g. 30s, 5m. 4 | # Default: 1m 5 | timeout: 5m 6 | # Exit code when at least one issue was found. 7 | # Default: 1 8 | issues-exit-code: 2 9 | # Include test files or not. 10 | # Default: true 11 | tests: false 12 | # If set we pass it to "go list -mod={option}". From "go help modules": 13 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 14 | # automatic updating of go.mod described above. Instead, it fails when any changes 15 | # to go.mod are needed. This setting is most useful to check that go.mod does 16 | # not need updates, such as in a continuous integration and testing system. 17 | # If invoked with -mod=vendor, the go command assumes that the vendor 18 | # directory holds the correct copies of dependencies and ignores 19 | # the dependency descriptions in go.mod. 20 | # 21 | # Allowed values: readonly|vendor|mod 22 | # By default, it isn't set. 23 | modules-download-mode: readonly 24 | # Define the Go version limit. 25 | # Mainly related to generics support in go1.18. 26 | # Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.17 27 | go: '1.23' 28 | linters: 29 | # Enable specific linter 30 | # https://golangci-lint.run/usage/linters/#enabled-by-default-linters 31 | enable: 32 | - bodyclose 33 | - deadcode 34 | - dogsled 35 | - errcheck 36 | - goconst 37 | - gocyclo 38 | - gofmt 39 | - gosimple 40 | - govet 41 | - importas 42 | - ineffassign 43 | - misspell 44 | - revive 45 | - rowserrcheck 46 | - sqlclosecheck 47 | - staticcheck 48 | - structcheck 49 | - stylecheck 50 | - typecheck 51 | - unused 52 | - varcheck 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Webservices in 3 weeks 2 | 3 | This repo contains the examples and exercises for The O'reilly online learning course[Go Web Development in 3 weeks](https://www.oreilly.com/live-events/go-for-web-development-in-3-weeks/0636920091015/). 4 | 5 | * [![wakatime](https://wakatime.com/badge/user/953eeb5a-d347-44af-9d8b-a5b8a918cecf/project/815add1c-01f3-412e-b6cd-730805338e0e.svg)](https://wakatime.com/badge/user/953eeb5a-d347-44af-9d8b-a5b8a918cecf/project/815add1c-01f3-412e-b6cd-730805338e0e) 6 | 7 | --- 8 | 9 | In this course, you will learn all the steps to build a web service in [Go](https://go.dev/). From starting the service to monitoring your service, it is meant to give you a comprehensive guide for building production-level service. The first section of the course will handle building restful best practices in Go. Communication is key for designing and building your services and is the foundation on which your functionality will be built. The second section is all about databases. Each web service needs a layer to store, fetch, and manipulate the data communicated with it. We need to make sure our data foundations are strong so we maintain the state of our services. The last section is an overview of reliability. This section just goes over reliability basics, but they are vital things that every engineer should include when building a web service. This course does not go over [Go](https://go.dev/) basics. 10 | 11 | ## pre-requisites 12 | 13 | * [Go](https://go.dev/) installed and running 14 | * Working knowledge of go 15 | 16 | ## New to go? 17 | 18 | If you are new to go, work through these exercises first 19 | 20 | * [Golang Zero to Hero](https://github.com/Soypete/Golang_tutorial_zero_to_hero) 21 | * [Tour of Go](https://go.dev/tour/welcome/1) 22 | * [Gophercises](https://gophercises.com/) 23 | 24 | --- 25 | 26 | # Day 1 - Rest API protocols 27 | 28 | * [Exercise 1](restful-go/README.md): std lib listenAndServe 29 | * [Quiz](http-quiz/): Status Codes 30 | * [Exercise 2](restful-go/README.md): Using a web framework 31 | * [Exercise 3](restful-go/README.md): Client to query hosted server 32 | * [Exercise 4](restful-go/README/md): HttpTests 33 | 34 | # Day 2 - Databases for webservices 35 | 36 | * [Exercise 1](database/README.md): Connect to a live database 37 | * [live coding](database/demo/): [SQLC](https://sqlc.dev/) and [goose](https://github.com/pressly/goose) 38 | * [Exercise 2](database/README.md): Add client and interface 39 | * [Exercise 3](database/README.md): Unit tests with mock client 40 | * [SQL quiz](sql-quiz) 41 | 42 | # Day 3 - Metrics and Monitoring 43 | 44 | * [Exercise 1](reliable-webservice-go/README.md): API Auth 45 | * [Exercise 2](reliable-webservice-go/README.md): Middleware 46 | * [Exercise 3](reliable-webservice-go/README.md): add monitoring for endpoints 47 | 48 | --- 49 | 50 | # Companion Service 51 | 52 | This is a live production service that implements the game 20 questions. Code from this service is pulled out for the course exercises. The service code can be found [here](https://github.com/Soypete/golang-cli-game/). 53 | 54 | --- 55 | 56 | ## Explore More 57 | 58 | * [Echo](https://echo.labstack.com/) 59 | * [Chi](https://github.com/go-chi/chi) 60 | * [Gin](https://github.com/gin-gonic/gin) 61 | * [Fiber](https://github.com/gofiber/fiber) 62 | * [Expvar](https://pkg.go.dev/expvar) 63 | * [pgx](https://github.com/jackc/pgx) 64 | * [pg](https://github.com/lib/pq) 65 | * [sqlx](https://github.com/jmoiron/sqlx) 66 | * [sqlc](https://sqlc.dev/) 67 | -------------------------------------------------------------------------------- /code-slides/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soypete/WebServices-in-3-weeks/ca1660c37e9ca3f2122698ca942eef9b08e2847a/code-slides/database.db -------------------------------------------------------------------------------- /code-slides/day-1-restful/handerfunc.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | ) 11 | 12 | func main() { 13 | req, err := http.NewRequest( 14 | "POST", 15 | "http://localhost:8080", 16 | bytes.NewBuffer([]byte("hello world"))) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | req.Header.Set("Content-Type", "text/plain") 21 | httpClient := http.DefaultClient 22 | resp, err := httpClient.Do(req) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | fmt.Println(resp.Status) 27 | } 28 | -------------------------------------------------------------------------------- /code-slides/day-1-restful/httptest.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | // from https://github.com/Soypete/golang-cli-game/blob/main/server/api_test.go 6 | func TestPassGetUsernameEmpty(t *testing.T) { 7 | ... 8 | sPass.Router = setupTestRouter(sPass, t) 9 | w := httptest.NewRecorder() 10 | reqNoUser := httptest.NewRequest("GET", "/register//get", nil) 11 | reqNoUser.Header.Set("Authorization", "Basic Y2FwdGFpbm5vYm9keTE6cGFzc3dvcmQK") 12 | sPass.Router.ServeHTTP(w, reqNoUser) 13 | if status := w.Code; status != http.StatusOK { 14 | t.Errorf("handler returned wrong status code: got %v want %v", 15 | status, http.StatusOK) 16 | } 17 | }ackage main 18 | -------------------------------------------------------------------------------- /code-slides/day-1-restful/listenandserve.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "io" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | func main() { 12 | helloHandler := func( 13 | w http.ResponseWriter, 14 | req *http.Request) { 15 | io.WriteString(w, "Hello, world!\n") 16 | } 17 | 18 | http.HandleFunc("/hello", helloHandler) 19 | log.Fatal(http.ListenAndServe(":8080", nil)) 20 | } 21 | -------------------------------------------------------------------------------- /code-slides/day-1-restful/muxserver.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | type apiHandler struct{} 11 | 12 | func (apiHandler) ServeHTTP( 13 | http.ResponseWriter, 14 | *http.Request) { 15 | } 16 | 17 | func main() { 18 | mux := http.NewServeMux() 19 | mux.Handle("/api/", apiHandler{}) 20 | 21 | mux.HandleFunc("/", func( 22 | w http.ResponseWriter, 23 | req *http.Request) { 24 | if req.URL.Path != "/" { 25 | http.NotFound(w, req) 26 | return 27 | } 28 | fmt.Fprintf(w, "Welcome to the home page!") 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /code-slides/day-2-databases/connect-mysql.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | "github.com/jmoiron/sqlx" 11 | ) 12 | 13 | func main() { 14 | connectionString := "server=127.0.0.1;uid=root;pwd=12345;database=test" 15 | db, err := sqlx.Connect("mysql", connectionString) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | fmt.Println("Connected!") 20 | db.Close() 21 | } 22 | -------------------------------------------------------------------------------- /code-slides/day-2-databases/connect-postgres.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log" 9 | 10 | "github.com/jackc/pgx/v4" 11 | _ "github.com/lib/pq" 12 | ) 13 | 14 | func main() { 15 | connectionString := "postgresql://user:secret@localhost/mydb?sslmode=disable" 16 | db, err := pgx.Connect(context.Background(), connectionString) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | fmt.Println("Connected!") 21 | db.Close() 22 | } 23 | -------------------------------------------------------------------------------- /code-slides/day-2-databases/connect-sqlite.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "fmt" 8 | "log" 9 | 10 | _ "github.com/mattn/go-sqlite3" 11 | ) 12 | 13 | func main() { 14 | fileName := "/database.db" 15 | db, err := sql.Open("sqlite3", fileName) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | if err = db.Ping(); err != nil { 20 | log.Fatal(err) 21 | } 22 | fmt.Println("Connected!") 23 | db.Close() 24 | } 25 | -------------------------------------------------------------------------------- /code-slides/day-2-databases/go-import.sh: -------------------------------------------------------------------------------- 1 | #!bin/bash 2 | # This script will show you how to 3 | # get the go package from github 4 | 5 | go mod init github.com/{username}/repo 6 | go get "github.com/mattn/go-sqlite3" 7 | go mod tidy 8 | 9 | -------------------------------------------------------------------------------- /code-slides/day-2-databases/query-data.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "fmt" 8 | "log" 9 | 10 | _ "github.com/mattn/go-sqlite3" 11 | ) 12 | 13 | func main() { 14 | fileName := "code-slides/database.db" 15 | db, err := sql.Open("sqlite3", fileName) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | // Query 21 | // Best practices for SELECT statements is to not use * 22 | // and instead use the column names 23 | rows, err := db.Query("SELECT id, name, email FROM users") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | defer rows.Close() 28 | 29 | // Iterate over the rows 30 | for rows.Next() { 31 | var id int 32 | var name, email string 33 | err = rows.Scan(&id, &name, &email) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | fmt.Println(id, name, email) 38 | } 39 | 40 | err = rows.Err() 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /code-slides/day-2-databases/update.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS users ( 2 | id INTEGER PRIMARY KEY, 3 | name TEXT, 4 | email TEXT UNIQUE, 5 | password TEXT NOT NULL); 6 | 7 | INSERT INTO users (name, email, password) VALUES 8 | ('John Doe', ' john.doe@email.com', 'opensesame') 9 | INSERT INTO users (name, email, password) VALUES 10 | ('Jane Smith', 'smithjane@email.com', 'opensesame') 11 | 12 | UPDATE users SET name = 'Alfred Doe' WHERE id = 1; 13 | 14 | SELECT * FROM users; 15 | -------------------------------------------------------------------------------- /database/README.md: -------------------------------------------------------------------------------- 1 | # Day 2 Databases and your Go app 2 | 3 | This section of the course contains examples and exercises for effectively leveraging a database in the backend of your Web services. The following are examples of finished exercises for day 2 of the [O'reilly Media Online Learning Course](https://www.oreilly.com/live-events/go-for-web-development-in-3-weeks/0636920091015/). They can be completed by following along with the instruction or independently. 4 | 5 | ## Exercise 1 6 | 7 | In your server project, add your preferred database driver and connect to the database in the main function (if you missed day one's exercises or have them in a different location using the [ex-1-connection/main.go](ex-1-connection/main.go)). After you have connected and verified your connection, explore the database. Make sure to query the database's users table and handle the error. Try running `SELECT`, `INSERT`, and `UPDATE` statements 8 | 9 | ### Follow-up questions: 10 | 11 | * What kind of package organization would make sense for organizing your database logic? 12 | * What database driver did you pick? 13 | * Did the data persist? 14 | 15 | _NOTE_: If you are not using postgres or are completing this independently. You can run many databases locally using docker. Below is an example of running postgres locally in a docker container. 16 | 17 | ``` 18 | docker pull postgres 19 | docker run -e POSTGRES_PASSWORD=postgres -e POSTGRES_USERNAME=postgres -p 5431:5432 postgres 20 | ``` 21 | 22 | After you get docker running in your local environment set up your database. You will need to `CREATE` your tables and `INSERT` data into the table. You can do this in your Go app or via a sql script editor. [psql](https://www.postgresql.org/docs/current/app-psql.html) is postgres's command line tool. 23 | 24 | An example of a go app that connect to a local postgres instance is in [database/ex-1-connection/solution](/database/ex-1-connection/solution/postgres.go). 25 | 26 | ## Live Demo 27 | 28 | Using tools to manage a database. Tools like [goose](https://github.com/pressly/goose) and [sqlc](https://sqlc.dev/) can be used to easily abstract database management into your software stack. The following page is the resulting [code from the live demo](/database/demo/main.go) 29 | 30 | [Demo Recording - sqlc](https://youtu.be/X5VGxx4aQAU) 31 | [Demo Recording - goose](https://youtu.be/3TnEeRttvyo) 32 | 33 | ## Exercise 2 34 | 35 | Build a client and interface around your database connection. You can use your existing main.go file and build new database package for your abstraction, or you can use the template files found in [database/ex-2-abstraction](/database/ex-2-abstraction/main.go). Make sure to create an interface, a User struct, a database client, and the methods to create, update, and query the User data. If you have any questions please put them in the chat. 36 | 37 | Follow-up Questions: 38 | 39 | * what are the differences between manually creating a database object vs generating one with sqlc? 40 | 41 | ## Exercise 3 mock database 42 | 43 | Using your new database interface mock the database functions into your [tests from last week](../restful-go/ex-4-tests/solution/framework_test.go). The goal is to imitate db interactions without connecting to the db. You will need to add the DB package to the same repo that your server lives in. 44 | 45 | [Here](https://github.com/Soypete/golang-cli-game/blob/24dc57852dee27bb17120555d3d390bd17a78d13/server/api_test.go#L14) are some working tests that use `passBD{}` and `failDB{}` to mock database functionality in an API test. 46 | -------------------------------------------------------------------------------- /database/demo/database/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.18.0 4 | 5 | package database 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/demo/database/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.18.0 4 | 5 | package database 6 | 7 | import ( 8 | "database/sql" 9 | "time" 10 | ) 11 | 12 | type Game struct { 13 | ID int32 14 | Host string 15 | Players []string 16 | Answer string 17 | Questions []string 18 | Guesses []string 19 | StartTime time.Time 20 | EndTime sql.NullTime 21 | Ended sql.NullBool 22 | } 23 | 24 | type Guess struct { 25 | ID int32 26 | Guess string 27 | UserID int32 28 | GameID int32 29 | Correct sql.NullBool 30 | } 31 | 32 | type Question struct { 33 | ID int32 34 | Question string 35 | UserID int32 36 | GameID int32 37 | } 38 | 39 | type User struct { 40 | ID int32 41 | Username string 42 | CreatedAt time.Time 43 | } 44 | -------------------------------------------------------------------------------- /database/demo/database/user.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.18.0 4 | // source: user.sql 5 | 6 | package database 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const deleteUser = `-- name: DeleteUser :exec 13 | DELETE FROM users 14 | WHERE username = $1 15 | ` 16 | 17 | func (q *Queries) DeleteUser(ctx context.Context, username string) error { 18 | _, err := q.db.ExecContext(ctx, deleteUser, username) 19 | return err 20 | } 21 | 22 | const getUser = `-- name: GetUser :one 23 | SELECT id, username, created_at FROM users 24 | WHERE username = $1 25 | ` 26 | 27 | func (q *Queries) GetUser(ctx context.Context, username string) (User, error) { 28 | row := q.db.QueryRowContext(ctx, getUser, username) 29 | var i User 30 | err := row.Scan(&i.ID, &i.Username, &i.CreatedAt) 31 | return i, err 32 | } 33 | 34 | const updateUser = `-- name: UpdateUser :exec 35 | INSERT INTO users 36 | (username) VALUES ($1) 37 | ON CONFLICT (username) 38 | DO NOTHING 39 | ` 40 | 41 | func (q *Queries) UpdateUser(ctx context.Context, username string) error { 42 | _, err := q.db.ExecContext(ctx, updateUser, username) 43 | return err 44 | } 45 | -------------------------------------------------------------------------------- /database/demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "embed" 7 | "fmt" 8 | "net/url" 9 | 10 | "github.com/pressly/goose/v3" 11 | "github.com/soypete/WebServices-in-3-weeks/database/demo/database" 12 | ) 13 | 14 | //go:embed migrations/*.sql 15 | var embedMigrations embed.FS 16 | 17 | func main() { 18 | params := url.Values{} 19 | params.Set("sslmode", "disable") 20 | 21 | // this is a personal preference to use url.URL to 22 | // build up the connection string. This works well for 23 | // postgres, but other drivers might have their own quirks. 24 | connectionString := url.URL{ 25 | Scheme: "postgresql", 26 | User: url.UserPassword("postgres", "postgres"), 27 | Host: "localhost:5431", 28 | Path: "postgres", 29 | RawQuery: params.Encode(), 30 | } 31 | 32 | db, err := sql.Open("postgres", connectionString.String()) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | goose.SetBaseFS(embedMigrations) 38 | 39 | if err := goose.SetDialect("postgres"); err != nil { 40 | panic(err) 41 | } 42 | 43 | if err := goose.Up(db, "migrations"); err != nil { 44 | panic(err) 45 | } 46 | 47 | queires := database.New(db) 48 | 49 | user, err := queires.GetUser(context.Background(), "pete") 50 | if err != nil { 51 | panic(err) 52 | } 53 | fmt.Println(user) 54 | } 55 | -------------------------------------------------------------------------------- /database/demo/migrations/0001_game_create.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | CREATE TABLE IF NOT EXISTS users ( 3 | id SERIAL PRIMARY KEY, 4 | username VARCHAR(255) UNIQUE NOT NULL, 5 | created_at TIMESTAMP NOT NULL DEFAULT NOW() 6 | ); 7 | 8 | CREATE TABLE IF NOT EXISTS questions ( 9 | id SERIAL PRIMARY KEY, 10 | question VARCHAR(255) NOT NULL, 11 | user_id INTEGER NOT NULL references users(id), 12 | game_id INTEGER NOT NULL references games(id) 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS guesses ( 16 | id SERIAL PRIMARY KEY, 17 | guess VARCHAR(255) NOT NULL, 18 | user_id INTEGER NOT NULL references users(id), 19 | game_id INTEGER NOT NULL references games(id), 20 | correct BOOLEAN DEFAULT FALSE 21 | ); 22 | 23 | CREATE TABLE IF NOT EXISTS games ( 24 | id SERIAL PRIMARY KEY, 25 | host VARCHAR(255) NOT NULL, 26 | players VARCHAR(255)[5] NOT NULL references users(id), 27 | answer VARCHAR(255) NOT NULL, 28 | questions VARCHAR(255)[] references questions(id) , 29 | guesses VARCHAR(255)[] references guesses(id), 30 | start_time TIMESTAMP NOT NULL DEFAULT NOW(), 31 | end_time TIMESTAMP, 32 | ended BOOLEAN DEFAULT FALSE 33 | ); 34 | 35 | -- +goose Down 36 | 37 | DROP TABLE IF EXISTS users; 38 | DROP TABLE IF EXISTS questions; 39 | DROP TABLE IF EXISTS guesses; 40 | DROP TABLE IF EXISTS games; 41 | 42 | -------------------------------------------------------------------------------- /database/demo/queries/user.sql: -------------------------------------------------------------------------------- 1 | -- name: GetUser :one 2 | SELECT * FROM users 3 | WHERE username = $1; 4 | 5 | -- name: UpdateUser :exec 6 | INSERT INTO users 7 | (username) VALUES ($1) 8 | ON CONFLICT (username) 9 | DO NOTHING; 10 | 11 | -- name: DeleteUser :exec 12 | DELETE FROM users 13 | WHERE username = $1; 14 | 15 | -------------------------------------------------------------------------------- /database/demo/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - engine: "postgresql" 4 | queries: "queries/user.sql" 5 | schema: "migrations/0001_game_create.sql" 6 | gen: 7 | go: 8 | package: "database" 9 | out: "database" 10 | -------------------------------------------------------------------------------- /database/ex-1-connection/main.go: -------------------------------------------------------------------------------- 1 | // to run postgres locally, you can use docker 2 | // docker pull postgres 3 | // docker run --name postgres -e POSTGRES_PASSWORD=postgres -p 5431:5431 -d postgres 4 | // 5 | // otherwise use the sqlite provided in the repo 6 | 7 | package main 8 | 9 | // add driver here 10 | 11 | func main() { 12 | connectionString := "/database.db" 13 | // TODO: connect to sqlite database here 14 | 15 | // TODO: query the database here 16 | db.Close() 17 | } 18 | -------------------------------------------------------------------------------- /database/ex-1-connection/solution/postgres.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/jmoiron/sqlx" 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | func main() { 12 | params := url.Values{} 13 | params.Set("sslmode", "disable") 14 | 15 | // this is a personal preference to use url.URL to 16 | // build up the connection string. This works well for 17 | // postgres, but other drivers might have their own quirks. 18 | connectionString := url.URL{ 19 | Scheme: "postgresql", 20 | User: url.UserPassword("postgres", "postgres"), 21 | Host: "localhost:5431", 22 | Path: "postgres", 23 | RawQuery: params.Encode(), 24 | } 25 | 26 | db, err := sqlx.Connect("postgres", connectionString.String()) 27 | if err != nil { 28 | panic(err) 29 | } 30 | fmt.Println(db.Ping()) 31 | } 32 | -------------------------------------------------------------------------------- /database/ex-1-connection/solution/sqlite.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | 8 | _ "github.com/mattn/go-sqlite3" 9 | ) 10 | 11 | func main() { 12 | fileName := "database/ex-1-connection/solution/sqlite.db" 13 | db, err := sql.Open("sqlite3", fileName) 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | if err = db.Ping(); err != nil { 18 | log.Fatal(err) 19 | } 20 | fmt.Println("Connected!") 21 | 22 | _, err = db.Exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT UNIQUE, password TEXT NOT NULL)") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('John Doe', ' john.doe@email.com', 'opensesame')") 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Jane Smith', 'smithjane@email.com', 'opensesame')") 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Robert Johnson', 'me@robertjohnson.com', 'opensesame')") 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Emily Davis', 'emily_davis@email.com', 'opensesame')") 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Michael Wilson', 'mwilson@email.com', 'opensesame')") 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Sarah Brown', 'sbbrown@email.com', 'opensesame')") 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('William Jones', 'whjones@email.com', 'opensesame')") 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Olivia Taylor', 'olivia.taylord@email.com', 'opensesame')") 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('David Evans', 'david@evans.com', 'opensesame')") 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Sophia Miller', 'sophiamiller@sophiamiller.com', 'opensesame')") 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('James Wilson', 'jamesthewil@email.com', 'opensesame')") 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Ava Martin', 'avamart@email.com', 'opensesame')") 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Charles Anderson', 'charles@anderson', 'opensesame')") 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Mia Martinez', 'miacasaessucasa@martinez.com', 'opensesame')") 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Joseph Thomas', 'joe@thomas.com', 'opensesame')") 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Chloe White', 'chloewhite@email.com', 'opensesame')") 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Daniel Harris', 'test@user.com', 'opensesame')") 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Emma Moore', 'emma_more@test.com', 'opensesame')") 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Matthew Clark', 'matt_clark@email.com', 'opensesame')") 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | _, err = db.Exec("INSERT INTO users (name, email, password) VALUES ('Harper Lewis', 'harp@lewis.com', 'opensesame')") 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | 107 | rows, err := db.Query("SELECT * FROM users") 108 | if err != nil { 109 | log.Fatal(err) 110 | } 111 | defer rows.Close() 112 | for rows.Next() { 113 | var id int 114 | var name string 115 | var email string 116 | var password string 117 | rows.Scan(&id, &name, &email, &password) 118 | fmt.Println(id, name, email, password) 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /database/ex-1-connection/sqlite.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Soypete/WebServices-in-3-weeks/ca1660c37e9ca3f2122698ca942eef9b08e2847a/database/ex-1-connection/sqlite.db -------------------------------------------------------------------------------- /database/ex-2-abstraction/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | ) 7 | 8 | // Connection struct holds the database connection 9 | type Connection struct { 10 | db *sql.DB 11 | } 12 | 13 | // User struct holds the user data 14 | type User struct { 15 | //TODO: add user fields 16 | } 17 | 18 | type Connector interface { 19 | QueryUser() (*User, error) 20 | UpdateUser() error 21 | AddUser() error 22 | } 23 | 24 | // Connect to database and return a Connection struct. 25 | // This return value needs to be a pointer because we are 26 | // using it to implement the Connector interface. 27 | func Connect() (*Connection, error) { 28 | fileName := "database/ex-1-connection/sqlite.db" 29 | db, err := sql.Open("sqlite3", fileName) 30 | if err != nil { 31 | return nil, fmt.Errorf("cannot open sqlite connection: %w", err) 32 | } 33 | if err = db.Ping(); err != nil { 34 | return nil, fmt.Errorf("cannot ping database: %w", err) 35 | } 36 | return &Connection{db: db}, nil 37 | } 38 | 39 | func (c *Connection) QueryUser() (*User, error) { 40 | // TODO: query database 41 | } 42 | 43 | func (c *Connection) UpdateUser() error { 44 | // TODO: update database 45 | } 46 | 47 | func (c *Connection) AddUser() error { 48 | // TODO: add user to database 49 | } 50 | -------------------------------------------------------------------------------- /database/ex-2-abstraction/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/soypete/WebServices-in-3-weeks/database/ex-2-abstraction/database" 7 | ) 8 | 9 | func main() { 10 | 11 | // this will enforge the interface 12 | var db database.Connector 13 | db, err := database.Connect() 14 | if err != nil { 15 | log.Fatal("Error connecting to database") 16 | } 17 | 18 | // call your client methods here 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/soypete/WebServices-in-3-weeks 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/go-chi/chi v1.5.4 7 | github.com/jmoiron/sqlx v1.3.5 8 | ) 9 | 10 | require ( 11 | github.com/gofiber/fiber/v2 v2.52.1 12 | github.com/lib/pq v1.10.9 13 | github.com/mattn/go-sqlite3 v1.14.17 14 | github.com/pressly/goose/v3 v3.11.2 15 | ) 16 | 17 | require ( 18 | github.com/andybalholm/brotli v1.0.5 // indirect 19 | github.com/google/uuid v1.5.0 // indirect 20 | github.com/klauspost/compress v1.17.0 // indirect 21 | github.com/mattn/go-colorable v0.1.13 // indirect 22 | github.com/mattn/go-isatty v0.0.20 // indirect 23 | github.com/mattn/go-runewidth v0.0.15 // indirect 24 | github.com/rivo/uniseg v0.2.0 // indirect 25 | github.com/valyala/bytebufferpool v1.0.0 // indirect 26 | github.com/valyala/fasthttp v1.51.0 // indirect 27 | github.com/valyala/tcplisten v1.0.0 // indirect 28 | golang.org/x/sys v0.15.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 4 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 5 | github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= 6 | github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= 7 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 8 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 9 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 10 | github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI= 11 | github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 12 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 13 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 14 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 15 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 16 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 17 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 18 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 19 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 20 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 21 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 22 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 23 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 24 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 25 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 26 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 27 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 28 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 29 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 30 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 31 | github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= 32 | github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 33 | github.com/pressly/goose/v3 v3.11.2 h1:QgTP45FhBBHdmf7hWKlbWFHtwPtxo0phSDkwDKGUrYs= 34 | github.com/pressly/goose/v3 v3.11.2/go.mod h1:LWQzSc4vwfHA/3B8getTp8g3J5Z8tFBxgxinmGlMlJk= 35 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 36 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 37 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 38 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 39 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 40 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 41 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 42 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 43 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 44 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 45 | golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 46 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 47 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 50 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 51 | golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= 52 | golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= 53 | lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= 54 | lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 55 | modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= 56 | modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= 57 | modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= 58 | modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= 59 | modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= 60 | modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= 61 | modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 62 | modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 63 | modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= 64 | modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 65 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 66 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 67 | modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= 68 | modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= 69 | modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= 70 | modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= 71 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 72 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 73 | -------------------------------------------------------------------------------- /http-quiz/question1.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | // 4 | // test with curl localhost:8080/ 5 | // expected output: Hello World! status 200 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "net/http" 12 | ) 13 | 14 | func main() { 15 | 16 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 17 | fmt.Fprintf(w, "Hello World!") 18 | }) 19 | http.ListenAndServe(":8080", nil) 20 | } 21 | -------------------------------------------------------------------------------- /http-quiz/question2.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | // 4 | // test with curl localhost:8080/hello\?name=World 5 | // expected output: Hello World! status 200 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "net/http" 12 | ) 13 | 14 | func main() { 15 | 16 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 17 | name := r.URL.Query().Get("name") 18 | if name == "" { 19 | http.Error(w, "missing name", http.StatusBadRequest) 20 | return 21 | } 22 | fmt.Fprintf(w, "Hello %s!", name) 23 | }) 24 | http.ListenAndServe(":8080", nil) 25 | } 26 | -------------------------------------------------------------------------------- /http-quiz/question3.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | // 4 | // test with curl localhost:8080/hello 5 | // expected output: Hello World! status 400 6 | // follow up question: should it return a different status code or error message? 7 | // What other status codes does this service return? 8 | 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "net/http" 14 | ) 15 | 16 | func main() { 17 | 18 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 19 | name := r.URL.Query().Get("name") 20 | if name == "" { 21 | http.Error(w, "missing name", http.StatusBadRequest) 22 | return 23 | } 24 | fmt.Fprintf(w, "Hello %s!", name) 25 | }) 26 | http.ListenAndServe(":8080", nil) 27 | } 28 | -------------------------------------------------------------------------------- /http-quiz/question4.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | // 4 | // test with curl -X localhost:8080/ -d "World!" -H "Content-Type: text/plain" 5 | // expected output: Hello World! status 200 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "net/http" 13 | ) 14 | 15 | func main() { 16 | 17 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 18 | b, err := io.ReadAll(r.Body) 19 | if err != nil { 20 | http.Error(w, err.Error(), http.StatusInternalServerError) 21 | return 22 | } 23 | defer r.Body.Close() 24 | 25 | contentType := r.Header.Get("Content-Type") 26 | if contentType != "text/plain" { 27 | http.Error(w, "Content-Type header is incorrect", http.StatusBadRequest) 28 | return 29 | } 30 | fmt.Fprintf(w, "Hello %s", b) 31 | 32 | }) 33 | 34 | http.ListenAndServe(":8080", nil) 35 | } 36 | -------------------------------------------------------------------------------- /http-quiz/question5.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | // 4 | // test with curl -I -X POST localhost:8080/ -d "World!" 5 | // expected output: Content-Type header is incorrect 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "net/http" 13 | ) 14 | 15 | func main() { 16 | 17 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 18 | b, err := io.ReadAll(r.Body) 19 | if err != nil { 20 | http.Error(w, err.Error(), http.StatusInternalServerError) 21 | return 22 | } 23 | defer r.Body.Close() 24 | 25 | contentType := r.Header.Get("Content-Type") 26 | if contentType != "text/plain" { 27 | http.Error(w, "Content-Type header is incorrect", http.StatusBadRequest) 28 | return 29 | } 30 | fmt.Fprintf(w, "Hello %s", b) 31 | 32 | }) 33 | 34 | http.ListenAndServe(":8080", nil) 35 | } 36 | -------------------------------------------------------------------------------- /http-quiz/question6.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | // 4 | // test with curl -I -X POST localhost:8080/ -H "Content-Type: text/plain" 5 | // expected output: Hello 6 | // Follow up: What are some ways to improve the code? 7 | // Follow up: what are some safety concerns with the code? 8 | // Follow up: what are some ways to handle errors? 9 | 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "io" 15 | "net/http" 16 | ) 17 | 18 | func main() { 19 | 20 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 21 | b, err := io.ReadAll(r.Body) 22 | if err != nil { 23 | http.Error(w, err.Error(), http.StatusInternalServerError) 24 | return 25 | } 26 | defer r.Body.Close() 27 | 28 | contentType := r.Header.Get("Content-Type") 29 | if contentType != "text/plain" { 30 | http.Error(w, "Content-Type header is incorrect", http.StatusBadRequest) 31 | return 32 | } 33 | fmt.Fprintf(w, "Hello %s", b) 34 | 35 | }) 36 | 37 | http.ListenAndServe(":8080", nil) 38 | } 39 | -------------------------------------------------------------------------------- /reliable-webservice-go/README.md: -------------------------------------------------------------------------------- 1 | # Day 3: Building Reliable Web Services 2 | 3 | These are the code-samples, exercises, quizzes, and links needed for this course. To complete these exercises follow the instructions in this doc. In the subdirectories of this section are example solutions. Your solutions do not need to match the provided solutions. The goal of the exercises is to learn and understand why things are implemented so that a participant apply principles to any framework or problem set. 4 | 5 | ## Exercises 6 | 7 | ### [Exercise 1](/reliable-webservice-go/ex-1-auth/auth.go) Add auth to your server endpoints 8 | 9 | Auth tooling is sometimes the first or last measure of security for your endpoints. There are various methods for adding auth to your server endpoint. When building production services the methods you choose for authentication and authorization will be determined by security professionals, but how you implement them is up to you as a developer. 10 | 11 | Add an auth method to your server. You can use any method you like such as a middle-ware, a helper functions or, by manually adding the logic to a single function. 12 | 13 | Here are some examples of how to add different kinds of auth in your apps. You can pick one to use as a reference for you code. 14 | 15 | * [chi middleware](https://github.com/go-chi/chi/blob/master/middleware/basic_auth.go) 16 | * [manual basic token](https://github.com/Soypete/golang-cli-game/blob/main/server/helpers.go#L36) 17 | * [JWT](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-Parse-Hmac) 18 | * [go-JWT example package](https://pkg.go.dev/github.com/golang-jwt/jwt/v5) 19 | * [Go-guardian](https://github.com/shaj13/go-guardian/tree/master/_examples) 20 | * [Oauth twitter](https://github.com/forgeutah/tweet_automated_bot/blob/main/client/setup.go) 21 | * [Oauth2 golang.com/x](https://github.com/Soypete/Meetup-chat-server/blob/main/twitch/auth.go) 22 | 23 | ### [Exercise 2](/reliable-webservice-go/ex-2-middleware/middleware.go) Add middleware to your go server 24 | 25 | _Middleware_: _Middleware also refers to the software that separates two or more APIs and provides services such as rate-limiting, authentication, and logging._[wikipedia](https://en.wikipedia.org/wiki/Middleware) The implementation is typically “built-in” functions. In Go, this tends to be platform style tooling shared across the organization. It allows you to add complex functionality to your endpoints. 26 | 27 | Using the same web frameworks you used for your web server or the go standard library, add a middleware function to your server. You can use middleware to add metrics, auth, profiling or custom logic to your programs. In this exercise add logging, retry, rate limiting or replace the auth from exercise 1 with a middleware. 28 | 29 | Below are framework docs, they will contain examples of build in middleware that you can add with single line functions. They also show you ways of adding custom middleware to your services. 30 | 31 | Web frameworks: 32 | 33 | * [Chi](https://github.com/go-chi/chi) 34 | * [Gin](https://github.com/gin-gonic/gin) 35 | * [Fiber](https://github.com/gofiber/fiber) 36 | 37 | Here is an [example](https://github.com/Soypete/golang-cli-game/blob/main/server/setup.go) of setting it up using the [chi](https://pkg.go.dev/github.com/go-chi/chi) framework 38 | 39 | ```go 40 | r := chi.NewRouter() 41 | 42 | // add prebuilt middleware for all requests 43 | r.Use(middleware.Logger) 44 | r.Use(middleware.RequestID) 45 | r.Use(middleware.RealIP) 46 | r.Use(middleware.Recoverer) 47 | ``` 48 | 49 | ### Pprof live Demo 50 | 51 | Pprof is an incredible profiling tool. It is the only tool currently provided to in the standard library what will let you follow memory hot path. 52 | If you plan on using pprof as part of your monitoring suit you will need to install [graphviz](https://graphviz.org/download/) first. 53 | 54 | [Pprof YouTube video](https://youtu.be/KzivSSjnBls) 55 | 56 | For more information check out this talk, [Pprof for beginners](https://www.youtube.com/watch?v=HjzJ5r2D8ZM) 57 | 58 | ### [Exercise 3](/reliable-webservice-go/ex-3-monitoring/monitoring.go) Add some monitoring endpoints to your server 59 | 60 | Monitoring is often setup as part of the middleware for commonly used metrics like db calls and http status codes. Often there are other metrics that should be added to track specific business logic and functionality. [Expvars](https://pkg.go.dev/expvar) are provided by the go standard library as a method for exposing metrics to an endpoint where they can be read via a web browser or consumed by a tracking service. 61 | 62 | [Prometheus](https://prometheus.io/docs/guides/go-application/) is a very common opensource solution for adding metrics to your web services. It adds metrics to end points that can be scraped into a prometheus instance. 63 | 64 | _NOTE:_ In this exercise it is not intended to have a prometheus instance up and running, just to set up the endpoint where you can manually view the metrics. 65 | 66 | Using Expvars and/or Prometheus SDK add some custom metrics. 67 | 68 | [Example](https://github.com/Soypete/golang-cli-game/blob/main/server/setup.go#L53) 69 | 70 | ```go 71 | reg := prometheus.NewRegistry() 72 | reg.MustRegister(collectors.NewBuildInfoCollector()) 73 | reg.MustRegister(collectors.NewDBStatsCollector(db.GetSqlDB(), "postgres")) 74 | reg.MustRegister(collectors.NewExpvarCollector( 75 | map[string]*prometheus.Desc{ 76 | "counter200Code": prometheus.NewDesc("expvar_200Status", "number of status 200 api calls", nil, nil), 77 | "counter400Code": prometheus.NewDesc("expvar_400status", "number of status 400 api calls", nil, nil), 78 | "counter500Code": prometheus.NewDesc("expvar_500status", "number of status 500 api calls", nil, nil), 79 | }, 80 | )) 81 | 82 | // add prometheus endpoint at /metrics. The above collectors will be shown 83 | // in the reverse order they are registered. 84 | r.Mount("/metrics", promhttp.HandlerFor( 85 | reg, 86 | promhttp.HandlerOpts{ 87 | // Opt into OpenMetrics to support exemplars. 88 | EnableOpenMetrics: true, 89 | }, 90 | )) 91 | ``` 92 | 93 | #### Bonus exercise: Add Pprof 94 | 95 | Add pprof to your service to see how it uses memory when handling API calls. Run pprof and see what insights are available to you. 96 | 97 | First add the pprof driver to your app. 98 | 99 | ```go 100 | import _ "net/http/pprof" 101 | ``` 102 | 103 | _*NOTE*: the "\_" means that the import is added globally as a backend system. This is common for servers, db drivers, etc_ 104 | 105 | Add a pprof server as its own goroutine in your main function. 106 | 107 | ```go 108 | // run pprof 109 | go func() { 110 | http.ListenAndServe("localhost:6060", nil) 111 | }() 112 | ``` 113 | 114 | Install [graphviz](https://graphviz.org/download/) on your machine to get the visual insights. 115 | 116 | _Mac:_ 117 | 118 | ```bash 119 | brew install graphviz 120 | ``` 121 | 122 | run pprof while your worker-pool is executing 123 | 124 | ```bash 125 | go tool pprof -http=:18080 http://localhost:6060/debug/pprof/profile?seconds=30 126 | ``` 127 | 128 | In the default graph each node is a function that your program is running. Size and color indicate how much CPU and time each function is taking. 129 | 130 | to access the command-line tool run 131 | 132 | ```bash 133 | go tool pprof http://localhost:6060/debug/pprof/allocs 134 | ``` 135 | -------------------------------------------------------------------------------- /reliable-webservice-go/ex-1-auth/auth.go: -------------------------------------------------------------------------------- 1 | // Auth middleware for basic auth 2 | // to test: curl -H "Authorization:Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" http://localhost:8080 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | // check if the that username and password are provided 12 | func authMiddleware(w http.ResponseWriter, r *http.Request) { 13 | //https://pkg.go.dev/net/http#Request.BasicAuth 14 | username, password, ok := r.BasicAuth() 15 | if !ok { 16 | http.Error(w, http.StatusText(http.StatusBadRequest)+", Authorization header must be in the form username:password", http.StatusBadRequest) 17 | return 18 | } 19 | // Add logic to check if the username and password are correct or not 20 | // This is often done by checking the username and password against a database 21 | // For the sake of simplicity, we will hardcode the username and password 22 | if username != "Aladdin" || password != "open sesame" { 23 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 24 | return 25 | } 26 | } 27 | 28 | func handler(w http.ResponseWriter, r *http.Request) { 29 | authMiddleware(w, r) 30 | fmt.Fprintf(w, "Hello") 31 | } 32 | 33 | func main() { 34 | http.HandleFunc("/", handler) 35 | http.ListenAndServe(":8080", nil) 36 | } 37 | -------------------------------------------------------------------------------- /reliable-webservice-go/ex-2-middleware/middleware.go: -------------------------------------------------------------------------------- 1 | // to test: curl -H "Authorization:Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" http://localhost:8080/hello/Aladdin 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/go-chi/chi" 9 | "github.com/go-chi/chi/middleware" 10 | ) 11 | 12 | func middlewareHandler(next http.Handler) http.Handler { 13 | return authHandler(next) 14 | } 15 | func authHandler(next http.Handler) http.HandlerFunc { 16 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | //https://pkg.go.dev/net/http#Request.BasicAuth 18 | user, pass, ok := r.BasicAuth() 19 | if !ok { 20 | fmt.Println("authHandler: !ok") 21 | http.Error(w, http.StatusText(http.StatusBadRequest)+", Authorization header must be in the form username:password", http.StatusBadRequest) 22 | return 23 | } 24 | if user != "Aladdin" || pass != "open sesame" { 25 | fmt.Println("authHandler: user != Aladdin || pass != open sesame") 26 | http.Error(w, "authorization failed", http.StatusUnauthorized) 27 | return 28 | } 29 | next.ServeHTTP(w, r) 30 | }) 31 | } 32 | 33 | func handler(w http.ResponseWriter, r *http.Request) { 34 | w.WriteHeader(http.StatusOK) 35 | fmt.Fprintf(w, "Hello World!\n") 36 | } 37 | 38 | func main() { 39 | r := chi.NewRouter() 40 | 41 | // add prebuild middleware for all requests 42 | r.Use(middleware.Logger) 43 | r.Use(middleware.RequestID) 44 | r.Use(middleware.RealIP) 45 | r.Use(middleware.Recoverer) 46 | r.Get("/hello", handler) 47 | r.With(middlewareHandler).Route("/middleware", func(r chi.Router) { 48 | r.Get("/hello", handler) 49 | }) 50 | 51 | http.ListenAndServe(":8080", r) 52 | 53 | } 54 | -------------------------------------------------------------------------------- /reliable-webservice-go/ex-3-monitoring/monitoring.go: -------------------------------------------------------------------------------- 1 | // to test: curl -H "Authorization:Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" http://localhost:8080/hello/Aladdin 2 | package main 3 | 4 | import ( 5 | "expvar" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/go-chi/chi" 10 | "github.com/go-chi/chi/middleware" 11 | ) 12 | 13 | var ( 14 | // expvar counters for the number of status codes returned 15 | counter200Code expvar.Int 16 | counter400Code expvar.Int 17 | counter500Code expvar.Int 18 | ) 19 | 20 | func middlewareHandler(next http.Handler) http.Handler { 21 | return authHandler(next) 22 | } 23 | func authHandler(next http.Handler) http.HandlerFunc { 24 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | //https://pkg.go.dev/net/http#Request.BasicAuth 26 | user, pass, ok := r.BasicAuth() 27 | if !ok { 28 | fmt.Println("authHandler: !ok") 29 | counter400Code.Add(1) 30 | http.Error(w, http.StatusText(http.StatusBadRequest)+", Authorization header must be in the form username:password", http.StatusBadRequest) 31 | return 32 | } 33 | if user != "Aladdin" || pass != "open sesame" { 34 | fmt.Println("authHandler: user != Aladdin || pass != open sesame") 35 | counter400Code.Add(1) 36 | http.Error(w, "authorization failed", http.StatusUnauthorized) 37 | return 38 | } 39 | next.ServeHTTP(w, r) 40 | }) 41 | } 42 | 43 | func handler(w http.ResponseWriter, r *http.Request) { 44 | w.WriteHeader(http.StatusOK) 45 | counter200Code.Add(1) 46 | fmt.Fprintf(w, "Hello World!\n") 47 | } 48 | 49 | func main() { 50 | r := chi.NewRouter() 51 | 52 | // add prebuild middleware for all requests 53 | r.Use(middleware.Logger) 54 | r.Use(middleware.RequestID) 55 | r.Use(middleware.RealIP) 56 | r.Use(middleware.Recoverer) 57 | r.Get("/hello", handler) 58 | r.With(middlewareHandler).Route("/middleware", func(r chi.Router) { 59 | r.Get("/hello", handler) 60 | }) 61 | // add expvars at /debug/vars 62 | expvar.Publish("counter200Code", &counter200Code) 63 | expvar.Publish("counter400Code", &counter400Code) 64 | // add pprof at /debug/ 65 | r.Mount("/debug", middleware.Profiler()) 66 | 67 | http.ListenAndServe(":8080", r) 68 | 69 | } 70 | -------------------------------------------------------------------------------- /restful-go/README.md: -------------------------------------------------------------------------------- 1 | # Day 1: Writing Restful Golang Servers and Client 2 | 3 | These are the code-samples, exercises, quizzes, and links needed for this course. To complete these exercises follow the instructions in this doc. This section has one folder for each exercise/solutions. Your solutions do not need to match the provided solutions. The goal of these exercises is to learn and understand why things are implemented, so that a participant can apply principles to any framework or problem set. 4 | 5 | ## Exercises 6 | 7 | ### [Exercise 1](/restful-go/ex-1-servers/server.go) Build a Standard Library Server 8 | 9 | In a new `main.go` file, create a web server using the `ListenAndServe()` function. This server should have at least one endpoint that accepts a parameter and returns it in a response. An example of a finished exercise in [ex-1-server](ex-1-servers/server.go) 10 | 11 | * reference: [go-by-example Https server](https://gobyexample.com/http-servers) 12 | 13 | _NOTE_: `ListenAndServe()` is great for prototyping and non-production development. When you are building out your services, make sure you have security in mind. Use a custom `mux`. We will learn more about security best practices later on in the course. Check out [Reliable web services](../reliable-webservice-go/README.md). 14 | 15 | --- 16 | 17 | ### [Exercise 2](/restful-go/ex-2-webframeworks/framework.go) Build a Server Using an open source Framework 18 | 19 | The [Standard Library](https://pkg.go.dev/net/http) provides all the functionality needed to build a robust web server, but sometimes, instead of building out your own infrastructure, there is a benefit to adopting an open source frameworks at the backbone of your server infrastructure. This exercise is you chance to experiment with some popular Go web frameworks. 20 | 21 | _Instructions:_ Pick a web framework and implement a server that can accept a username, validate it, and print the username to standard out. Add logic to handle duplicate usernames, empty username parameters, or any additional error cases. When a username is accepted and printed to standard out return a 200 class status code. If the parameter is empty or invalid return a 400 class error code. Handle any other errors. _We will connect this server to a database in the Day 2 exercises_ 22 | 23 | Examples of the finished exercise: 24 | 25 | * using the [Chi web framework](/restful-go/ex-2-webframeworks/framework.go). 26 | * using the [Fiber web framework](/restful-go/ex-2-fiber/fiber.go). 27 | 28 | _*Note:* you do not have to do this with a web framework. Feel free to use the standard library `net/http` tooling if you are more comfortable. The options for using a web framework are just to try you hand at evaluating popular open source tools. But just like adding any tool to your software start there are risks and rewards._ 29 | 30 | Here are some examples of open source frameworks: 31 | 32 | * [Chi](https://github.com/go-chi/chi) 33 | * [Gin](https://github.com/gin-gonic/gin) 34 | * [Fiber](https://github.com/gofiber/fiber) 35 | 36 | --- 37 | 38 | ### [Exercise 3](/restful-go/ex-3-clients/client.go) Build a Go app that calls your new endpoint. 39 | 40 | Using the standard library to create a client that calls your new web endpoint. To connect to your server, you will need to run your server in one terminal window and the client app in a different terminal window. 41 | 42 | _Extra Practice:_ Lots of companies use [Postman](https://www.postman.com/) to test their service endpoints. Build a client in postman that will call your server. Save your postman solution and generate a go client from the [Postman UI](https://learning.postman.com/docs/sending-requests/generate-code-snippets/). Does your generated go code match the one you build? 43 | 44 | --- 45 | 46 | ### [Exercise 4](/restful-go/ex-4-tests/solution/framework_test.go) 47 | 48 | Using the [httptest](https://pkg.go.dev/net/http/httptest#example-ResponseRecorder) package add some tests endpoints in [server.go](ex-4-tests/server.go). You can use the file [server_test.go](ex-4-tests/server_test.go) to implement the pass and fail checks for each of the endpoints. You should have one test that checks for a 200s class error and one that tests for a 400s class error. 49 | 50 | _Extra Practice:_ If you prefer one of the frameworks we used in the example try writing tests for it. Using the endpoints you wrote in `Exercise 3` write tests to simulate a _200_ class status code, a _400_ class status code, and a _500_ class status code. 51 | Here are some examples of tests for the above open source frameworks. 52 | 53 | * [Chi tests](https://go-chi.io/#/pages/testing) 54 | * [Gin tests](https://gin-gonic.com/docs/testing/) 55 | * [Fiber tests](https://docs.gofiber.io/api/app#test) 56 | 57 | ## Helpful links: 58 | 59 | We will be using `curl` to test the functionality of our endpoints and servers. If you are unfamiliar with the `curl` syntax check out these resources: 60 | 61 | * [Free code camp](https://www.freecodecamp.org/news/how-to-start-using-curl-and-why-a-hands-on-introduction-ea1c913caaaa/) 62 | * [Curl docs](https://curl.se/docs/manual.html) 63 | -------------------------------------------------------------------------------- /restful-go/ex-1-servers/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // curl http://localhost:8090/hello?name=world 9 | func hello(w http.ResponseWriter, req *http.Request) { 10 | if req.Method != "GET" { 11 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 12 | return 13 | } 14 | if name := req.FormValue("name"); name != "" { 15 | fmt.Fprintf(w, "hello %s\n", name) 16 | } 17 | 18 | } 19 | 20 | func main() { 21 | http.HandleFunc("/hello", hello) 22 | 23 | http.ListenAndServe(":8090", nil) 24 | } 25 | -------------------------------------------------------------------------------- /restful-go/ex-2-fiber/fiber.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | ) 6 | 7 | func main() { 8 | app := fiber.New() 9 | 10 | app.Get("/", func(c *fiber.Ctx) error { 11 | return c.SendString("Hello, World 👋!") 12 | }) 13 | app.Route("/3weeks-go", func(api fiber.Router) { 14 | api.Get("/testGet", func(c *fiber.Ctx) error { 15 | return c.SendString("Hello, World!") 16 | }).Name("foo") // /test/foo (name: test.foo) 17 | api.Post("/testPost", func(c *fiber.Ctx) error { 18 | body := c.Body() 19 | if len(body) == 0 { 20 | return c.SendStatus(fiber.StatusBadRequest) 21 | } 22 | return c.SendString(string(body)) 23 | }).Name("bar") // /test/bar (name: test.bar) 24 | }, "test.") 25 | 26 | app.Listen(":3000") 27 | } 28 | -------------------------------------------------------------------------------- /restful-go/ex-2-webframeworks/framework.go: -------------------------------------------------------------------------------- 1 | // This is exercise uses the following web frameworks: 2 | // 1. chi - https://github.com/go-chi/chi 3 | 4 | // to run this exercise: 5 | // go test -run TestUserEndpoints 6 | package framework 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/go-chi/chi" 12 | ) 13 | 14 | func getUsername(w http.ResponseWriter, r *http.Request) { 15 | username := chi.URLParam(r, "username") 16 | // check if username is empty -> return 400 17 | if username == "" { 18 | http.Error(w, http.StatusText(400)+", username parameter cannot be empty", 400) 19 | return 20 | } 21 | 22 | // return username and 200 23 | w.Write([]byte(username)) 24 | } 25 | 26 | func updateUsername(w http.ResponseWriter, r *http.Request) { 27 | username := chi.URLParam(r, "username") 28 | // check if username is empty -> return 400 29 | if username == "" { 30 | http.Error(w, http.StatusText(400)+", username parameter cannot be empty", 400) 31 | return 32 | } 33 | 34 | // return response and 200 35 | w.Write([]byte("user registered")) 36 | } 37 | 38 | func deleteUsername(w http.ResponseWriter, r *http.Request) { 39 | username := chi.URLParam(r, "username") 40 | // check if username is empty -> return 400 41 | if username == "" { 42 | http.Error(w, http.StatusText(400)+", username parameter cannot be empty", 400) 43 | return 44 | } 45 | 46 | // return response and 200 47 | w.Write([]byte("user deleted")) // TODO: return deleted username 48 | } 49 | -------------------------------------------------------------------------------- /restful-go/ex-3-clients/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | 11 | client := http.Client{ 12 | Timeout: time.Second * 2, 13 | } 14 | 15 | req, err := http.NewRequest("GET", "http://localhost:3000/3weeks-go/testGet", nil) 16 | if err != nil { 17 | panic(err) 18 | } 19 | body, err := client.Do(req) 20 | if err != nil { 21 | panic(err) 22 | } 23 | fmt.Println(body) 24 | } 25 | -------------------------------------------------------------------------------- /restful-go/ex-4-tests/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // endpoint: /getUsername/{username} 8 | func getUsername(w http.ResponseWriter, r *http.Request) { 9 | username := r.URL.Path[len("/getUsername/"):] 10 | // check if username is empty -> return 400 11 | if username == "" { 12 | http.Error(w, http.StatusText(400)+", username parameter cannot be empty", 400) 13 | return 14 | } 15 | // return username and 200 16 | w.Write([]byte(username)) 17 | } 18 | 19 | // endpoint: /updateUsername/{username} 20 | func updateUsername(w http.ResponseWriter, r *http.Request) { 21 | username := r.URL.Path[len("/updateUsername/"):] 22 | // check if username is empty -> return 400 23 | if username == "" { 24 | http.Error(w, http.StatusText(400)+", username parameter cannot be empty", 400) 25 | return 26 | } 27 | 28 | // return response and 200 29 | w.Write([]byte("user registered")) 30 | } 31 | 32 | // endpoint: /deleteUsername/{username} 33 | func deleteUsername(w http.ResponseWriter, r *http.Request) { 34 | username := r.URL.Path[len("/deleteUsername/"):] 35 | // check if username is empty -> return 400 36 | if username == "" { 37 | http.Error(w, http.StatusText(400)+", username parameter cannot be empty", 400) 38 | return 39 | } 40 | 41 | // return response and 200 42 | w.Write([]byte("user deleted")) // TODO: return deleted username 43 | } 44 | 45 | func main() { 46 | http.HandleFunc("/getUsername/", getUsername) 47 | http.HandleFunc("/updateUsername/", updateUsername) 48 | http.HandleFunc("/deleteUsername/", deleteUsername) 49 | 50 | http.ListenAndServe(":8090", nil) 51 | } 52 | -------------------------------------------------------------------------------- /restful-go/ex-4-tests/server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | ) 7 | 8 | func TestUserendpoints(t *testing.T) { 9 | t.Run("Get Username: Pass", testPassGetUsername) 10 | t.Run("Get Username: no Username", testFailGetUsernameempty) 11 | t.Run("Update Username: Pass", testPassUpdateUser) 12 | t.Run("Update Username:Fail", testFailUpdateUsernameempty) 13 | t.Run("Delete Username: Pass", testPassDeleteUser) 14 | t.Run("Delete Username:Fail", testFailDeleteUsernameempty) 15 | } 16 | 17 | func testPassGetUsername(t *testing.T) { 18 | w := httptest.NewRecorder() 19 | req := httptest.NewRequest("GET", "/getUsername/captainnobody1", nil) 20 | getUsername(w, req) 21 | resp := w.Result() 22 | if resp.StatusCode != 200 { 23 | t.Errorf("Expected status code 200, got %d", resp.StatusCode) 24 | } 25 | } 26 | 27 | func testFailGetUsernameempty(t *testing.T) { 28 | w := httptest.NewRecorder() 29 | req := httptest.NewRequest("GET", "/getUsername/", nil) 30 | getUsername(w, req) 31 | resp := w.Result() 32 | if resp.StatusCode != 400 { 33 | t.Errorf("Expected status code 400, got %d", resp.StatusCode) 34 | } 35 | } 36 | 37 | func testPassUpdateUser(t *testing.T) { 38 | // TODO: implement 39 | } 40 | 41 | func testFailUpdateUsernameempty(t *testing.T) { 42 | // TODO: implement 43 | } 44 | 45 | func testPassDeleteUser(t *testing.T) { 46 | // ToDO: implement 47 | } 48 | 49 | func testFailDeleteUsernameempty(t *testing.T) { 50 | // TODO: implement 51 | } 52 | -------------------------------------------------------------------------------- /restful-go/ex-4-tests/solution/framework.go: -------------------------------------------------------------------------------- 1 | // This is exercise uses the following web frameworks: 2 | // 1. chi - https://github.com/go-chi/chi 3 | 4 | // to run this exercise: 5 | // go test -run TestUserEndpoints 6 | package framework 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/go-chi/chi" 12 | ) 13 | 14 | func getUsername(w http.ResponseWriter, r *http.Request) { 15 | username := chi.URLParam(r, "username") 16 | // check if username is empty -> return 400 17 | if username == "" { 18 | http.Error(w, http.StatusText(400)+", username parameter cannot be empty", 400) 19 | return 20 | } 21 | // return username and 200 22 | w.Write([]byte(username)) 23 | } 24 | 25 | func updateUsername(w http.ResponseWriter, r *http.Request) { 26 | username := chi.URLParam(r, "username") 27 | // check if username is empty -> return 400 28 | if username == "" { 29 | http.Error(w, http.StatusText(400)+", username parameter cannot be empty", 400) 30 | return 31 | } 32 | 33 | // return response and 200 34 | w.Write([]byte("user registered")) 35 | } 36 | 37 | func deleteUsername(w http.ResponseWriter, r *http.Request) { 38 | username := chi.URLParam(r, "username") 39 | // check if username is empty -> return 400 40 | if username == "" { 41 | http.Error(w, http.StatusText(400)+", username parameter cannot be empty", 400) 42 | return 43 | } 44 | 45 | // return response and 200 46 | w.Write([]byte("user deleted")) // TODO: return deleted username 47 | } 48 | -------------------------------------------------------------------------------- /restful-go/ex-4-tests/solution/framework_test.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/go-chi/chi" 9 | ) 10 | 11 | func setupTestRouter(t *testing.T) *chi.Mux { 12 | r := chi.NewRouter() 13 | 14 | // setup routes 15 | r.Route("/register", func(r chi.Router) { 16 | // subroutes for register 17 | r.Route("/{username}", func(r chi.Router) { 18 | r.Get("/get", getUsername) // GET /register/123/get 19 | r.Get("/update", updateUsername) // PUT /register/123/update // TODO: this is a get because I am not providing a body 20 | r.Delete("/delete", deleteUsername) // DELETE /register/123/delete 21 | }) 22 | }) 23 | return r 24 | } 25 | 26 | func TestUserEndpoints(t *testing.T) { 27 | t.Run("get username: Pass", testPassGetUserName) 28 | t.Run("get username: No Username", testFailGetUsernameEmpty) 29 | t.Run("update username: Pass", testPassUpdateUser) 30 | t.Run("update username:Fail", testFailUpdateUsernameEmpty) 31 | t.Run("delete username: Pass", testPassDeleteUser) 32 | t.Run("delete username:Fail", testFailDeleteUsernameEmpty) 33 | } 34 | func testPassGetUserName(t *testing.T) { 35 | router := setupTestRouter(t) 36 | w := httptest.NewRecorder() 37 | req := httptest.NewRequest("GET", "/register/captainnobody1/get", nil) 38 | router.ServeHTTP(w, req) 39 | 40 | // check status code 41 | if status := w.Code; status != http.StatusOK { 42 | t.Errorf("handler returned wrong status code: got %v want %v", 43 | status, http.StatusOK) 44 | } 45 | } 46 | 47 | // test no username 48 | func testFailGetUsernameEmpty(t *testing.T) { 49 | router := setupTestRouter(t) 50 | w := httptest.NewRecorder() 51 | reqNoUser := httptest.NewRequest("GET", "/register//get", nil) 52 | router.ServeHTTP(w, reqNoUser) 53 | if status := w.Code; status != http.StatusBadRequest { 54 | t.Errorf("handler returned wrong status code: got %v want %v", 55 | status, http.StatusBadRequest) 56 | } 57 | } 58 | 59 | func testPassUpdateUser(t *testing.T) { 60 | router := setupTestRouter(t) 61 | w := httptest.NewRecorder() 62 | req := httptest.NewRequest("GET", "/register/captainnobody1/update", nil) 63 | router.ServeHTTP(w, req) 64 | 65 | // check status code 66 | if status := w.Code; status != http.StatusOK { 67 | t.Errorf("handler returned wrong status code: got %v want %v", 68 | status, http.StatusOK) 69 | } 70 | } 71 | 72 | // test no username 73 | func testFailUpdateUsernameEmpty(t *testing.T) { 74 | router := setupTestRouter(t) 75 | w := httptest.NewRecorder() 76 | reqNoUser := httptest.NewRequest("GET", "/register//update", nil) 77 | router.ServeHTTP(w, reqNoUser) 78 | if status := w.Code; status != http.StatusBadRequest { 79 | t.Errorf("handler returned wrong status code: got %v want %v", 80 | status, http.StatusBadRequest) 81 | } 82 | } 83 | 84 | func testPassDeleteUser(t *testing.T) { 85 | router := setupTestRouter(t) 86 | w := httptest.NewRecorder() 87 | req := httptest.NewRequest("DELETE", "/register/captainnobody1/delete", nil) 88 | router.ServeHTTP(w, req) 89 | 90 | // check status code 91 | if status := w.Code; status != http.StatusOK { 92 | t.Errorf("handler returned wrong status code: got %v want %v", 93 | status, http.StatusOK) 94 | } 95 | } 96 | 97 | // test db error 98 | func testFailDeleteUsernameEmpty(t *testing.T) { 99 | router := setupTestRouter(t) 100 | w := httptest.NewRecorder() 101 | req := httptest.NewRequest("DELETE", "/register//delete", nil) 102 | router.ServeHTTP(w, req) 103 | 104 | // check status code 105 | if status := w.Code; status != http.StatusBadRequest { 106 | t.Errorf("handler returned wrong status code: got %v want %v", 107 | status, http.StatusBadRequest) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /sql-quiz/question1.sql: -------------------------------------------------------------------------------- 1 | CREATE table users ( 2 | id int primary key, 3 | name varchar(255) not null, 4 | email varchar(255) not null, 5 | password varchar(255) not null, 6 | created_at timestamp not null default NOW(), 7 | updated_at timestamp not null default NOW() 8 | ); 9 | 10 | --valid 11 | --create table with primary key 12 | -------------------------------------------------------------------------------- /sql-quiz/question2.sql: -------------------------------------------------------------------------------- 1 | create table users ( 2 | id int primary key, 3 | name varchar(255) not null, 4 | email varchar(255) not null, 5 | password varchar(255) not null, 6 | created_at timestamp not null default now(), 7 | updated_at timestamp not null default now() 8 | ); 9 | 10 | INSERT into users (id, name, email) 11 | values (1, 'John Doe', 'john@email.com'); 12 | 13 | --valid sql 14 | -- failed to insert because password is not null 15 | --response: ERROR: ERROR: null value in column "password" violates not-null constraint 16 | --correct: 17 | -- INSERT into users (id, name, email, password) values (1, 'John Doe', 'john@email.com', 'password'); 18 | -- Query 1 OK: INSERT 0 1, 1 row affected 19 | -------------------------------------------------------------------------------- /sql-quiz/question3.sql: -------------------------------------------------------------------------------- 1 | CREATE table users ( 2 | id int primary key, 3 | name varchar(255) not null, 4 | email varchar(255) not null, 5 | password varchar(255) not null, 6 | created_at timestamp not null default NOW(), 7 | updated_at timestamp not null default NOW() 8 | ); 9 | 10 | INSERT into users (id, name, email) 11 | values (1, 'John Doe', 'john@email.com', 'password'); 12 | INSERT into users (id, name, email, password) 13 | values (1, 'Jane Doe', 'jane@email.com', 'password1'); 14 | 15 | --valid sql 16 | -- failed to insert because id is primary key and already exist 17 | -- response: ERROR: duplicate key value violates unique constraint "users_pkey" 18 | -- in postgres use ON CONFLICT UPDATE 19 | -- in mysql use ON DUPLICATE KEY UPDATE 20 | -- is msql use Merge INTO 21 | 22 | --correct:INSERT into users (id, name, email, password) values (2, 'Jane Doe', 'jane@email.com', 'password1'); 23 | 24 | -- follow up question: should another value be unique? 25 | -- follow up question: what can we do to make sure that the id is unique? 26 | -- answer: use sequence, serial, or auto increment 27 | -------------------------------------------------------------------------------- /sql-quiz/question4.sql: -------------------------------------------------------------------------------- 1 | CREATE table transactions( 2 | id int primary key, 3 | user_id int not null, 4 | amount int not null, 5 | created_at timestamp not null default NOW(), 6 | updated_at timestamp not null default NOW() 7 | foreign key (user_id) references users(id) 8 | ); 9 | 10 | INSERT into transactions (id, user_id, amount) values (1, 1, 1000); 11 | INSERT into transactions (id, user_id, amount) values (2, 2, 2000); 12 | INSERT into transactions (id, user_id, amount) values (3, 2, 3000); 13 | INSERT into transactions (id, user_id, amount) values (4, 1, 4000); 14 | 15 | SELECT COUNT(*) from transactions; 16 | 17 | -- valid 18 | -- response: 4 19 | -------------------------------------------------------------------------------- /sql-quiz/question5.sql: -------------------------------------------------------------------------------- 1 | CREATE table transactions( 2 | id int primary key, 3 | user_id int not null, 4 | amount int not null, 5 | created_at timestamp not null default NOW(), 6 | updated_at timestamp not null default NOW() 7 | foreign key (user_id) references users(id) 8 | ); 9 | 10 | INSERT into transactions (id, user_id, amount) values (1, 1, 1000); 11 | INSERT into transactions (id, user_id, amount) values (2, 2, 2000); 12 | INSERT into transactions (id, user_id, amount) values (3, 2, 3000); 13 | INSERT into transactions (id, user_id, amount) values (4, 1, 4000); 14 | 15 | UPDATE transactions set amount = 5000 where user_id = 1; 16 | 17 | select * from transactions; 18 | 19 | -- response 20 | -- 2 2 2000 2023-06-27 20:22:33.719055 2023-06-27 20:22:33.719055 21 | -- 3 2 3000 2023-06-27 20:22:33.764045 2023-06-27 20:22:33.764045 22 | -- 4 1 5000 2023-06-27 20:22:33.767405 2023-06-27 20:22:33.767405 23 | -- 1 1 5000 2023-06-27 20:26:08.17272 2023-06-27 20:26:08.17272 24 | 25 | -- what is the problem with this query? 26 | -- answer: it updates all the rows with user_id = 1 27 | -- how can we fix this? 28 | -- answer: add where clause 29 | --------------------------------------------------------------------------------