go mod init github.com/vlasove/go2/5.StandardWebServer
Useful link: https://github.com/golang-standards/project-layout (There can be found information about structuring/packaging/refactoring of any Go apps)
Standard pattern of entry point :
cmd/<app_name>/main.go
Here was created :
cmd/api/main.go
Standard pattern dictated in a following way
internal/app/<app_name>/<app_name>.go
We have internal/app/api/api.go
Rule: in go:
- configurations are always stored in external files (.toml, .env)
- in Go projects always exists default configurations (exclusion - DB is intended not to have defaults)
Basically, for configuration only PORT is needed.
intrenal/app/api/config.go
configs/<app_name>.toml or configs/.env
//api.toml
bind_addr = ":8080"
We would want to pass the following way:
api.exe -path configs/api.toml
go get -u github.com/gorilla/mux
database/sql
sqlx
gosql
storage/storage.go
Purpose of this model is:
- Instance of DB
- constructor of DB
- public method Open (setup connection)
- public method Close (close connection)
storage.go
The main problem lies inside the Open method, because in fact the low-level sql.Open is "lazy" (establishes a connection to the database only when the first query is made)
config.go
Contains a config instance and a constructor. The config attribute is only a connection string of the form :
"host=localhost port=5432 user=postgres password=postgres dbname=restapi sslmode=disable"
Add new attribute storage
//Base API server instance description
type API struct {
//UNEXPORTED FIELD!
config *Config
logger *logrus.Logger
router *mux.Router
storage *storage.Storage
}
Add new configurator:
//Configure storage (storage API)
func (a *API) configreStorageField() error {
storage := storage.New(a.config.Storage)
if err := storage.Open(); err != nil {
return err
}
a.storage = storage
return nil
}
First install Scoop scoop
- Open PowerShell:
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
andInvoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
After installation scoop
run: scoop install migrate
- Run
$ curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz
- Then move it to GOPATH
mv migrate.linux-amd64 $GOPATH/bin/migrate
This repository will hold up/down pairs of sql migration requests to the database.
migrate create -ext sql -dir migrations UsersCreationMigration
Look migrations/....up.sql
and migrations/...down.sql
migrate -path migrations -database "postgres://localhost:5432/restapi?sslmode=disable&user=postgres&password=postgres" up
To execute revert migrate -path migrations -database "postgres://localhost:5432/restapi?sslmode=disable&user=postgres&password=postgres" down
Open file migrations/.....up.sql
CREATE TABLE users (
id bigserial not null primary key,
login varchar not null unique,
password varchar not null
);
CREATE TABLE articles (
id bigserial not null primary key,
title varchar not null unique,
author varchar not null,
content varchar not null
);
Execute command migrate -path migrations -database "postgres://localhost:5432/restapi?sslmode=disable&user=postgres&password=postgres" down
To define models internal/app/models/
2 models:
- user.go
- article.go
//user.go
package models
//User model defeniton
type User struct {
ID int `json:"id"`
Login string `json:"login"`
Password string `json:"password"`
}
//article.go
package models
//Article model defenition
type Article struct {
ID int `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Content string `json:"content"`
}
Working with models through repositories. To do so initialize 2 files:
storage/userrepository.go
storage/articlerepository.go
//articlerepository.go
package storage
//Instance of Article repository (model interface)
type ArticleRepository struct {
storage *Storage
}
Alike for users.
We want our application to communicate with models through repositories (which will contain the necessary set of methods to interact with the database). We need to define 2 methods at the repository, which will provide public repositories:
//storage.go
//Instance of storage
type Storage struct {
config *Config
// DataBase FileDescriptor
db *sql.DB
//Subfield for repo interfacing (model user)
userRepository *UserRepository
//Subfield for repo interfaceing (model article)
articleRepository *ArticleRepository
}
....
//Public Repo for Article
func (s *Storage) User() *UserRepository {
if s.userRepository != nil {
return s.userRepository
}
s.userRepository = &UserRepository{
storage: s,
}
return nil
}
//Public Repo for User
func (s *Storage) Article() *ArticleRepository {
if s.articleRepository != nil {
return s.articleRepository
}
s.articleRepository = &ArticleRepository{
storage: s,
}
return nil
}
- Save a new user to the database (INSERT user or Create)
- For authentication, you need a user search function by
login
. - Output all users from the database
package storage
import (
"fmt"
"log"
"github.com/vlasove/go2/7.ServerAndDB2/internal/app/models"
)
//Instance of User repository (model interface)
type UserRepository struct {
storage *Storage
}
var (
tableUser string = "users"
)
//Create User in db
func (ur *UserRepository) Create(u *models.User) (*models.User, error) {
query := fmt.Sprintf("INSERT INTO %s (login, password) VALUES ($1, $2) RETURNING id", tableUser)
if err := ur.storage.db.QueryRow(query, u.Login, u.Password).Scan(&u.ID); err != nil {
return nil, err
}
return u, nil
}
//Find user by login
func (ur *UserRepository) FindByLogin(login string) (*models.User, bool, error) {
users, err := ur.SelectAll()
var founded bool
if err != nil {
return nil, founded, err
}
var userFinded *models.User
for _, u := range users {
if u.Login == login {
userFinded = u
founded = true
break
}
}
return userFinded, founded, nil
}
//Select all users in db
func (ur *UserRepository) SelectAll() ([]*models.User, error) {
query := fmt.Sprintf("SELECT * FROM %s", tableUser)
rows, err := ur.storage.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
//Prepare, where we going to read
users := make([]*models.User, 0)
for rows.Next() {
u := models.User{}
err := rows.Scan(&u.ID, &u.Login, &u.Password)
if err != nil {
log.Println(err)
continue
}
users = append(users, &u)
}
return users, nil
}
- To be able to add an article to the database
- Be able to delete by id
- Receive all articles
- Retrieve an article by id
- Update (at home)
articlerepository.go
package storage
import (
"fmt"
"log"
"github.com/vlasove/go2/7.ServerAndDB2/internal/app/models"
)
//Instance of Article repository (model interface)
type ArticleRepository struct {
storage *Storage
}
var (
tableArticle string = "articles"
)
//Add article to DB
func (ar *ArticleRepository) Create(a *models.Article) (*models.Article, error) {
query := fmt.Sprintf("INSERT INTO %s (title, author, content) VALUES ($1, $2, $3) RETURNING id", tableArticle)
if err := ar.storage.db.QueryRow(query, a.Title, a.Author, a.Content).Scan(&a.ID); err != nil {
return nil, err
}
return a, nil
}
//Delete article by ID
func (ar *ArticleRepository) DeleteById(id int) (*models.Article, error) {
article, ok, err := ar.FindArticleById(id)
if err != nil {
return nil, err
}
if ok {
query := fmt.Sprintf("DELETE FROM %s WHERE id=$1", tableArticle)
_, err := ar.storage.db.Exec(query, id)
if err != nil {
return nil, err
}
}
return article, nil
}
//Retrieve article by ID
func (ar *ArticleRepository) FindArticleById(id int) (*models.Article, bool, error) {
articles, err := ar.SelectAll()
var founded bool
if err != nil {
return nil, founded, err
}
var articleFinded *models.Article
for _, a := range articles {
if a.ID == id {
articleFinded = a
founded = true
break
}
}
return articleFinded, founded, nil
}
//Get all articles from DB
func (ar *ArticleRepository) SelectAll() ([]*models.Article, error) {
query := fmt.Sprintf("SELECT * FROM %s", tableArticle)
rows, err := ar.storage.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
//Prepare where we are going to read
articles := make([]*models.Article, 0)
for rows.Next() {
a := models.Article{}
err := rows.Scan(&a.ID, &a.Title, &a.Author, &a.Content)
if err != nil {
log.Println(err)
continue
}
articles = append(articles, &a)
}
return articles, nil
}
Enter api
// Trying to configure the router (specifically the router API field)
func (a *API) configreRouterField() {
a.router.HandleFunc(prefix+"/articles", a.GetAllArticles).Methods("GET")
a.router.HandleFunc(prefix+"/articles/{id}", a.GetArticleById).Methods("GET")
a.router.HandleFunc(prefix+"/articles/{id}", a.DeleteArticleById).Methods("DELETE")
a.router.HandleFunc(prefix+"/articles", a.PostArticle).Methods("POST")
a.router.HandleFunc(prefix+"/user/register", a.PostUserRegister).Methods("POST")
}
Create file internal/app/api/handlers.go
internal/app/api/handlers.go
Inside define 2 entities:
package api
import "net/http"
// Auxiliary structure for message formation
type Message struct {
StatusCode int `json:"status_code"`
Message string `json:"message"`
IsError bool `json:"is_error"`
}
func initHeaders(writer http.ResponseWriter) {
writer.Header().Set("Content-Type", "application/json")
}
// Returns all articles from the database at the moment
func (api *API) GetAllArticles(writer http.ResponseWriter, req *http.Request) {
// Initializing Headers
initHeaders(writer)
// Logging the moment when request processing starts
api.logger.Info("Get All Artiles GET /api/v1/articles")
// Trying to get something from the database
articles, err := api.storage.Article().SelectAll()
if err != nil {
// What do we do if there was an error at the connection stage?
api.logger.Info("Error while Articles.SelectAll : ", err)
msg := Message{
StatusCode: 501,
Message: "We have some troubles to accessing database. Try again later",
IsError: true,
}
writer.WriteHeader(501)
json.NewEncoder(writer).Encode(msg)
return
}
writer.WriteHeader(200)
json.NewEncoder(writer).Encode(articles)
}