Kubernetes 이야기

Golang - Gin Framework 본문

개발/go

Golang - Gin Framework

kmaster 2022. 8. 8. 18:38
반응형

Golang은 Google에서 만든 프로그래밍 언어로 C와 유사한 구문을 사용하여 정적으로 유형이 지정되고 컴파일를 지원하는 언어이다. Gin은 사용하기 쉽고 빠르게 설계된 Go 웹 프레임워크이다.

 

Gin에는 Go언로를 사용하여 개발을 좀 더 빠르고 쉽게 할 수 있도록 많은 기능을 제공한다.

 

  • 내장 웹 서버
  • 자동 라우팅
  • 요청 및 응답을 처리하기 위한 도구
  • 기능 추가를 위한 미들웨어
  • 내장 Logger
  • Template 지원

Gin은 다른 Web Framework보다 많은 Star를 가지고 있는 Framework이다.

출처 : https://github.com/mingrammer/go-web-framework-stars

Go 설치

 

# wget https://go.dev/dl/go1.19.linux-amd64.tar.gz
# tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz

설치 후 .bashrc에 path 에 등록한다.

export PATH=$PATH:/usr/local/go/bin

 

Go 프로젝트 생성

프로젝트 폴더 생성 후 아래와 같이 go 명령어를 이용하여 Gin을 설치한다.

# mkdir myproject
# cd myproject
# go mod init github.com/kmaster8/gin/myproject
# go install github.com/gin-gonic/gin

sample code

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "message": "pong",
    })
  })
  r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

sample code를 실행해 보자.

# go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

 

실행 시 "go: go.mod file not found in current directory or any parent directory." 오류가 발생하면 go env -w GO111MODULE=auto 를 실행한다.

 

브라우저에서 호출해 보자.

Parameter

func main() {
  router := gin.Default()

  router.GET("/user/:name", func(c *gin.Context) {
    name := c.Param("name")
    c.String(http.StatusOK, "Hello %s", name)
  })

  router.GET("/user/:name/*action", func(c *gin.Context) {
    name := c.Param("name")
    action := c.Param("action")
    message := name + " is " + action
    c.String(http.StatusOK, message)
  })

  router.POST("/user/:name/*action", func(c *gin.Context) {
    b := c.FullPath() == "/user/:name/*action" // true
    c.String(http.StatusOK, "%t", b)
  })

  router.GET("/user/groups", func(c *gin.Context) {
    c.String(http.StatusOK, "The available groups are [...]")
  })

  router.Run(":8080")
}

실행결과

# curl localhost:8080/user
404 page not found

# curl localhost:8080/user/kmaster8
Hello kmaster8

# curl localhost:8080/user/kmaster8/
kmaster8 is /

# curl localhost:8080/user/kmaster8/test
kmaster8 is /test

# curl -X POST localhost:8080/user/kmaster8/test
true

# curl localhost:8080/user/groups
The available groups are [...]

 

Querystring parameters

func main() {
  router := gin.Default()

  router.GET("/welcome", func(c *gin.Context) {
    firstname := c.DefaultQuery("firstname", "Guest")
    lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

    c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
  })
  router.Run(":8080")
}

실행결과

# curl "localhost:8080/welcome?firstname=hong&lastname=gildong"
Hello hong gildong

 

Post Parameters

func main() {
  router := gin.Default()

  router.POST("/post", func(c *gin.Context) {

    id := c.Query("id")
    page := c.DefaultQuery("page", "0")
    name := c.PostForm("name")
    message := c.PostForm("message")

    fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
  })
  router.Run(":8080")
}

실행결과

# curl -X POST "localhost:8080/post?id=1234&page=1" -d "name=manu&message=this_is_great"
id: 1234; page: 1; name: manu; message: this_is_great

 

Upload File

func main() {
  router := gin.Default()
  router.MaxMultipartMemory = 8 << 20  // 8 MiB
  router.POST("/upload", func(c *gin.Context) {
    // Single file
    file, err := c.FormFile("file")
    log.Println(file.Filename)

    if err != nil {
        c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
        return
    }

    // Upload the file to specific dst.
    dst := "/tmp/upload/" + file.Filename
    if err := c.SaveUploadedFile(file, dst); err != nil {
        c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
        return
    }


    c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
  })
  router.Run(":8080")
}

실행결과

# curl -X POST http://localhost:8080/upload   -F "file=@/tmp/a.txt"   -H "Content-Type: multipart/form-data"
'a.txt' uploaded!

 

Grouping routes

func main() {
  router := gin.Default()

  // Simple group: v1
  v1 := router.Group("/v1")
  {
    v1.POST("/login", loginEndpoint)
    v1.POST("/submit", submitEndpoint)
    v1.POST("/read", readEndpoint)
  }

  // Simple group: v2
  v2 := router.Group("/v2")
  {
    v2.POST("/login", loginEndpoint)
    v2.POST("/submit", submitEndpoint)
    v2.POST("/read", readEndpoint)
  }

  router.Run(":8080")
}

