Added go http and db pages

This commit is contained in:
2025-01-11 09:29:50 -05:00
parent 9458db3381
commit 2644b07951
3 changed files with 902 additions and 1 deletions

555
docs/go/advanced/db.md Normal file
View File

@@ -0,0 +1,555 @@
# Databases
## Postgres
```bash
go get github.com/jackc/pgconn
go get github.com/jackc/pgx/v4
go get github.com/jackc/pgx/v4/stdlib
```
```go
// models.go
package data
import (
"context"
"database/sql"
"errors"
"log"
"time"
"golang.org/x/crypto/bcrypt"
)
const dbTimeout = time.Second * 3
var db *sql.DB
type PostgresRepository struct {
Conn *sql.DB
}
func NewPostgresRepository(conn *sql.DB) *PostgresRepository {
db = conn
return &PostgresRepository{
Conn: conn,
}
}
type User struct {
ID int `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Password string `json:"-"`
Active int `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// GetAll returns a slice of all users, sorted by last name
func (u *PostgresRepository) GetAll() ([]*User, error) {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at
from users order by last_name`
rows, err := u.Conn.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*User
for rows.Next() {
var user User
err := rows.Scan(
&user.ID,
&user.Email,
&user.FirstName,
&user.LastName,
&user.Password,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
log.Println("Error scanning", err)
return nil, err
}
users = append(users, &user)
}
return users, nil
}
// GetByEmail returns one user by email
func (u *PostgresRepository) GetByEmail(email string) (*User, error) {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where email = $1`
var user User
row := db.QueryRowContext(ctx, query, email)
err := row.Scan(
&user.ID,
&user.Email,
&user.FirstName,
&user.LastName,
&user.Password,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, err
}
return &user, nil
}
// GetOne returns one user by id
func (u *PostgresRepository) GetOne(id int) (*User, error) {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where id = $1`
var user User
row := db.QueryRowContext(ctx, query, id)
err := row.Scan(
&user.ID,
&user.Email,
&user.FirstName,
&user.LastName,
&user.Password,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, err
}
return &user, nil
}
// Update updates one user in the database, using the information
// stored in the receiver u
func (u *PostgresRepository) Update(user User) error {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
stmt := `update users set
email = $1,
first_name = $2,
last_name = $3,
user_active = $4,
updated_at = $5
where id = $6
`
_, err := db.ExecContext(ctx, stmt,
user.Email,
user.FirstName,
user.LastName,
user.Active,
time.Now(),
user.ID,
)
if err != nil {
return err
}
return nil
}
// DeleteByID deletes one user from the database, by ID
func (u *PostgresRepository) DeleteByID(id int) error {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
stmt := `delete from users where id = $1`
_, err := db.ExecContext(ctx, stmt, id)
if err != nil {
return err
}
return nil
}
// Insert inserts a new user into the database, and returns the ID of the newly inserted row
func (u *PostgresRepository) Insert(user User) (int, error) {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
if err != nil {
return 0, err
}
var newID int
stmt := `insert into users (email, first_name, last_name, password, user_active, created_at, updated_at)
values ($1, $2, $3, $4, $5, $6, $7) returning id`
err = db.QueryRowContext(ctx, stmt,
user.Email,
user.FirstName,
user.LastName,
hashedPassword,
user.Active,
time.Now(),
time.Now(),
).Scan(&newID)
if err != nil {
return 0, err
}
return newID, nil
}
```
```go
// main.go, instantiate connection and repo
package main
import (
"authentication/data"
_ "github.com/jackc/pgconn"
_ "github.com/jackc/pgx/v4"
_ "github.com/jackc/pgx/v4/stdlib"
)
func connectToDB() *sql.DB {
dsn := os.Getenv("DSN")
for {
conn, err := openDB(dsn)
if err != nil {
log.Println("DB not ready yet...")
counts++
} else {
log.Println("Connected to DB!")
return conn
}
if counts > 10 {
log.Println(err)
return nil
}
log.Println("Backing off for 2 seconds...")
time.Sleep(time.Second * 2)
continue
}
}
func (app *Config) setupRepo(conn *sql.DB) {
db := data.NewPostgresRepository(conn)
app.Repo = db
}
```
### Testing
It is best to use a repository pattern when dealing with databases, because it allows you to implement a mock repo that implements all the same functions as the real repo with mock return values.
```go
// repository.go
package data
type Repository interface {
GetAll() ([]*User, error)
GetByEmail(email string) (*User, error)
GetOne(id int) (*User, error)
Insert(user User) (int, error)
Update(user User) error
DeleteByID(id int) error
ResetPassword(password string, user User) error
PasswordMatches(password string, user User) (bool, error)
}
```
```go
package data
import (
"database/sql"
"time"
)
type PostgresTestRepository struct {
Conn *sql.DB
}
func NewPostgresTestRepository(conn *sql.DB) *PostgresTestRepository {
return &PostgresTestRepository{
Conn: conn,
}
}
// Implement all methods of Repository interface
func (repo *PostgresTestRepository) GetAll() ([]*User, error) {
users := []*User{}
return users, nil
}
```
```go
// setup_test.go
package main
import (
"authentication/data"
"os"
"testing"
)
var testApp Config
func TestMain(m *testing.M) {
repo := data.NewPostgresTestRepository(nil)
testApp.Repo = repo
os.Exit(m.Run())
}
```
## Mongo
```bash
go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/mongo/options
```
```go
// models.go
package data
import (
"context"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var client *mongo.Client
func New(mongo *mongo.Client) Models {
client = mongo
return Models{
LogEntry: LogEntry{},
}
}
type Models struct {
LogEntry LogEntry
}
type LogEntry struct {
ID string `bson:"_id,omitempty" json:"id,omitempty"`
Name string `bson:"name" json:"name"`
Data string `bson:"data" json:"data"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
func (l *LogEntry) Insert(entry LogEntry) error {
collection := client.Database("logs").Collection("logs")
_, err := collection.InsertOne(context.TODO(), LogEntry{
Name: entry.Name,
Data: entry.Data,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
if err != nil {
log.Println("Error insertint into logs: ", err)
return err
}
return nil
}
func (l *LogEntry) All() ([]*LogEntry, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
collection := client.Database("logs").Collection("logs")
opts := options.Find()
opts.SetSort(bson.D{{"created_at", -1}})
cursor, err := collection.Find(context.TODO(), bson.D{}, opts)
if err != nil {
log.Println("Finding all docs error: ", err)
return nil, err
}
defer cursor.Close(ctx)
var logs []*LogEntry
for cursor.Next(ctx) {
var item LogEntry
err := cursor.Decode(&item)
if err != nil {
log.Println("Error decoding log into slice: ", err)
return nil, err
} else {
logs = append(logs, &item)
}
}
return logs, nil
}
func (l *LogEntry) GetOne(id string) (*LogEntry, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
collection := client.Database("logs").Collection("logs")
docId, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, err
}
var entry LogEntry
err = collection.FindOne(ctx, bson.M{"_id": docId}).Decode(&entry)
if err != nil {
return nil, err
}
return &entry, nil
}
func (l *LogEntry) DropCollections() error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
collection := client.Database("logs").Collection("logs")
if err := collection.Drop(ctx); err != nil {
return err
}
return nil
}
func (l *LogEntry) Update() (*mongo.UpdateResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
collection := client.Database("logs").Collection("logs")
docId, err := primitive.ObjectIDFromHex(l.ID)
if err != nil {
return nil, err
}
result, err := collection.UpdateOne(
ctx,
bson.M{"_id": docId},
bson.D{
{"$set", bson.D{
{"name", l.Name},
{"data", l.Data},
{"updated_at", time.Now()},
}},
},
)
if err != nil {
return nil, err
}
return result, err
}
```
```go
// main.go
package main
import (
"context"
"fmt"
"log"
"logger/data"
"net"
"net/http"
"net/rpc"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
mongoURL = "mongodb://mongo:27017"
)
var client *mongo.Client
type Config struct {
Models data.Models
}
func main() {
// connect to mongo
mongoClient, err := connectToMongo()
if err != nil {
log.Panic(err)
}
client = mongoClient
// create a context to disconnect
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// close connection
defer func() {
if err = client.Disconnect(ctx); err != nil {
panic(err)
}
}()
app := Config{
Models: data.New(client),
}
}
func connectToMongo() (*mongo.Client, error) {
// create connection options
clientOptions := options.Client().ApplyURI(mongoURL)
clientOptions.SetAuth(options.Credential{
Username: "admin",
Password: "password",
})
c, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
log.Println("Error connecting: %s", err)
return nil, err
}
return c, nil
}
```

View File

@@ -0,0 +1,345 @@
# HTTP
The below snippets are using the `chi` package for Go.
### To Install
```bash
go get github.com/go-chi/chi/v5
go get github.com/go-chi/chi/v5/middleware
go get github.com/go-chi/cors
```
```go
// main.go
package main
import (
"fmt"
"log"
"net/http"
"os"
"time"
_ "github.com/jackc/pgconn"
_ "github.com/jackc/pgx/v4"
_ "github.com/jackc/pgx/v4/stdlib"
)
const webPort = "80"
var counts int64
type Config struct {
Client *http.Client
}
func main() {
app := Config{
Client: &http.Client{},
}
srv := &http.Server{
Addr: fmt.Sprintf(":%s", webPort),
Handler: app.routes(),
}
err := srv.ListenAndServe()
if err != nil {
log.Panic(err)
}
}
```
```go
// routes.go
package main
import (
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/cors"
)
func (app *Config) routes() http.Handler {
mux := chi.NewRouter()
mux.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://*", "http://*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300,
}))
mux.Use(middleware.Heartbeat("/ping"))
mux.Post("/authenticate", app.Authenticate)
return mux
}
```
```go
// handlers.go
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
)
func (app *Config) Authenticate(w http.ResponseWriter, r *http.Request) {
var requestPayload struct {
Email string `json:"email"`
Password string `json:"password"`
}
err := app.readJSON(w, r, &requestPayload)
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
// validate user against db
user, err := app.Repo.GetByEmail(requestPayload.Email)
if err != nil {
app.errorJSON(w, errors.New("invalid credentials"), http.StatusBadRequest)
return
}
valid, err := app.Repo.PasswordMatches(requestPayload.Password, *user)
if err != nil || !valid {
app.errorJSON(w, errors.New("invalid credentials"), http.StatusBadRequest)
return
}
// log authentication
err = app.logRequest("authentication", fmt.Sprintf("%s logged in", user.Email))
if err != nil {
app.errorJSON(w, err)
return
}
log.Println("Logged authentication request")
payload := jsonResponse{
Error: false,
Message: fmt.Sprintf("Logged in user %s", requestPayload.Email),
Data: user,
}
app.writeJSON(w, http.StatusAccepted, payload)
}
func (app *Config) logRequest(name, data string) error {
var entry struct {
Name string `json:"name"`
Data string `json:"data"`
}
entry.Name = name
entry.Data = data
jsonData, _ := json.MarshalIndent(entry, "", "\t")
logURL := "http://logger/log"
request, err := http.NewRequest("POST", logURL, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
_, err = app.Client.Do(request)
if err != nil {
return err
}
return nil
}
```
```go
// helpers.go
package main
import (
"encoding/json"
"errors"
"io"
"net/http"
)
type jsonResponse struct {
Error bool `json:"error"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
func (app *Config) readJSON(w http.ResponseWriter, r *http.Request, data any) error {
maxBytes := 1048576 // one MB
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
dec := json.NewDecoder(r.Body)
err := dec.Decode(data)
if err != nil {
return err
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
return errors.New("Body must have only a single json value")
}
return nil
}
func (app *Config) writeJSON(w http.ResponseWriter, status int, data any, headers ...http.Header) error {
out, err := json.Marshal(data)
if err != nil {
return err
}
if len(headers) > 0 {
for key, val := range headers[0] {
w.Header()[key] = val
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, err = w.Write(out)
if err != nil {
return err
}
return nil
}
func (app *Config) errorJSON(w http.ResponseWriter, err error, status ...int) error {
statusCode := http.StatusBadRequest
if len(status) > 0 {
statusCode = status[0]
}
var payload jsonResponse
payload.Error = true
payload.Message = err.Error()
return app.writeJSON(w, statusCode, payload)
}
```
## Testing
```go
// handlers_test.go
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
)
type RoundTripFunc func(req *http.Request) *http.Response
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
func NewTestClient(fn RoundTripFunc) *http.Client {
return &http.Client{
Transport: fn,
}
}
func Test_Authenticate(t *testing.T) {
jsonToReturn := `
{
"error": false,
"message": "some message"
}
`
client := NewTestClient(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(jsonToReturn)),
Header: make(http.Header),
}
})
testApp.Client = client
postBody := map[string]interface{}{
"email": "me@here.com",
"password": "verysecret",
}
body, _ := json.Marshal(postBody)
req, _ := http.NewRequest("POST", "/authenticate", bytes.NewReader(body))
rr := httptest.NewRecorder()
handler := http.HandlerFunc(testApp.Authenticate)
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusAccepted {
t.Errorf("handler returned wrong status code: got %v want %v",
rr.Code, http.StatusAccepted)
}
}
```
```go
// routes_test.go
package main
import (
"net/http"
"testing"
"github.com/go-chi/chi"
)
func Test_routes_exist(t *testing.T) {
testApp := Config{}
testRoutes := testApp.routes()
chiRoutes := testRoutes.(chi.Router)
routes := []string{"/authenticate"}
for _, route := range routes {
routeExists(t, chiRoutes, route)
}
}
func routeExists(t *testing.T, routes chi.Router, route string) {
found := false
_ = chi.Walk(routes, func(method string, foundRoute string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
if route == foundRoute {
found = true
}
return nil
})
if !found {
t.Errorf("route %s not found", route)
}
}
```

View File

@@ -19,7 +19,8 @@
{ "text": "Testing", "link": "/go/advanced/testing" }, { "text": "Testing", "link": "/go/advanced/testing" },
{"text": "HTTP", "link": "/go/advanced/http"}, {"text": "HTTP", "link": "/go/advanced/http"},
{"text": "RPC", "link": "/go/advanced/rpc"}, {"text": "RPC", "link": "/go/advanced/rpc"},
{"text": "gRPC", "link": "/go/advanced/grpc"} {"text": "gRPC", "link": "/go/advanced/grpc"},
{"text": "Databases", "link": "/go/advanced/db"}
] ]
} }
] ]