Рефакторинг

- добавлен gin-gonic

- перетасованы пакет

- добавлен первый репозиторий

- handlers теперь возвращают собственную ошибку
This commit is contained in:
Michael Makarochkin 2025-03-11 23:19:31 +03:00
parent 3879c9bd24
commit ff614cea04
20 changed files with 298 additions and 50 deletions

View File

@ -15,6 +15,7 @@ services:
- ./environment:/var/environment/pink_fox
- ./.storage/go:/go
environment:
- TZ=Europe/Moscow
- ARG1
postgres:
@ -22,6 +23,8 @@ services:
environment:
- POSTGRES_USER=pink_fox
- POSTGRES_PASSWORD=pink_fox_pass
- TZ=Europe/Moscow
- PGTZ=Europe/Moscow
volumes:
- ./.storage/postgres:/var/lib/postgresql/data
- ./services/postgres/data:/var/backups/postgres

View File

@ -1,3 +1,5 @@
out.log
err.log
pink_fox
dlv.log

View File

@ -0,0 +1,11 @@
DROP TABLE IF EXISTS links;
CREATE TABLE links (
id SERIAL PRIMARY KEY,
token VARCHAR(24) NOT NULL UNIQUE,
url VARCHAR(512) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
);
CREATE INDEX idx_links_token ON links (token);

View File

@ -1,2 +0,0 @@
2025-03-03T16:51:40Z warning layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
не удалось открыть файл лога: open : no such file or directory

View File