func loginEndpoint(c *gin.Context) {
  getPath := c.Request.URL.String()
  c.JSON(200, gin.H{
    "pathInfo": getPath,
  })
}

func submitEndpoint(c *gin.Context) {
  getPath := c.Request.URL.String()
  c.JSON(200, gin.H{
    "pathInfo": getPath,
  })
}

func readEndpoint(c *gin.Context) {
  getPath := c.Request.URL.String()
  c.JSON(200, gin.H{
    "pathInfo": getPath,
  })
}

실행결과

# curl -X POST http://localhost:8080/v1/login
{"pathInfo":"/v1/login"}

# curl -X POST http://localhost:8080/v2/login
{"pathInfo":"/v2/login"}

 

Custom Recovery behavior

func main() {
  // Creates a router without any middleware by default
  r := gin.New()

  // Global middleware
  // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
  // By default gin.DefaultWriter = os.Stdout
  r.Use(gin.Logger())

  // Recovery middleware recovers from any panics and writes a 500 if there was one.
  r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
    if err, ok := recovered.(string); ok {
      c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
    }
    c.AbortWithStatus(http.StatusInternalServerError)
  }))

  r.GET("/panic", func(c *gin.Context) {
    // panic with a string -- the custom middleware could save this to a database or report it to the user
    panic("foo")
  })

  r.GET("/", func(c *gin.Context) {
    c.String(http.StatusOK, "ohai")
  })

  // Listen and serve on 0.0.0.0:8080
  r.Run(":8080")
}

실행결과

]# curl http://localhost:8080/
ohai

# curl http://localhost:8080/panic
error: foo

--> 서버 로그

2022/08/08 13:50:12 [Recovery] 2022/08/08 - 13:50:12 panic recovered:
GET /panic HTTP/1.1
Host: localhost:8080
Accept: */*
User-Agent: curl/7.29.0


foo
/root/go/myproject/recovery.go:28 (0x744166)
        main.func2: panic("foo")
/root/go/pkg/mod/github.com/gin-gonic/gin@v1.8.1/context.go:173 (0x73e421)
        (*Context).Next: c.handlers[c.index](c)
/root/go/pkg/mod/github.com/gin-gonic/gin@v1.8.1/recovery.go:101 (0x73e40c)
        CustomRecoveryWithWriter.func1: c.Next()
/root/go/pkg/mod/github.com/gin-gonic/gin@v1.8.1/context.go:173 (0x73d526)
        (*Context).Next: c.handlers[c.index](c)
/root/go/pkg/mod/github.com/gin-gonic/gin@v1.8.1/logger.go:240 (0x73d509)
        LoggerWithConfig.func1: c.Next()
/root/go/pkg/mod/github.com/gin-gonic/gin@v1.8.1/context.go:173 (0x73c5f0)
        (*Context).Next: c.handlers[c.index](c)
/root/go/pkg/mod/github.com/gin-gonic/gin@v1.8.1/gin.go:616 (0x73c258)
        (*Engine).handleHTTPRequest: c.Next()
/root/go/pkg/mod/github.com/gin-gonic/gin@v1.8.1/gin.go:572 (0x73bf1c)
        (*Engine).ServeHTTP: engine.handleHTTPRequest(c)
/usr/local/go/src/net/http/server.go:2947 (0x6214eb)
        serverHandler.ServeHTTP: handler.ServeHTTP(rw, req)
/usr/local/go/src/net/http/server.go:1991 (0x61dbc6)
        (*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req)
/usr/local/go/src/runtime/asm_amd64.s:1594 (0x4672a0)
        goexit: BYTE    $0x90   // NOP

[GIN] 2022/08/08 - 13:50:12 | 500 |    1.257669ms |             ::1 | GET      "/panic"

--> 재호출
# curl http://localhost:8080/
ohai

 

Model binding and validation

http form이든 json, xml 이든 동일한 json 모델에 맵핑이 가능하다.

// Binding from JSON
type Login struct {
  User     string `form:"user" json:"user" xml:"user"  binding:"required"`
  Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

func main() {
  router := gin.Default()

  // Example for binding JSON ({"user": "manu", "password": "123"})
  router.POST("/loginJSON", func(c *gin.Context) {
    var json Login
    if err := c.ShouldBindJSON(&json); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
    }

    if json.User != "manu" || json.Password != "123" {
      c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
      return
    }

    c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
  })

  // Example for binding a HTML form (user=manu&password=123)
  router.POST("/loginForm", func(c *gin.Context) {
    var form Login
    // This will infer what binder to use depending on the content-type header.
    if err := c.ShouldBind(&form); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
    }

    if form.User != "manu" || form.Password != "123" {
      c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
      return
    }

    c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
  })

  // Listen and serve on 0.0.0.0:8080
  router.Run(":8080")
}

실행결과

# curl -v -X POST \
>   http://localhost:8080/loginJSON \
>   -H 'content-type: application/json' \
>   -d '{ "user": "manu" }'
* About to connect() to localhost port 8080 (#0)
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> POST /loginJSON HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:8080
> Accept: */*
> content-type: application/json
> Content-Length: 18
>
* upload completely sent off: 18 out of 18 bytes
< HTTP/1.1 400 Bad Request
< Content-Type: application/json; charset=utf-8
< Date: Mon, 08 Aug 2022 05:40:21 GMT
< Content-Length: 100
<
* Connection #0 to host localhost left intact
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}[root@p-thlee-master myproject]#

 

