{"id":27134821,"url":"https://github.com/kokoiruby/fast-gin","last_synced_at":"2025-04-10T00:02:48.047Z","repository":{"id":286387564,"uuid":"960470887","full_name":"KokoiRuby/fast-gin","owner":"KokoiRuby","description":"A scaffolder to initialize a project based on Gin web framework.","archived":false,"fork":false,"pushed_at":"2025-04-09T13:22:01.000Z","size":82,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-10T00:01:42.311Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KokoiRuby.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-04-04T13:45:54.000Z","updated_at":"2025-04-09T13:22:06.000Z","dependencies_parsed_at":"2025-04-06T06:27:12.537Z","dependency_job_id":"6d345432-7213-4faf-a956-f078c4fc6878","html_url":"https://github.com/KokoiRuby/fast-gin","commit_stats":null,"previous_names":["kokoiruby/fast-gin"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KokoiRuby%2Ffast-gin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KokoiRuby%2Ffast-gin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KokoiRuby%2Ffast-gin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KokoiRuby%2Ffast-gin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KokoiRuby","download_url":"https://codeload.github.com/KokoiRuby/fast-gin/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248131324,"owners_count":21052819,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-04-08T00:59:23.760Z","updated_at":"2025-04-10T00:02:47.941Z","avatar_url":"https://github.com/KokoiRuby.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# fast-gin\r\n\r\nA scaffolder to initialize a project based on [Gin](https://gin-gonic.com/) web framework.\r\n\r\n## Features\r\n\r\n- **Initialization**: Configuration, Database connection, Redis, Logging, Misc.\r\n- **Command line**: DB initialization, Data import and export.\r\n- **Routes**: Static, Grouping\r\n- **Middleware**: Authentication, Rate limit.\r\n- **JWT**: Login, Logout\r\n- **Common**: File upload, Captcha, List query\r\n- **Deployment**: Dockerfile, docker-compose\r\n\r\n## Configuration\r\n\r\nIn a program, some values that don’t change frequently are typically stored in a configuration file. For example, things like the database address, username and password, JWT expiration time, file upload paths, and so on.\r\n\r\nIf you don’t use a configuration file and want to change a specific configuration, you’d have to recompile your program.\r\n\r\nConfiguration files often use [`YAML`](https://yaml.org/) format. You could also use `TOML`, `INI`, or `JSON`, just parse the corresponding file with Go. However, `YAML` is a bit more flexible and easier to use, plus it supports comments.\r\n\r\nLibrary:\r\n\r\n```bash\r\ngo get gopkg.in/yaml.v3\r\n```\r\n\r\nParse yaml file.\r\n\r\n```go\r\npackage main\r\n\r\nimport (\r\n    \"fmt\"\r\n    \"os\"\r\n\r\n    \"gopkg.in/yaml.v3\"\r\n)\r\n\r\nfunc main() {\r\n    var data map[string]any\r\n    \r\n    byteData, _ := os.ReadFile(\"/path/to/settings.yaml\") // Ignoring error for brevity\r\n    err := yaml.Unmarshal(byteData, \u0026data)\r\n    if err != nil {\r\n        fmt.Println(err)\r\n        return\r\n    }\r\n    fmt.Println(data)\r\n}\r\n```\r\n\r\nImprovements:\r\n\r\n- Use `struct` which provides compile-time type checking and avoids runtime assertions over `map`.\r\n- Use `os.Open` with a decoder for streaming or larger files, avoiding the need to load everything into memory at once.\r\n- Encapsulate parsing into a reusable function for better modularity.\r\n- Handle missing files or fields with defaults.\r\n\r\n```yaml\r\n# Example configuration\r\ndatabase:\r\n  host:     localhost\r\n  port:     5432\r\n  user:     admin\r\n  password: admin\r\n```\r\n\r\n```go\r\npackage main\r\n\r\nimport (\r\n    \"fmt\"\r\n    \"os\"\r\n\r\n    \"gopkg.in/yaml.v3\"\r\n)\r\n\r\ntype Config struct {\r\n    Database struct {\r\n        Host     string `yaml:\"host\"`\r\n        Port     int    `yaml:\"port\"`\r\n        User     string `yaml:\"user\"`\r\n        Password string `yaml:\"password\"`\r\n    } `yaml:\"database\"`\r\n}\r\n\r\nfunc LoadConfig(filename string) (Config, error) {\r\n    config := Config{\r\n        Database: struct {\r\n            Host string `yaml:\"host\"`\r\n            Port int    `yaml:\"port\"`\r\n        }{Host: \"localhost\", Port: 5432}, // Default\r\n        JWTExpiry: \"24h\", // Default value\r\n    }\r\n\r\n    file, err := os.Open(filename)\r\n    if err != nil {\r\n        return config, nil // Return defaults on error\r\n    }\r\n    defer file.Close()\r\n\r\n    decoder := yaml.NewDecoder(file)\r\n    err = decoder.Decode(\u0026config)\r\n    if err != nil {\r\n        return config, fmt.Errorf(\"decoding YAML: %w\", err)\r\n    }\r\n    return config, nil\r\n}\r\n\r\nfunc main() {\r\n    config, err := LoadConfig(\"settings.yaml\")\r\n    if err != nil {\r\n        fmt.Println(\"Error:\", err)\r\n    }\r\n    fmt.Printf(\"Config: %+v\\n\", config)\r\n}\r\n```\r\n\r\nRead the configuration file from a command-line flag instead of hardcoding it.\r\n\r\n```go\r\nfunc LoadConfig() (cfg *config.Config, err error) {  \r\n    cfg = new(config.Config)  \r\n    file, err := os.Open(flags.Options.File)\r\n    ...\r\n}\r\n```\r\n\r\n😖 If the configuration file is modified, the program needs to be restarted to retrieve the new values.\r\n\r\n😕 Is there a way to dynamically modify the configuration without restarting the container?\r\n\r\n💡\r\n\r\n1. Store configuration directly in memory (small or medium project).\r\n2. Access via APIs of configuration management system, such as etcd.\r\n\r\n```go\r\nfunc DumpConfig() error {  \r\n    byteData, err := yaml.Marshal(global.Config)  \r\n    if err != nil {  \r\n       return fmt.Errorf(\"error when dumping configuration: %w\", err)  \r\n    }  \r\n    err = os.WriteFile(flags.Options.File, byteData, 0666)  \r\n    if err != nil {  \r\n       return fmt.Errorf(\"error when dumping configuration: %w\", err)  \r\n    }  \r\n    fmt.Println(\"Configuration dumped successfully\")  \r\n    return nil  \r\n}\r\n```\r\n\r\n## Flags\r\n\r\n| Option | Type     | Description               | Default                  |\r\n| ------ | -------- | ------------------------- | ------------------------ |\r\n| `-f`   | `string` | Configuration file        | `./config/settings.yaml` |\r\n| `-v`   | `bool`   | Print version information | `false`                  |\r\n| `-db`  | `bool`   | Database migration        | `false`                  |\r\n\r\n## Logging\r\n\r\nLogging is a very important aspect. It is highly recommended that everyone logs extensively when working on projects.\r\n\r\n- Where the log is printed?\r\n- When the log is printed?\r\n- Log segmentation: by time, by size?\r\n- Log level?\r\n- ⚠ Fatal when error occurs in loading configurations. Do not fatal during runtime.\r\n\r\nThis also brings up the question of whether backend errors should be returned to the frontend.\r\n\r\n- If it’s for internal company use, just return the errors directly. That way, when an error occurs later, you can immediately know the reason and fix it.\r\n- But if it’s for external use, directly returning backend errors makes your product seem unprofessional. It’s better to standardize the responses, such as \"network error\" or \"system error,\" and then display the specific error details in the logs.\r\n\r\nTo choose:\r\n\r\n- **[zap](https://github.com/uber-go/zap)** for new projects, especially those requiring high performance and active maintenance, given its benchmarks and community backing.\r\n- **[logrus](https://github.com/sirupsen/logrus)** for existing projects where its feature set is already integrated, but be aware of its maintenance mode and potential need for migration in the future.\r\n\r\nHere we choose logrus.\r\n\r\n```go\r\ngo get github.com/sirupsen/logrus\r\n```\r\n\r\n### Format\r\n\r\nImplement `Format(entry *logrus.Entry) ([]byte, error)`.\r\n\r\n```go\r\ntype MyLog struct {}\r\n\r\nfunc (MyLog) Format(entry *logrus.Entry) ([]byte, error) {  \r\n    // Color  \r\n    var color int  \r\n    switch entry.Level {  \r\n    case logrus.DebugLevel, logrus.TraceLevel:  \r\n       color = gray  \r\n    case logrus.WarnLevel:  \r\n       color = yellow  \r\n    case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:  \r\n       color = red  \r\n    default:  \r\n       color = blue  \r\n    }  \r\n  \r\n    // Buffer is required for formatting log messages before outputting them.  \r\n    var buf *bytes.Buffer  \r\n    if entry.Buffer != nil {  \r\n       buf = entry.Buffer  \r\n    } else {  \r\n       buf = \u0026bytes.Buffer{}  \r\n    }  \r\n    \r\n    // Time format  \r\n    timeFormat := entry.Time.Format(\"2006-01-02T15:04:05Z0700\")  \r\n  \r\n    if entry.HasCaller() {  \r\n       // Custom file path and line  \r\n       funcVal := entry.Caller.Function  \r\n       fileVal := fmt.Sprintf(\"%s:%d\", path.Base(entry.Caller.File), entry.Caller.Line)  \r\n       // Custom format  \r\n       _, err := fmt.Fprintf(buf, \"[%s] \\x1b[%dm[%s]\\x1b[0m %s %s %s\\n\", timeFormat, color, entry.Level, fileVal, funcVal, entry.Message)  \r\n       if err != nil {  \r\n          return nil, err  \r\n       }  \r\n    }    return buf.Bytes(), nil  \r\n}\r\n```\r\n\r\n```go\r\nfunc InitLogger() {  \r\n    logrus.SetLevel(logrus.DebugLevel)  \r\n    logrus.SetReportCaller(true)  \r\n    logrus.SetFormatter(MyLog{})  \r\n    //logrus.SetFormatter(\u0026logrus.JSONFormatter{})  // To external\r\n}\r\n```\r\n\r\n### Hook\r\n\r\nHooks are called whenever a log entry is created. \r\n\r\n```go\r\ntype MyHook struct {  \r\n    file     *os.File   // Log file  \r\n    errFile  *os.File   // Error log file  \r\n    fileDate string     // Date of log file  \r\n    logPath  string     // Path of log file  \r\n    mu       sync.Mutex // Mutex lock  \r\n}\r\n```\r\n\r\n```go\r\nfunc InitLogger() {  \r\n    ...\r\n    logrus.AddHook(\u0026MyHook{  \r\n       logPath: \"logs\",  \r\n    })  \r\n}\r\n```\r\n\r\n```go\r\nfunc (hook *MyHook) Fire(entry *logrus.Entry) error {\r\n\thook.mu.Lock()\r\n\tdefer hook.mu.Unlock()\r\n\r\n    date := entry.Time.Format(\"2006-01-02\")  \r\n    if hook.fileDate != date {  \r\n       // Rotate if day is passed  \r\n       if err := hook.rotate(date); err != nil {  \r\n          return err  \r\n       }  \r\n    }  \r\n    \r\n    // Dump logs to file  \r\n    entryStr, err := entry.String()  \r\n    if err != nil {  \r\n       return fmt.Errorf(\"failed to get log entry: %v\", err)  \r\n    }  \r\n    if _, err := hook.file.Write([]byte(entryStr)); err != nil {  \r\n       return fmt.Errorf(\"failed to write to log file: %v\", err)  \r\n    }  \r\n  \r\n    // Dump error logs to file  \r\n    if entry.Level \u003c= logrus.ErrorLevel {  \r\n       if _, err := hook.errFile.Write([]byte(entryStr)); err != nil {  \r\n          return fmt.Errorf(\"failed to write to error log file: %v\", err)  \r\n       }  \r\n    }  \r\n    return nil  \r\n}\r\n```\r\n\r\n```go\r\nfunc (hook *MyHook) rotate(date string) error {  \r\n    if hook.file != nil {  \r\n       // Close the old one  \r\n       if err := hook.file.Close(); err != nil {  \r\n          return fmt.Errorf(\"failed to close the old log file when rotation: %v\", err)  \r\n       }  \r\n    }    if hook.errFile != nil {  \r\n       // Close the old one  \r\n       if err := hook.errFile.Close(); err != nil {  \r\n          return fmt.Errorf(\"failed to close the old error log file when rotation: %v\", err)  \r\n       }  \r\n    }  \r\n    \r\n    // Log file directory  \r\n    dir := fmt.Sprintf(\"%s/%s\", hook.logPath, date)  \r\n    if err := os.MkdirAll(dir, os.ModePerm); err != nil {  \r\n       return fmt.Errorf(\"failed to create log directory: %v\", err)  \r\n    }  \r\n  \r\n    infoLog := fmt.Sprintf(\"%s/info.log\", dir)  \r\n    errLog := fmt.Sprintf(\"%s/err.log\", dir)  \r\n  \r\n    // Create new log files  \r\n    var err error  \r\n    hook.file, err = os.OpenFile(infoLog, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)  \r\n    if err != nil {  \r\n       return fmt.Errorf(\"failed to open log file: %v\", err)  \r\n    }  \r\n    hook.errFile, err = os.OpenFile(errLog, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)  \r\n    if err != nil {  \r\n       return fmt.Errorf(\"failed to open error log file: %v\", err)  \r\n    }  \r\n  \r\n    // Update file date  \r\n    hook.fileDate = date  \r\n    return nil  \r\n}\r\n```\r\n\r\n## [GORM](https://gorm.io/docs/connecting_to_the_database.html)\r\n\r\nGORM officially supports the databases MySQL, PostgreSQL, SQLite, SQL Server, and TiDB.\r\n\r\nThis scaffolder supports MySQL, PostgreSQL, SQLite.\r\n\r\n```bash\r\ngo get gorm.io/gorm\r\ngo get gorm.io/driver/mysql\r\ngo get gorm.io/driver/postgres\r\ngo get github.com/glebarez/sqlite\r\n```\r\n\r\n⚠ For SQLite in CGO.\r\n\r\n```bash\r\ngo get gorm.io/driver/sqlite\r\n```\r\n\r\n```yaml\r\ndb:  \r\n    mode: mysql # Supports: mysql pgsql sqlite  \r\n    db_name:  \r\n    host:   \r\n    port: 3306  \r\n    user:  \r\n    password:\r\n```\r\n\r\nUse simple factory pattern to initialize `gorm.DB` given `mode`.\r\n\r\n```go\r\ntype DB struct {  \r\n    Mode     DBMode `yaml:\"mode\"` // Supports: mysql pgsql sqlite  \r\n    DBName   string `yaml:\"db_name\"`  \r\n    Host     string `yaml:\"host\"`  \r\n    Port     int    `yaml:\"port\"`  \r\n    User     string `yaml:\"user\"`  \r\n    Password string `yaml:\"password\"`  \r\n}  \r\n  \r\nfunc (db DB) GetDSN() gorm.Dialector {  \r\n    switch db.Mode {  \r\n    case MYSQL:  \r\n       dsn := fmt.Sprintf(\"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4\u0026parseTime=True\u0026loc=Local\",  \r\n          db.User,  \r\n          db.Password,  \r\n          db.Host,  \r\n          db.Port,  \r\n          db.DBName,  \r\n       )  \r\n       return mysql.Open(dsn)  \r\n    case PG:  \r\n       dsn := fmt.Sprintf(\"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai\",  \r\n          db.User,  \r\n          db.Password,  \r\n          db.Host,  \r\n          db.Port,  \r\n          db.DBName,  \r\n       )  \r\n       return postgres.Open(dsn)  \r\n    case SQLITE:  \r\n       return sqlite.Open(db.DBName)  \r\n    case \"\":  \r\n       logrus.Warnf(\"Database mode not specified\")  \r\n       return nil  \r\n    default:  \r\n       logrus.Fatalf(\"Database is not supported\")  \r\n       return nil  \r\n    }  \r\n}\r\n```\r\n\r\n```go\r\nfunc InitGorm() (db *gorm.DB) {  \r\n    cfg := global.Config.DB  \r\n  \r\n    dialector := cfg.GetDSN()  \r\n    if dialector == nil {  \r\n       return  \r\n    }  \r\n  \r\n    // Open initialize db session based on dialector  \r\n    database, err := gorm.Open(dialector, \u0026gorm.Config{  \r\n       DisableForeignKeyConstraintWhenMigrating: true,  \r\n    })  \r\n    if err != nil {  \r\n       logrus.Fatalf(\"Failed to connect to database: %v\", err)  \r\n    }  \r\n  \r\n    // Get DB connection pool  \r\n    sqlDB, err := database.DB()  \r\n    if err != nil {  \r\n       logrus.Fatalf(\"Failed to get database connection pool: %s\", err)  \r\n       return  \r\n    }  \r\n    err = sqlDB.Ping()  \r\n    if err != nil {  \r\n       logrus.Fatalf(\"Failed to probe database connection pool liveness: %s\", err)  \r\n       return  \r\n    }  \r\n  \r\n    // Configure DB connection pool  \r\n    // TODO: Add to configuration file  \r\n    sqlDB.SetMaxIdleConns(10)  \r\n    sqlDB.SetMaxOpenConns(100)  \r\n    sqlDB.SetConnMaxLifetime(time.Hour)  \r\n  \r\n    logrus.Infof(\"DB initialized successfully\")  \r\n    return  \r\n}\r\n```\r\n\r\n## Redis\r\n\r\n```bash\r\ngo get github.com/redis/go-redis/v9\r\n```\r\n\r\n```yaml\r\nredis:  \r\n    addr: \"127.0.0.1:6379\"  \r\n    password: \"\"  \r\n    db: 1\r\n```\r\n\r\n```go\r\ntype Redis struct {  \r\n    Addr     string `yaml:\"addr\"`  \r\n    Password string `yaml:\"password\"`  \r\n    DB       int    `yaml:\"db\"`  \r\n}\r\n```\r\n\r\n```go\r\nfunc InitRedis() *redis.Client {  \r\n    cfg := global.Config  \r\n    rdb := redis.NewClient(\u0026redis.Options{  \r\n       Addr:     cfg.Redis.Addr,  \r\n       Password: cfg.Redis.Password,  \r\n       DB:       cfg.Redis.DB,  \r\n    })  \r\n  \r\n    _, err := rdb.Ping(context.Background()).Result()  \r\n    if err != nil {  \r\n       logrus.Errorf(\"Failed to connect to redis: %s\", err)  \r\n       return nil  \r\n    }  \r\n    logrus.Infof(\"Connect to redis successfully\")  \r\n    return rdb  \r\n}\r\n```\r\n\r\n```go\r\nfunc main() {  \r\n    ... \r\n    // Redis  \r\n    global.Redis = core.InitRedis()  \r\n  \r\n}\r\n```\r\n\r\n## Database migration\r\n\r\n```go\r\ntype Model struct {  \r\n    ID        uint `gorm:\"primaryKey\"`  \r\n    CreatedAt time.Time  \r\n    UpdatedAt time.Time  \r\n}\r\n```\r\n\r\n```go\r\ntype UserModel struct {  \r\n    Model           // Base  \r\n    Username string `gorm:\"size:16\" json:\"username\"`  \r\n    Nickname string `gorm:\"size:32\" json:\"nickname\"`  \r\n    Password string `gorm:\"size:64\" json:\"password\"`  \r\n    RoleID   int8   `json:\"roleID\"` // 1: admin, 2: normal  \r\n  \r\n    // TODO: Email, Phone, UUID, OpenID...  \r\n}\r\n```\r\n\r\n```go\r\nfunc MigrateDB() {  \r\n    err := global.DB.AutoMigrate(\u0026models.UserModel{})  \r\n    if err != nil {  \r\n       logrus.Errorf(\"Failed to migrate database: %s\", err)  \r\n       return  \r\n    }  \r\n    logrus.Infof(\"Migrate database successfully\")  \r\n}\r\n```\r\n\r\n## User\r\n\r\n```go\r\ntype User struct {}\r\n```\r\n\r\nCreate a user.\r\n\r\n```go\r\nfunc (User) Create() {  \r\n    var user models.UserModel  \r\n  \r\n    // Role  \r\n    fmt.Println(\"Please select a role for user (1 (admin) 2 (normal)): \")  \r\n    _, err := fmt.Scanln(\u0026user.RoleID)  \r\n    if err != nil {  \r\n       fmt.Println(\"Input error:\", err)  \r\n       return  \r\n    }  \r\n    if user.RoleID != 1 \u0026\u0026 user.RoleID != 2 {  \r\n       fmt.Println(\"Role err:\", err)  \r\n       return  \r\n    }  \r\n  \r\n    // Username  \r\n    for {  \r\n       fmt.Println(\"Please input username: \")  \r\n       _, err = fmt.Scanln(\u0026user.Username)  \r\n       if err != nil {  \r\n          fmt.Println(\"Input error:\", err)  \r\n          return  \r\n       }  \r\n       var u models.UserModel  \r\n       err = global.DB.Take(\u0026u, \"username = ?\", user.Username).Error  \r\n       if err == nil {  \r\n          fmt.Println(\"User already exists\")  \r\n          continue  \r\n       }  \r\n       break  \r\n    }  \r\n  \r\n    // Password  \r\n    fmt.Println(\"Please input password: \")  \r\n    password, err := terminal.ReadPassword(int(os.Stdin.Fd()))  \r\n    if err != nil {  \r\n       fmt.Println(\"Failed to read password:\", err)  \r\n       return  \r\n    }  \r\n    fmt.Println(\"Please input password again: \")  \r\n    rePassword, err := terminal.ReadPassword(int(os.Stdin.Fd()))  \r\n    if err != nil {  \r\n       fmt.Println(\"Failed to read password:\", err)  \r\n       return  \r\n    }  \r\n    if string(password) != string(rePassword) {  \r\n       fmt.Println(\"Password mismatched\")  \r\n       return  \r\n    }  \r\n  \r\n    // Persist  \r\n    encryptedPassword, err := pwd.Encrypt(string(password))  \r\n    if err != nil {  \r\n       fmt.Println(\"Failed to encrypt password:\", err)  \r\n    }  \r\n    err = global.DB.Create(\u0026models.UserModel{  \r\n       Username: user.Username,  \r\n       Password: encryptedPassword,  \r\n       RoleID:   user.RoleID,  \r\n    }).Error  \r\n    if err != nil {  \r\n       logrus.Errorf(\"Failed to create user: %s\", err)  \r\n       return  \r\n    }  \r\n    logrus.Infof(\"Create user [%s] successfully\", user.Username)  \r\n}\r\n```\r\n\r\n```bash\r\ngo run main.go -res user -op create\r\n```\r\n\r\nEncrypt password by `bcrypt`.\r\n\r\n```bash\r\ngo get golang.org/x/crypto/bcrypt\r\n```\r\n\r\n```go\r\nfunc Encrypt(password string) (string, error) {  \r\n    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)  \r\n    if err != nil {  \r\n       logrus.Errorf(\"Failed to encrypt password: %s\", err)  \r\n       return \"\", err  \r\n    }  \r\n    return string(hashedPassword), nil  \r\n}\r\n```\r\n\r\nList users.\r\n\r\n```go\r\nfunc (User) List() {  \r\n    var userList []models.UserModel  \r\n    global.DB.Order(\"created_at desc\").Limit(10).Find(\u0026userList)  \r\n    for _, model := range userList {  \r\n       fmt.Printf(\"UserID: %d  Username: %s Nickname: %s Role: %d CreatedAt: %s\\n\",  \r\n          model.ID,  \r\n          model.Username,  \r\n          model.Nickname,  \r\n          model.RoleID,  \r\n          model.CreatedAt.Format(\"2006-01-02 15:04:05\"),  \r\n       )  \r\n    }  \r\n}\r\n```\r\n\r\n```bash\r\ngo run main.go -res user -op list\r\n```\r\n\r\nRemove a user.\r\n\r\n```go\r\nfunc (User) Remove() {  \r\n    var username string  \r\n  \r\n    // Username  \r\n    for {  \r\n       fmt.Println(\"Please input username of user to be deleted: \")  \r\n       _, err := fmt.Scanln(\u0026username)  \r\n       if err != nil {  \r\n          fmt.Println(\"Input error:\", err)  \r\n          return  \r\n       }  \r\n       var u models.UserModel  \r\n       err = global.DB.Take(\u0026u, \"username = ?\", username).Error  \r\n       if err != nil {  \r\n          fmt.Println(\"User does not exist\")  \r\n          continue  \r\n       }  \r\n       break  \r\n    }  \r\n  \r\n    err := global.DB.  \r\n       Where(\"username = ?\", username).  \r\n       Delete(\u0026models.UserModel{}).Error  \r\n    if err != nil {  \r\n       logrus.Errorf(\"Failed to delete user: %s\", err)  \r\n       return  \r\n    }  \r\n    logrus.Infof(\"Delete user [%s] successfully\", username)  \r\n}\r\n```\r\n\r\n```bash\r\ngo run main.go -res user -op remove\r\n```\r\n\r\n## Routing\r\n\r\n```go\r\nfunc Run() {  \r\n    gin.SetMode(global.Config.Gin.Mode)  \r\n  \r\n    r := gin.Default()  \r\n  \r\n    // Static route  \r\n    // curl http://localhost:8080/uploads/test.txt  \r\n    r.Static(\"/uploads\", \"./static/uploads\")  \r\n  \r\n    // Grouping routes  \r\n    root := r.Group(\"api\")  \r\n    UserRouter(root)  \r\n  \r\n    // Run Gin server  \r\n    err := r.Run(global.Config.Gin.Addr())  \r\n    if err != nil {  \r\n       logrus.Fatalf(\"Failed to start Gin server: %v\", err)  \r\n       return  \r\n    }  \r\n}\r\n```\r\n\r\n```go\r\nfunc UserRouter(g *gin.RouterGroup) {  \r\n    userAPI := api.Apis.UserAPI  \r\n  \r\n    r := g.Group(\"users\").Use()  \r\n  \r\n    r.POST(\"/login\", userAPI.LoginView)  \r\n}\r\n```\r\n\r\n```go\r\ntype APIs struct {  \r\n    UserAPI user.API  \r\n}  \r\n  \r\nvar Apis = new(APIs)\r\n```\r\n\r\n```go\r\nfunc (API) LoginView(c *gin.Context) {  \r\n    c.String(http.StatusOK, \"Login successfully\")  \r\n    return  \r\n}\r\n```\r\n\r\nVerify.\r\n\r\n```bash\r\ncurl http://localhost:8080/uploads/test.txt\r\n```\r\n\r\n```bash\r\ncurl -X POST http://localhost:8080/v1/users/login\r\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkokoiruby%2Ffast-gin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkokoiruby%2Ffast-gin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkokoiruby%2Ffast-gin/lists"}