├── .air.conf ├── .gitignore ├── LICENSE.txt ├── README.md ├── cmd └── project-name │ └── main.go ├── core ├── config.go ├── database.go ├── db │ └── migrations │ │ └── 20210405172040_base.sql └── utils.go ├── go.mod ├── go.sum ├── scripts ├── migration_create.sh └── migration_down.sh ├── server └── server.go └── user ├── user.go ├── user_handler.go ├── user_repository.go └── user_test.go /.air.conf: -------------------------------------------------------------------------------- 1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format 2 | 3 | # Working directory 4 | # . or absolute path, please note that the directories following must be under root. 5 | root = "." 6 | tmp_dir = "tmp" 7 | 8 | [build] 9 | # Just plain old shell command. You could use `make` as well. 10 | cmd = "go build -o ./tmp/project-name cmd/project-name/main.go" 11 | # Binary file yields from `cmd`. 12 | bin = "tmp/project-name" 13 | # Customize binary. 14 | full_bin = "PORT=8008 ./tmp/project-name" 15 | # Watch these filename extensions. 16 | include_ext = ["go", "tpl", "tmpl", "html", "sql"] 17 | # Ignore these filename extensions or directories. 18 | exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"] 19 | # Watch these directories if you specified. 20 | include_dir = [] 21 | # Exclude files. 22 | exclude_file = [] 23 | # Exclude unchanged files. 24 | exclude_unchanged = true 25 | # This log file places in your tmp_dir. 26 | log = "air.log" 27 | # It's not necessary to trigger build each time file changes if it's too frequent. 28 | delay = 1000 # ms 29 | # Stop running old binary when build errors occur. 30 | stop_on_error = true 31 | # Send Interrupt signal before killing process (windows does not support this feature) 32 | send_interrupt = false 33 | # Delay after sending Interrupt signal 34 | kill_delay = 500 # ms 35 | 36 | [log] 37 | # Show log time 38 | time = false 39 | 40 | [color] 41 | # Customize each part's color. If no color found, use the raw app log. 42 | main = "magenta" 43 | watcher = "cyan" 44 | build = "yellow" 45 | runner = "green" 46 | 47 | [misc] 48 | # Delete tmp directory on exit 49 | clean_on_exit = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | *.db 3 | dist/* 4 | tmp/* -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 El Donaldo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Simple Go Project Template - Suited for Webapp MVPs 2 | 3 | A simple go project structure setup with all dependencies you need to get your MVP off the ground. :rocket: 4 | 5 | The project is suited as starting point for a simple webapp MVP. Besides the core files needed to get the setup running 6 | it contains a simple user module showcasing the intended project structure. Running the project starts the app exposing 7 | a REST API using the go-shipped webserver. 8 | 9 | **Features** 10 | 11 | * Hot reloading. :trollface: 12 | * sqlite3 database for local development. :heart: 13 | * ORM ready to go. :runner: 14 | * A simple database migration system. :raised_hands: 15 | * Injection of environment variables into config files. :notes: 16 | * Simple spring-like repository structure for convenient database queries. :star: 17 | * Testing setup. :construction_worker: 18 | 19 | Feedback and PRs welcome! I hope you find it useful. :beer: :pizza: 20 | 21 | ## Installation and Run Project 22 | 23 | ```bash 24 | $ git clone https://github.com/eldonaldo/go-project-template 25 | $ cd go-project-template 26 | 27 | # You need to get air separately to get the hot reloading running 28 | $ go get -u github.com/cosmtrek/air 29 | 30 | # Runs the project with hot reloading enabled 31 | $ air 32 | ``` 33 | 34 | Interact with the app using the command line :computer: 35 | 36 | ```bash 37 | $ curl localhost:8008/create?name=John 38 | $ User with name John1 created 39 | 40 | $ curl localhost:8008/greet?name=John 41 | $ Hi John 42 | $ { 43 | $ "ID": 1, 44 | $ "CreatedAt": "2021-04-07T21:32:14.135761+02:00", 45 | $ "UpdatedAt": "2021-04-07T21:32:14.135761+02:00", 46 | $ "DeletedAt": null, 47 | $ "Name": "John" 48 | $ } 49 | ``` 50 | 51 | ## Project Setup 52 | 53 | * _cmd_: Contains the app binaries 54 | * _core_: The app's core functionality resides here (such as database handling, migrations etc.) 55 | * _core/db/migrations_: SQL migration files go in here. Use `./scripts/migration_create.sh migration_name` to create a 56 | new one. Migrations in this folder are automatically executed (in order) once upon app start. 57 | * _scripts_: Automation scripts. There is a script to bootstrap a new migration and another to downgrade an already 58 | applied migration. 59 | * _server_: Exposes the app as REST API and handles the HTTP server setup. 60 | * _user_: A user module showcasing a simple structure using repository and REST handler. 61 | 62 | ### Changing the Project Name 63 | 64 | To change the project name from _go-project-name_ to something else you need to edit the _go.mod_ module end edit the 65 | first line `module github.com/eldonaldo/go-project-template` to `module github.com/your-username/your-new-name`. Imports 66 | in all files need to be changed according (your IDE probably can do that for you :recycle:). Further, you might also 67 | want change `cmd/project-name` and therefore you also need to change line 6 of `.air.conf`. But that should be it 68 | then. :white_check_mark: 69 | 70 | ## Libraries Used 71 | 72 | * [github.com/caarlos0/env - Read env variables into struct fields](https://github.com/caarlos0/env) 73 | * [github.com/pressly/goose - Database migrations](https://github.com/pressly/goose) 74 | * [gorm.io/gorm - ORM](https://gorm.io/) 75 | * [github.com/cosmtrek/air - Hot reloading](https://github.com/cosmtrek/air) 76 | * [github.com/stretchr/testify/assert - For convenient testing](github.com/stretchr/testify/assert) 77 | 78 | :wave: -------------------------------------------------------------------------------- /cmd/project-name/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/eldonaldo/go-project-template/core" 5 | "github.com/eldonaldo/go-project-template/server" 6 | ) 7 | 8 | // Runs the app 9 | func main() { 10 | 11 | // Performs migrations, reads the config and starts the server 12 | core.Migrate() 13 | config := core.ReadConfig() 14 | server.StartServer(config.Port) 15 | } 16 | -------------------------------------------------------------------------------- /core/config.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/caarlos0/env/v6" 5 | "log" 6 | ) 7 | 8 | // Not configurable - internal only - config 9 | const ( 10 | DatabaseDriver = "sqlite3" 11 | DatabaseFile = "database.db" 12 | ) 13 | 14 | // Config for which the fields can be configured using environment variables 15 | type Config struct { 16 | 17 | // Reads the content of environment variable PORT as int into this field. 18 | // If none is setup, the default value 8008 is used 19 | Port int `env:"PORT" envDefault:"8008"` 20 | } 21 | 22 | // Parse config from environment variables, if possible 23 | func ReadConfig() Config { 24 | cfg := Config{} 25 | if err := env.Parse(&cfg); err != nil { 26 | log.Printf("%+v\n", err) 27 | } 28 | 29 | return cfg 30 | } 31 | -------------------------------------------------------------------------------- /core/database.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pressly/goose" 6 | "gorm.io/driver/sqlite" 7 | "gorm.io/gorm" 8 | "time" 9 | ) 10 | 11 | const ( 12 | MigrationFolder = "core/db/migrations/" 13 | ) 14 | 15 | // Base model which all models extend 16 | type BaseModel struct { 17 | ID uint `gorm:"primarykey"` 18 | CreatedAt time.Time 19 | UpdatedAt time.Time 20 | DeletedAt gorm.DeletedAt `gorm:"index"` 21 | } 22 | 23 | // Base repository which all repositories extend 24 | type BaseRepository struct { 25 | } 26 | 27 | // Returns the singleton database handle 28 | func (receiver BaseRepository) Db() *gorm.DB { 29 | return databaseHandle 30 | } 31 | 32 | // Finds or panics 33 | func (receiver BaseRepository) FindFirstOrPanic(dest interface{}, conds ...interface{}) (tx *gorm.DB) { 34 | result := receiver.Db().First(dest, conds...) 35 | if err := result.Error; err != nil { 36 | panic(err) 37 | } 38 | 39 | return result 40 | } 41 | 42 | // Singleton - only accessible through the repository 43 | var databaseHandle *gorm.DB 44 | 45 | // Package init 46 | func init() { 47 | // Only connects once to the database for the entire app 48 | _db, err := gorm.Open(sqlite.Open(DatabaseFile), &gorm.Config{}) 49 | if err != nil { 50 | panic(fmt.Sprintf("Failed to connect to database: %s", DatabaseFile)) 51 | } 52 | 53 | databaseHandle = _db 54 | } 55 | 56 | // Performs migrations 57 | func Migrate() { 58 | db, _ := goose.OpenDBWithDriver(DatabaseDriver, DatabaseFile) 59 | goose.SetVerbose(true) 60 | if err := goose.Up(db, MigrationFolder); err != nil { 61 | panic(fmt.Sprintf("Migration failed: %s", err)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/db/migrations/20210405172040_base.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | create table users 4 | ( 5 | id integer 6 | primary key, 7 | created_at datetime, 8 | updated_at datetime, 9 | deleted_at datetime, 10 | name text 11 | ); 12 | 13 | create 14 | index idx_users_deleted_at 15 | on users (deleted_at); 16 | -- +goose StatementEnd 17 | 18 | -- +goose Down 19 | -- +goose StatementBegin 20 | DROP TABLE users; 21 | -- +goose StatementEnd 22 | -------------------------------------------------------------------------------- /core/utils.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | const ( 12 | empty = "" 13 | space = " " 14 | ) 15 | 16 | // Extracts the required argument from the HTTP request 17 | func GetRequiredArgument(argumentName string, r *http.Request) string { 18 | arguments, ok := r.URL.Query()[argumentName] 19 | if !ok || len(arguments[0]) < 1 { 20 | panic(fmt.Sprintf("Parameter %s is missing", argumentName)) 21 | } 22 | 23 | return arguments[0] 24 | } 25 | 26 | // Pretty prints the data as json to the stream 27 | func PrettyPrint(w http.ResponseWriter, data interface{}) { 28 | encoder := json.NewEncoder(w) 29 | encoder.SetIndent(empty, space) 30 | _ = encoder.Encode(data) 31 | } 32 | 33 | // Defers the closing of the given closer 34 | func CheckClose(closer io.Closer) { 35 | err := closer.Close() 36 | if err != nil { 37 | log.Fatal("Error during close") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eldonaldo/go-project-template 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/caarlos0/env/v6 v6.5.0 7 | github.com/cosmtrek/air v1.21.2 // indirect 8 | github.com/creack/pty v1.1.11 // indirect 9 | github.com/davecgh/go-spew v1.1.1 // indirect 10 | github.com/fatih/color v1.10.0 // indirect 11 | github.com/imdario/mergo v0.3.12 // indirect 12 | github.com/mattn/go-sqlite3 v1.14.6 // indirect 13 | github.com/pelletier/go-toml v1.9.0 // indirect 14 | github.com/pkg/errors v0.9.1 // indirect 15 | github.com/pressly/goose v2.7.0+incompatible 16 | github.com/stretchr/testify v1.7.0 // indirect 17 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect 18 | gorm.io/driver/sqlite v1.1.4 19 | gorm.io/gorm v1.21.6 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/caarlos0/env/v6 v6.5.0 h1:f4C7ZQwm0nRFo8vETCQviLUOtOlOwsOhgc/QXp0zrTM= 3 | github.com/caarlos0/env/v6 v6.5.0/go.mod h1:5ZqhjfyF261xGkANuSuMQ1FeA9ikA3wzDY64wSd9k8k= 4 | github.com/cosmtrek/air v1.21.2 h1:PwChdKs3qlSkKucKwwC04daw5eoy4SVgiEBQiHX5L9A= 5 | github.com/cosmtrek/air v1.21.2/go.mod h1:5EsgUqrBIHlW2ghNoevwPBEG1FQvF5XNulikjPte538= 6 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 7 | github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= 8 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 13 | github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= 14 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 15 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 16 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 17 | github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 18 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 19 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 20 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 21 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 22 | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 23 | github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= 24 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 25 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 26 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 27 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 28 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 29 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 30 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 31 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 32 | github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= 33 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 34 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 35 | github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= 36 | github.com/pelletier/go-toml v1.9.0 h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0= 37 | github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 38 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 39 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/pressly/goose v2.7.0+incompatible h1:PWejVEv07LCerQEzMMeAtjuyCKbyprZ/LBa6K5P0OCQ= 43 | github.com/pressly/goose v2.7.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 46 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 47 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 48 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 49 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= 55 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 57 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 58 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 60 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 61 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 62 | gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= 63 | gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= 64 | gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= 65 | gorm.io/gorm v1.21.6 h1:xEFbH7WShsnAM+HeRNv7lOeyqmDAK+dDnf1AMf/cVPQ= 66 | gorm.io/gorm v1.21.6/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 67 | -------------------------------------------------------------------------------- /scripts/migration_create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Absolute path to project root 4 | base="$( cd "$(dirname "$0")" ; pwd -P )/.." 5 | cd ${base} 6 | 7 | # name of the migration 8 | name="not-set" 9 | if [[ -z ${1+x} ]]; then 10 | echo "Migration name not set, aborting (usage: ./scripts/migration_create.sh migration_name)"; 11 | exit 12 | else 13 | # concat all args into a single string 14 | name="$*" 15 | 16 | # convert whitespaces to underscores 17 | name=${name// /_} 18 | fi 19 | 20 | migration_folder="core/db/migrations" 21 | database="database.db" 22 | 23 | goose -dir ${migration_folder} sqlite3 ./${database} create ${name} sql -------------------------------------------------------------------------------- /scripts/migration_down.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Absolute path to project root 4 | base="$( cd "$(dirname "$0")" ; pwd -P )/.." 5 | cd ${base} 6 | 7 | migration_folder="core/db/migrations" 8 | database="database.db" 9 | 10 | # downgrades an already performed migration 11 | goose -dir ${migration_folder} sqlite3 ./${database} down -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/eldonaldo/go-project-template/user" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | // Starts the server 11 | func StartServer(port int) { 12 | 13 | // Handlers 14 | http.HandleFunc("/create", user.CreateUser) 15 | http.HandleFunc("/greet", user.GreetHandler) 16 | 17 | address := fmt.Sprintf("0.0.0.0:%d", port) 18 | log.Println(fmt.Sprintf("Listening: %s ", address)) 19 | log.Fatal(http.ListenAndServe(address, nil)) 20 | } 21 | -------------------------------------------------------------------------------- /user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | "github.com/eldonaldo/go-project-template/core" 6 | ) 7 | 8 | // User model 9 | type User struct { 10 | core.BaseModel 11 | Name string 12 | } 13 | 14 | // Greets the user 15 | func Greet(user *User) string { 16 | return fmt.Sprintf("Hi %s", user.Name) 17 | } 18 | -------------------------------------------------------------------------------- /user/user_handler.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | "github.com/eldonaldo/go-project-template/core" 6 | "net/http" 7 | ) 8 | 9 | // Handles user creation 10 | func CreateUser(w http.ResponseWriter, r *http.Request) { 11 | name := core.GetRequiredArgument("name", r) 12 | 13 | // Creates a new user 14 | userRepository := UserRepository{} 15 | _ = userRepository.CreateNew(name) 16 | 17 | _, _ = fmt.Fprintf(w, "User with name %s created", name) 18 | } 19 | 20 | // Handles user greetings 21 | func GreetHandler(w http.ResponseWriter, r *http.Request) { 22 | name := core.GetRequiredArgument("name", r) 23 | 24 | // Find user by name 25 | userRepository := UserRepository{} 26 | user := userRepository.FindByName(name) 27 | 28 | // And greets him 29 | _, _ = fmt.Fprintln(w, Greet(user)) 30 | 31 | // And also return the user model 32 | core.PrettyPrint(w, user) 33 | } 34 | -------------------------------------------------------------------------------- /user/user_repository.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/eldonaldo/go-project-template/core" 5 | ) 6 | 7 | // Table name for user 8 | func (User) TableName() string { 9 | return "users" 10 | } 11 | 12 | // Package init 13 | func init() { 14 | // If you create a new model use the commands below once to auto generate 15 | // SQL statements which are directly issue to the database. The SQL statements 16 | //can then be used as initial SQL setup to bootstrap your migrations 17 | 18 | //userRepository := UserRepository{} 19 | //_ = userRepository.Db().AutoMigrate(&User{}) 20 | } 21 | 22 | // user repository 23 | type UserRepository struct { 24 | core.BaseRepository 25 | } 26 | 27 | // Creates a new user 28 | func (receiver UserRepository) CreateNew(name string) *User { 29 | account := &User{ 30 | Name: name, 31 | } 32 | 33 | receiver.Db().Create(account) 34 | return account 35 | } 36 | 37 | // Finds the user by name 38 | func (receiver UserRepository) FindByName(name string) *User { 39 | var user User 40 | receiver.FindFirstOrPanic(&user, "name = ?", name) 41 | return &user 42 | } 43 | -------------------------------------------------------------------------------- /user/user_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | // Sample unit test the user module 9 | func TestUser_SampleTest(t *testing.T) { 10 | var user = User{Name: "John"} 11 | assert.Equal(t, user.TableName(), "users") 12 | assert.Equal(t, Greet(&user), "Hi John") 13 | } --------------------------------------------------------------------------------