Testing

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func setupRouter() *gin.Engine {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.String(http.StatusOK, "pong")
  })
  return r
}

func main() {
  r := setupRouter()
  r.Run(":8080")
}

위의 테스트 코드는 아래와 같다.

package main

import (
  "net/http"
  "net/http/httptest"
  "testing"

  "github.com/stretchr/testify/assert"
)

func TestPingRoute(t *testing.T) {
  router := setupRouter()

  w := httptest.NewRecorder()
  req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
  router.ServeHTTP(w, req)

  assert.Equal(t, http.StatusOK, w.Code)
  assert.Equal(t, "pong", w.Body.String())
}

실행결과

=== RUN   TestPingRoute
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> command-line-arguments.setupRouter.func1 (3 handlers)
[GIN] 2022/08/08 - 17:40:37 | 200 |       8.403µs |                 | GET      "/ping"
--- PASS: TestPingRoute (0.00s)
PASS
ok      command-line-arguments  0.010s

 

Swagger

# go install github.com/swaggo/swag/cmd/swag@latest
# go get -u github.com/swaggo/files
# go get -u github.com/swaggo/gin-swagger

 

# swag -h
NAME:
   swag - Automatically generate RESTful API documentation with Swagger 2.0 for Go.

USAGE:
   swag [global options] command [command options] [arguments...]

VERSION:
   v1.8.4

COMMANDS:
   init, i  Create docs.go
   fmt, f   format swag comments
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help (default: false)
   --version, -v  print the version (default: false)

main.go 는 아래와 같다.

package main

import (
  swaggerFiles "github.com/swaggo/files"
  ginSwagger "github.com/swaggo/gin-swagger"
  "github.com/gin-gonic/gin"
)

func main() {
  router := gin.Default()

  router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

  router.Run(":8080")
}

swag init는 API에 대한 문서를 업데이트할 때마다 실행 합니다.

# swag init
2022/08/08 17:59:19 Generate swagger docs....
2022/08/08 17:59:19 Generate general API Info, search dir:./
2022/08/08 17:59:19 create docs.go at  docs/docs.go
2022/08/08 17:59:19 create swagger.json at  docs/swagger.json
2022/08/08 17:59:19 create swagger.yaml at  docs/swagger.yaml

실행을 하면 docs 하위에 swagger 문서가 생성된다.

# tree docs
docs
├── docs.go
├── swagger.json
└── swagger.yaml

0 directories, 3 files

한번 docs 를 호출해 보자.

아직 swagger 관련 내용이 없어 error가 발생한다. 아래와 같이 main.go 를 수정한다.

package main

import (
  _ "github.com/kmaster8/gin/myproject/docs"
  swaggerFiles "github.com/swaggo/files"
  ginSwagger "github.com/swaggo/gin-swagger"
  "github.com/gin-gonic/gin"
)

func setupRouter() *gin.Engine {

        r := gin.Default()

        r.GET("/hello/:name", hello)
        r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

        return r
}

// Hello      godoc
// @Summary      hello name
// @Description  이름을 리턴한다.
// @Accept       json
// @Produce      json
// @Param name path string true "Name"
// @Success      200
// @Router       /hello/{name} [get]
func hello(c *gin.Context) {
  name := c.Param("name")
  c.JSON(200, gin.H{"name ": name})
}

// @title           Swagger Example API
// @version         1.0
// @description     This is a sample server celler server.
// @termsOfService  http://swagger.io/terms/

// @contact.name   API Support
// @contact.url    http://www.swagger.io/support
// @contact.email  support@swagger.io

// @license.name  Apache 2.0
// @license.url   http://www.apache.org/licenses/LICENSE-2.0.html

// @host      localhost:8080
// @BasePath  /
func main() {
  router := setupRouter()

  router.Run(":8080")
}

[실행결과]

 

 

참고

https://github.com/gin-gonic/gin

https://github.com/swaggo/swag

반응형

'개발 > go' 카테고리의 다른 글

GoDS ( Go Data Structures )  (0) 2022.08.24
Viper  (0) 2022.08.23
cobra library 사용법  (0) 2022.08.22
모둘과 패키지  (0) 2022.08.22
Kubebuilder ( Kubernetes operator )  (0) 2022.08.17
Comments