@ -1,10 +1,10 @@
module pink_fox
go 1.24.0
go 1.24.1
require (
github.com/bytedance/sonic v1.12.9 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/bytedance/sonic v1.13.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
@ -27,11 +27,11 @@ require (
github.com/spf13/pflag v1.0.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -1,8 +1,8 @@
github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ=
github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
@ -67,17 +67,17 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -6,6 +6,11 @@ import (
"pink_fox/src/app/cmd"
)
// FIXME
// перегрузка конфига
// отображение html страницы
// вынести перехват ошибок в middleware
func main() {
err := cmd.Execute()
if err != nil {

View File

@ -18,6 +18,7 @@ type Config struct {
Port int `yaml:"port"`
Db DB `yaml:"db"`
LogFile string `yaml:"logFile"`
Debug bool `yaml:"debug"`
}
func LoadConfig(defaultConfig string, defaultPort int) (*Config, error) {
@ -38,7 +39,7 @@ func setDefaultValue(conf *Config, defaultPort int) *Config {
conf.Port = defaultPort
}
if conf.LogFile == "" {
conf.LogFile = "/var/logger/pink_fox/app.logger"
conf.LogFile = "/var/log/pink_fox/app.log"
}
return conf
}

View File

@ -0,0 +1,17 @@
package http_server
import "github.com/gin-gonic/gin"
type Request struct {
ctx *gin.Context
}
func NewRequest(ctx *gin.Context) *Request {
return &Request{
ctx: ctx,
}
}
func (it *Request) Param(key string) string {
return it.ctx.Param(key)
}

View File

@ -12,6 +12,14 @@ func NewResponseFactory(ctx *gin.Context) *ResponseFactory {
return &ResponseFactory{ctx: ctx}
}
func (it *ResponseFactory) String(s string) Response {
func (it *ResponseFactory) String(s string) *StringResponse {
return NewStringResponse(s, it.ctx)
}
func (it *ResponseFactory) HtmlError(code int) *HtmlErrorResponse {
return NewHtmlErrorResponse(code, it.ctx)
}
func (it *ResponseFactory) Redirect(url string) *RedirectResponse {
return NewRedirectResponse(url, it.ctx)
}

View File

@ -1,6 +1,7 @@
package http_server
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
@ -21,3 +22,46 @@ func NewStringResponse(s string, ctx *gin.Context) *StringResponse {
func (it *StringResponse) Render() {
it.ctx.String(http.StatusOK, it.str)
}
type HtmlErrorResponse struct {
code int
msg string
ctx *gin.Context
}
func NewHtmlErrorResponse(code int, ctx *gin.Context) *HtmlErrorResponse {
return &HtmlErrorResponse{code: code, ctx: ctx}
}
func (it *HtmlErrorResponse) Msg(msg string) *HtmlErrorResponse {
it.msg = msg
return it
}
func (it *HtmlErrorResponse) Render() {
message := ""
if it.msg != "" {
message = fmt.Sprintf("<p>%s</p>", it.msg)
}
it.ctx.Header("Content-Type", "text/html; charset=utf-8")
it.ctx.String(it.code, fmt.Sprintf("<h1>%d %s</h1>\n%s\n", it.code, http.StatusText(it.code), message))
}
type RedirectResponse struct {
code int
to string
ctx *gin.Context
}
func NewRedirectResponse(to string, ctx *gin.Context) *RedirectResponse {
return &RedirectResponse{code: 302, to: to, ctx: ctx}
}
func (it *RedirectResponse) SetCode(code int) *RedirectResponse {
it.code = code
return it
}
func (it *RedirectResponse) Render() {
it.ctx.Redirect(it.code, it.to)
}

View File

@ -22,6 +22,7 @@ func init() {
type Error struct {
msg string
trace []string
debugStack []byte
}
func NewString(msg string) *Error {
@ -31,7 +32,7 @@ func NewString(msg string) *Error {
}
}
func NewError(err error) *Error {
func New(err error) *Error {
return &Error{
msg: err.Error(),
trace: []string{trace()},
@ -44,14 +45,29 @@ func (it *Error) Add(msg string) *Error {
return it
}
func (it *Error) Tap() *Error {
it.trace = append(it.trace, trace())
return it
}
func (it *Error) String() string {
result := it.msg
for _, line := range it.trace {
result += "\n" + line
}
return result
}
func (it *Error) GetTrace() []string {
return it.trace
}
func (it *Error) SetDebugStack(stack []byte) *Error {
it.debugStack = stack
return it
}
func (it *Error) GetDebugStack() string {
return string(it.debugStack)
}
func trace() string {
_, file, line, ok := runtime.Caller(2)
if !ok {

View File

@ -28,3 +28,7 @@ func NewLogger(path string) (*Logger, error) {
func (it *Logger) Close() error {
return it.file.Close()
}
func (it *Logger) Error(msg string, args ...any) {
it.logger.Error(msg, args...)
}

View File

@ -1,10 +1,15 @@
package routes_manager
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"pink_fox/src/app"
"pink_fox/src/app/http_server"
le "pink_fox/src/app/lerror"
"pink_fox/src/dependence"
"runtime/debug"
"strings"
)
type RoutesManager struct {
@ -21,11 +26,11 @@ func NewManager(e *gin.Engine, application *app.Application) *RoutesManager {
}
}
type MiddlewareFunc func(*dependence.Container, ActionFunc) http_server.Response
type MiddlewareFunc func(*dependence.Container, ActionFunc) (http_server.Response, *le.Error)
type HandlerFunc func(container *dependence.Container) http_server.Response
type HandlerFunc func(container *dependence.Container) (http_server.Response, *le.Error)
type ActionFunc func() http_server.Response
type ActionFunc func() (http_server.Response, *le.Error)
func (it *RoutesManager) Group(relativePath string, middlewares ...MiddlewareFunc) *Group {
return NewGroup(it, relativePath, middlewares...)
@ -97,13 +102,25 @@ func makeHandlerGin(handler HandlerFunc, middlewares []MiddlewareFunc, applicati
return func(ctx *gin.Context) {
dic := dependence.NewContainer(application, ctx)
defer dic.Close()
res := pipeline(handler, middlewares, dic)
res, err := pipeline(handler, middlewares, dic)
if err != nil {
writeToLog(err, application)
showHtmlError(err, application, ctx)
return
}
res.Render()
}
}
func pipeline(endpoint HandlerFunc, middlewares []MiddlewareFunc, di *dependence.Container) http_server.Response {
handler := func() http_server.Response {
func pipeline(endpoint HandlerFunc, middlewares []MiddlewareFunc, di *dependence.Container) (resp http_server.Response, lerr *le.Error) {
defer func() {
if err := recover(); err != nil {
resp = nil
lerr = le.New(fmt.Errorf("произошла паника: %v", err)).SetDebugStack(debug.Stack())
}
}()
handler := func() (http_server.Response, *le.Error) {
return endpoint(di)
}
@ -111,11 +128,35 @@ func pipeline(endpoint HandlerFunc, middlewares []MiddlewareFunc, di *dependence
handler = createMiddleware(handler, middlewares[i], di)
}
return handler()
resp, lerr = handler()
return
}
func createMiddleware(next ActionFunc, middleware MiddlewareFunc, di *dependence.Container) ActionFunc {
return func() http_server.Response {
return func() (http_server.Response, *le.Error) {
return middleware(di, next)
}
}
func writeToLog(err *le.Error, application *app.Application) {
moreMessage := make([]any, 0)
if debugStack := err.GetDebugStack(); debugStack != "" {
moreMessage = append(moreMessage, "stack", debugStack)
}
moreMessage = append(moreMessage, "trace")
moreMessage = append(moreMessage, strings.Join(err.GetTrace(), ";\n")+";")
application.Logger.Error(err.String(), moreMessage...)
}
func showHtmlError(err *le.Error, application *app.Application, ctx *gin.Context) {
message := ""
if application.Conf.Debug {
if debugStack := err.GetDebugStack(); debugStack != "" {
message = fmt.Sprintf("<pre>%s\n\n%s\n%s\n<pre>", err.String(), debugStack, strings.Join(err.GetTrace(), "\n"))
} else {
message = fmt.Sprintf("<pre>%s\n\n%s\n<pre>", err.String(), strings.Join(err.GetTrace(), "\n"))
}
}
ctx.Header("Content-Type", "text/html; charset=utf-8")
ctx.String(500, fmt.Sprintf("<h1>500 %s</h1>\n%s\n", http.StatusText(500), message))
}

View File

@ -0,0 +1,33 @@
package controllers
import (
"fmt"
"pink_fox/src/app/http_server"
le "pink_fox/src/app/lerror"
"pink_fox/src/repositories"
)
type GoController struct {
}
func NewGoController() *GoController {
return &GoController{}
}
type IndexActionDependence interface {
MakeRequest() *http_server.Request
MakeResponseFactory() *http_server.ResponseFactory
MakeLinksRepository() repositories.LinksRepository
}
func (it *GoController) IndexAction(depend IndexActionDependence) (http_server.Response, *le.Error) {
token := depend.MakeRequest().Param("token")
link, err := depend.MakeLinksRepository().ByToken(token)
if err != nil {
return nil, err.Tap()
} else if link == nil {
return depend.MakeResponseFactory().HtmlError(404), nil
}
return depend.MakeResponseFactory().String(fmt.Sprintf("Ссылка %d, token \"%s\" to \"%v\", дата %v", link.ID, link.Token, link.Url, link.CreatedAt)), nil
}

View File

@ -2,22 +2,23 @@ package controllers
import (
"pink_fox/src/app/http_server"
le "pink_fox/src/app/lerror"
)
type IndexController struct {
responseFactory *http_server.ResponseFactory
}
type IndexControllerContainer interface {
type IndexControllerDependence interface {
MakeResponseFactory() *http_server.ResponseFactory
}
func NewIndexController(di IndexControllerContainer) *IndexController {
func NewIndexController(depend IndexControllerDependence) *IndexController {
return &IndexController{
responseFactory: di.MakeResponseFactory(),
responseFactory: depend.MakeResponseFactory(),
}
}
func (it *IndexController) ActionIndex() http_server.Response {
return it.responseFactory.String("Hello world!")
func (it *IndexController) IndexAction() (http_server.Response, *le.Error) {
return it.responseFactory.String("Hello world!"), nil
}

View File

@ -4,6 +4,7 @@ import (
"github.com/gin-gonic/gin"
"pink_fox/src/app"
"pink_fox/src/app/http_server"
repo "pink_fox/src/repositories"
)
type Container struct {
@ -21,6 +22,14 @@ func NewContainer(application *app.Application, context *gin.Context) *Container
func (it *Container) Close() {
}
func (it *Container) MakeResponseFactory() *http_server.ResponseFactory {
return nil
func (it *Container) MakeRequest() *http_server.Request {
return http_server.NewRequest(it.context)
}
func (it *Container) MakeResponseFactory() *http_server.ResponseFactory {
return http_server.NewResponseFactory(it.context)
}
func (it *Container) MakeLinksRepository() repo.LinksRepository {
return repo.NewLinksRepository(it.application.Conn)
}

View File

@ -0,0 +1,48 @@
package repositories
import (
"database/sql"
"errors"
le "pink_fox/src/app/lerror"
"time"
)
type LinkData struct {
ID int
Token string
Url string
CreatedAt time.Time
ExpiresAt time.Time
}
type LinksRepository interface {
ByToken(string) (*LinkData, *le.Error)
}
type LinksRepositoryImpl struct {
db *sql.DB
}
func NewLinksRepository(conn *sql.DB) *LinksRepositoryImpl {
return &LinksRepositoryImpl{
db: conn,
}
}
// FIXME нужно настроить время
// время для го
// время для postgres
func (it *LinksRepositoryImpl) ByToken(token string) (*LinkData, *le.Error) {
var link LinkData
query := `SELECT id, token, url, created_at, expires_at FROM links WHERE token = $1`
err := it.db.QueryRow(query, token).Scan(&link.ID, &link.Token, &link.Url, &link.CreatedAt, &link.ExpiresAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
} else {
return nil, le.New(err)
}
}
return &link, nil
}

View File

@ -2,6 +2,7 @@ package routes
import (
"pink_fox/src/app/http_server"
le "pink_fox/src/app/lerror"
"pink_fox/src/app/routes_manager"
"pink_fox/src/controllers"
di "pink_fox/src/dependence"
@ -9,8 +10,14 @@ import (
func RegistrationRoutes(r *routes_manager.RoutesManager) {
r.GET("/", IndexController)
r.GET("/link/:token", GoController)
}
func IndexController(dic *di.Container) http_server.Response {
return controllers.NewIndexController(dic).ActionIndex()
func IndexController(depend *di.Container) (http_server.Response, *le.Error) {
return controllers.NewIndexController(depend).IndexAction()
}
func GoController(depend *di.Container) (http_server.Response, *le.Error) {
return controllers.NewGoController().IndexAction(depend)
}

View File

@ -1 +1 @@
SELECT 'CREATE DATABASE pink_fox_db with owner pink_fox' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'pink_fox_db')\gexec
CREATE DATABASE pink_fox_db with owner pink_fox;