Skip to content

Gin

공식 문서: Gin Web Framework (gin-gonic.com)

저장소 주소: gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)

공식 예제: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

소개

Gin 은 Go(Golang) 로 작성된 웹 프레임워크입니다. martini 와 유사한 API 를 가지고 있으며 성능이 훨씬 좋고 httprouter 덕분에 속도가 40 배 향상되었습니다. 성능과 좋은 생산성이 필요하다면 Gin 을 좋아하게 될 것입니다. Gin 은 Iris 와 Beego 에 비해 더 경량화된 프레임워크로 웹 부분만 담당하며 극致的인 라우팅 성능을 추구합니다. 기능이 그렇게 많지는 않지만 가볍고 확장하기 쉽다는 것이 장점입니다. 따라서 모든 웹 프레임워크 중에서 Gin 이 가장 쉽게 배우고 익힐 수 있습니다.

특징

  • 빠름: Radix 트리 기반 라우팅, 작은 메모리 사용량. 리플렉션 없음. 예측 가능한 API 성능.
  • 미들웨어 지원: 들어오는 HTTP 요청은 일련의 미들웨어와 최종 작업으로 처리될 수 있습니다. 예: Logger, Authorization, GZIP, 최종 작업 DB.
  • Crash 처리: Gin 은 HTTP 요청에서 발생한 panic 을 catch 하고 recover 할 수 있습니다. 이렇게 하면 서버가 항상 사용 가능합니다.
  • JSON 검증: Gin 은 요청의 JSON 을 구문 분석하고 검증할 수 있습니다 (예: 필수 값 존재 확인).
  • 라우팅 그룹: 라우팅을 더 잘 구성할 수 있습니다. 권한 부여 필요 여부, 다른 API 버전 등... 또한 이러한 그룹은 성능 저하 없이 무제한으로 중첩될 수 있습니다.
  • 오류 관리: Gin 은 HTTP 요청 중에 발생하는 모든 오류를 수집하는 편리한 방법을 제공합니다. 최종적으로 미들웨어는 이를 로그 파일, 데이터베이스에 기록하거나 네트워크를 통해 전송할 수 있습니다.
  • 내장 렌더링: Gin 은 JSON, XML 및 HTML 렌더링을 위한 사용하기 쉬운 API 를 제공합니다.
  • 확장성: 새 미들웨어를 만드는 것은 매우 간단합니다.

설치

현재 시점 2022/11/22 에서 gin 이 지원하는 go 의 최소 버전은 1.16 이며 go mod 를 사용하여 프로젝트 종속성을 관리하는 것이 좋습니다.

powershell
go get -u github.com/gin-gonic/gin

가져오기

go
import "github.com/gin-gonic/gin"

빠른 시작

go
package main

import (
   "github.com/gin-gonic/gin"
   "net/http"
)

func main() {
   engine := gin.Default() // gin 엔진 생성
   engine.GET("/ping", func(context *gin.Context) {
      context.JSON(http.StatusOK, gin.H{
         "message": "pong",
      })
   })
   engine.Run() // 서버 시작, 기본적으로 localhost:8080 리스닝
}

요청 URL

http
GET localhost:8080/ping

반환

http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Nov 2022 08:47:11 GMT
Content-Length: 18

{
  "message": "pong"
}

문서

사실 Gin 공식 문서에는 많은 튜토리얼이 없으며 대부분 소개와 기본 사용법 및 몇 가지 예제뿐입니다. 하지만 gin-gonic/ 조직 아래에는 gin-gonic/examples 저장소가 있으며 이는 커뮤니티에서 함께 유지 관리하는 gin 예제 저장소입니다. 모두 영어이며 업데이트 빈도가 그리 높지 않지만笔者도 여기서 천천히 gin 프레임워크를 배웠습니다.

예제 저장소 주소: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

TIP

시작하기 전에 HttpRouter 를 읽어보는 것이 좋습니다: HttpRouter

파라미터 파싱

gin 의 파라미터 파싱은 총 세 가지 방식을 지원합니다: 라우팅 파라미터, URL 파라미터, 폼 파라미터. 아래에서 하나씩 설명하고 코드 예제와 함께 살펴보겠습니다. 비교적 간단하고 이해하기 쉽습니다.

라우팅 파라미터

라우팅 파라미터는 실제로 HttpRouter 의 파라미터 파싱 기능을 캡슐화한 것으로 사용 방법은 기본적으로 HttpRouter 와 일치합니다.

go
package main

import (
   "github.com/gin-gonic/gin"
   "log"
   "net/http"
)

func main() {
   e := gin.Default()
   e.GET("/findUser/:username/:userid", FindUser)
   e.GET("/downloadFile/*filepath", UserPage)

   log.Fatalln(e.Run(":8080"))
}

// 명명된 파라미터 예제
func FindUser(c *gin.Context) {
   username := c.Param("username")
   userid := c.Param("userid")
   c.String(http.StatusOK, "username is %s\n userid is %s", username, userid)
}

// 경로 파라미터 예제
func UserPage(c *gin.Context) {
   filepath := c.Param("filepath")
   c.String(http.StatusOK, "filepath is  %s", filepath)
}

예제 1

bash
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'
username is jack
 userid is 001

예제 2

bash
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'
filepath is  /img/fruit.png

URL 파라미터

전통적인 URL 파라미터로 형식은 /url?key=val&key1=val1&key2=val2입니다.

go
package main

import (
   "github.com/gin-gonic/gin"
   "log"
   "net/http"
)

func main() {
   e := gin.Default()
   e.GET("/findUser", FindUser)
   log.Fatalln(e.Run(":8080"))
}

func FindUser(c *gin.Context) {
   username := c.DefaultQuery("username", "defaultUser")
   userid := c.Query("userid")
   c.String(http.StatusOK, "username is %s\nuserid is %s", username, userid)
}

예제 1

bash
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'
username is jack
userid is 001

예제 2

bash
curl --location --request GET '127.0.0.1:8080/findUser'
username is defaultUser
userid is

폼 파라미터

폼의 콘텐츠 타입은 일반적으로 application/json, application/x-www-form-urlencoded, application/xml, multipart/form-data가 있습니다.

go
package main

import (
  "github.com/gin-gonic/gin"
  "net/http"
)

func main() {
  e := gin.Default()
  e.POST("/register", RegisterUser)
  e.POST("/update", UpdateUser)
  e.Run(":8080")
}

func RegisterUser(c *gin.Context) {
  username := c.PostForm("username")
  password := c.PostForm("password")
  c.String(http.StatusOK, "successfully registered,your username is [%s],password is [%s]", username, password)
}

func UpdateUser(c *gin.Context) {
  var form map[string]string
  c.ShouldBind(&form)
  c.String(http.StatusOK, "successfully update,your username is [%s],password is [%s]", form["username"], form["password"])
}

예제 1: form-data 사용

bash
curl --location --request POST '127.0.0.1:8080/register' \
--form 'username="jack"' \
--form 'password="123456"'
successfully registered,your username is [jack],password is [123456]

PostForm 메서드는 기본적으로 application/x-www-form-urlencodedmultipart/form-data 타입의 폼을 파싱합니다.

예제 2: json 사용

bash
curl --location --request POST '127.0.0.1:8080/update' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"username",
    "password":"123456"
}'
successfully update,your username is [username],password is [123456]

데이터 파싱

대부분의 경우 직접 파라미터를 파싱하는 대신 구조체를 사용하여 데이터를 운반합니다. gin에서 데이터 바인딩에 사용되는 메서드는 주로 Bind()ShouldBind()이며 두 메서드의 차이점은 전자는 내부에서 직접 ShouldBind() 를 호출한다는 점입니다. 물론 err 를 반환할 때 400 응답을 직접 수행하고 후자는 그렇지 않습니다. 더 유연하게 오류를 처리하려면 후자를 사용하는 것이 좋습니다. 이 두 함수는 요청의 content-type 에 따라 자동으로 추론하여 어떤 방식으로 파싱할지 결정합니다.

go
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
    // ShouldBindWith() 호출
  if err := c.ShouldBindWith(obj, b); err != nil {
    c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // 400 badrequest 직접 응답
    return err
  }
  return nil
}

직접 선택하려면 BindWith()ShouldBindWith() 를 사용할 수 있습니다. 예를 들어

go
c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xml

gin 이 지원하는 바인딩 타입은 다음과 같습니다.

go
var (
   JSON          = jsonBinding{}
   XML           = xmlBinding{}
   Form          = formBinding{}
   Query         = queryBinding{}
   FormPost      = formPostBinding{}
   FormMultipart = formMultipartBinding{}
   ProtoBuf      = protobufBinding{}
   MsgPack       = msgpackBinding{}
   YAML          = yamlBinding{}
   Uri           = uriBinding{}
   Header        = headerBinding{}
   TOML          = tomlBinding{}
)

예제

go
package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
  "net/http"
)

type LoginUser struct {
  Username string `binding:"required" json:"username" form:"username" uri:"username"`
  Password string `binding:"required" json:"password" form:"password" uri:"password"`
}

func main() {
  e := gin.Default()
  e.POST("/loginWithJSON", Login)
  e.POST("/loginWithForm", Login)
  e.GET("/loginWithQuery/:username/:password", Login)
  e.Run(":8080")
}

func Login(c *gin.Context) {
  var login LoginUser
    // ShouldBind 를 사용하여 gin 이 자동으로 추론하도록 함
  if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
    c.String(http.StatusOK, "login successfully !")
  } else {
    c.String(http.StatusBadRequest, "login failed !")
  }
  fmt.Println(login)
}

Json 데이터 바인딩

bash
curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"root",
    "password":"root"
}'
login successfully !

폼 데이터 바인딩

bash
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'
login successfully !

URL 데이터 바인딩

bash
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'
login failed !

여기서 오류가 발생합니다. 출력된 content-type 이 빈 문자열이므로 데이터 파싱 방식을 추론할 수 없기 때문입니다. 따라서 URL 파라미터를 사용할 때는 수동으로 파싱 방식을 지정해야 합니다. 예를 들어:

go
if err := c.ShouldBindUri(&login); err == nil && login.Password != "" && login.Username != "" {
   c.String(http.StatusOK, "login successfully !")
} else {
   fmt.Println(err)
   c.String(http.StatusBadRequest, "login failed !")
}

여러 번 바인딩

일반적인 방법은 c.Request.Body 메서드를 호출하여 데이터를 바인딩하는 것이지만 이 메서드를 여러 번 호출할 수는 없습니다. 예를 들어 c.ShouldBind 는 재사용할 수 없습니다. 여러 번 바인딩하려면 c.ShouldBindBodyWith 를 사용할 수 있습니다.

go
func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // c.Request.Body 를 읽고 결과를 컨텍스트에 저장
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // 이때 컨텍스트에 저장된 body 를 재사용
  }
  if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // 다른 형식도 허용
  }
  if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `the body should be formB XML`)
  }
}

TIP

c.ShouldBindBodyWith 는 바인딩 전에 body 를 컨텍스트에 저장합니다. 이는 성능에 약간의 영향을 미치므로 한 번 호출로 바인딩을 완료할 수 있다면 이 메서드를 사용하지 않는 것이 좋습니다. JSON, XML, MsgPack, ProtoBuf 와 같은 일부 형식에만 이 기능이 필요합니다. Query, Form, FormPost, FormMultipart 와 같은 다른 형식은 성능 저하 없이 c.ShouldBind() 를 여러 번 호출할 수 있습니다.

데이터 검증

gin 내장 검증 도구는 실제로 github.com/go-playground/validator/v10이며 사용 방법도 거의 차이가 없습니다. Validator

간단한 예제

go
type LoginUser struct {
   Username string `binding:"required"  json:"username" form:"username" uri:"username"`
   Password string `binding:"required" json:"password" form:"password" uri:"password"`
}

func main() {
   e := gin.Default()
   e.POST("/register", Register)
   log.Fatalln(e.Run(":8080"))
}

func Register(ctx *gin.Context) {
   newUser := &LoginUser{}
   if err := ctx.ShouldBind(newUser); err == nil {
      ctx.String(http.StatusOK, "user%+v", *newUser)
   } else {
      ctx.String(http.StatusBadRequest, "invalid user,%v", err)
   }
}

테스트

curl --location --request POST 'http://localhost:8080/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"jack1"

}'

출력

invalid user,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tag

TIP

주의할 점은 gin 에서 validator 의 검증 tag 는 binding 이지만单独로 validator 를 사용할 때의 검증 tag 는 validator라는 점입니다.

데이터 응답

데이터 응답은 인터페이스 처리에서 마지막 단계로 수행해야 할 작업입니다. 백엔드는 모든 데이터 처리를 완료한 후 HTTP 프로토콜을 통해 호출자에게 반환하며 gin 은 데이터 응답에 대해 풍부한 내장 지원을 제공하며 사용법이 간단하고 이해하기 쉽습니다.

간단한 예제

go
func Hello(c *gin.Context) {
    // 순수 문자열 형식의 데이터 반환, http.StatusOK 은 200 상태码를 의미하며 데이터는 "Hello world !"
  c.String(http.StatusOK, "Hello world !")
}

HTML 렌더링

TIP

파일을 로드할 때 기본 루트 경로는 프로젝트 경로입니다. 즉 go.mod 파일이 있는 경로이며 아래 예제의 index.html 은 루트 경로 아래의 index.html 에 위치합니다. 하지만 일반적으로 이러한 템플릿 파일들은 루트 경로에 두지 않고 정적 리소스 폴더에 저장합니다.

go
func main() {
   e := gin.Default()
    // HTML 파일 로드, Engine.LoadHTMLGlob() 도 사용 가능
   e.LoadHTMLFiles("index.html")
   e.GET("/", Index)
   log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
   c.HTML(http.StatusOK, "index.html", gin.H{})
}

테스트

curl --location --request GET 'http://localhost:8080/'

반환

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>GinLearn</title>
  </head>

  <body>
    <h1>Hello World!</h1>
    <h1>This is a HTML Template Render Example</h1>
  </body>
</html>

빠른 응답

앞에서 자주 context.String() 메서드를 사용하여 데이터 응답을 수행했는데 이는 가장 원시적인 응답 방법으로 직접 문자열을 반환합니다. gin 에는 많은 빠른 응답 메서드가 내장되어 있습니다. 예를 들어:

go
// Render 를 사용하여 응답 헤더를 쓰고 데이터 렌더링 수행
func (c *Context) Render(code int, r render.Render)

// HTML 템플릿 렌더링, name 은 html 경로, obj 는 콘텐츠
func (c *Context) HTML(code int, name string, obj any)

// 미화된 들여쓰기가 있는 JSON 문자열로 데이터 렌더링, 일반적으로 더 많은 전송 소모를 초래하므로 이 메서드 사용 권장하지 않음
func (c *Context) IndentedJSON(code int, obj any)

// 안전한 JSON, JSON 하이재킹 방지, 자세한 내용: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)

// JSONP 방식으로 렌더링
func (c *Context) JSONP(code int, obj any)

// JSON 방식으로 렌더링
func (c *Context) JSON(code int, obj any)

// JSON 방식으로 렌더링, unicode 코드를 ASCII 코드로 변환
func (c *Context) AsciiJSON(code int, obj any)

// JSON 방식으로 렌더링, HTML 특수 문자 이스케이프 처리하지 않음
func (c *Context) PureJSON(code int, obj any)

// XML 방식으로 렌더링
func (c *Context) XML(code int, obj any)

// YML 방식으로 렌더링
func (c *Context) YAML(code int, obj any)

// TOML 방식으로 렌더링
func (c *Context) TOML(code int, interface{})

// ProtoBuf 방식으로 렌더링
func (c *Context) ProtoBuf(code int, obj any)

// String 방식으로 렌더링
func (c *Context) String(code int, format string, values ...any)

// 특정 위치로 리디렉션
func (c *Context) Redirect(code int, location string)

// data 를 응답 스트림에 쓰기
func (c *Context) Data(code int, contentType string, data []byte)

// reader 를 통해 스트림을 읽고 응답 스트림에 쓰기
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)

// 파일을 응답 스트림에 효율적으로 쓰기
func (c *Context) File(filepath string)

// fs 의 파일 스트림을 응답 스트림에 효율적으로 쓰기
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)

// fs 의 파일 스트림을 응답 스트림에 효율적으로 쓰며 클라이언트에서 지정된 파일명으로 다운로드
func (c *Context) FileAttachment(filepath, filename string)

// 서버 푸시 스트림을 응답 스트림에 쓰기
func (c *Context) SSEvent(name string, message any)

// 스트림 응답을 전송하고 클라이언트가 스트림 중간에 연결을 끊었는지 여부를 판단하는 부울 값 반환
func (c *Context) Stream(step func(w io.Writer) bool) bool

대부분의 애플리케이션에서 가장 많이 사용하는 것은 context.JSON이며 다른 것들은 상대적으로 적게 사용됩니다. 여기서는 예제를 들지 않겠습니다. 모두 비교적 간단하고 이해하기 쉬우며 직접 호출하는 것뿐입니다.

비동기 처리

gin 에서 비동기 처리는 goroutine 과 함께 사용해야 하며 사용이 매우 간단합니다.

go
// copy 는 현재 Context 의 사본을 반환하여 현재 Context 범위 밖에서 안전하게 사용할 수 있으며 goroutine 에 전달하는 데 사용할 수 있습니다
func (c *Context) Copy() *Context
go
func main() {
  e := gin.Default()
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

func Hello(c *gin.Context) {
  ctx := c.Copy()
  go func() {
    // 서브 루틴은 원래 Context 를 사용하지 않고 Context 의 사본을 사용해야 합니다
    log.Println("비동기 처리 함수: ", ctx.HandlerNames())
  }()
  log.Println("인터페이스 처리 함수: ", c.HandlerNames())
  c.String(http.StatusOK, "hello")
}

테스트

go
curl --location --request GET 'http://localhost:8080/hello'

출력

go
2022/12/21 13:33:47 비동기 처리 함수:  []
2022/12/21 13:33:47 인터페이스 처리 함수:  [github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.Hello]
[GIN] 2022/12/21 - 13:33:47 | 200 |     11.1927ms |             ::1 | GET      "/hello"

두 출력이 다르다는 것을 알 수 있으며 사본은 복사 시 안전을 위해 많은 요소의 값을 삭제했습니다.

파일 전송

파일 전송은 웹 애플리케이션에 없어서는 안 될 기능이며 gin 은 이에 대한 지원을 매우 간단하게 캡슐화했지만 본질적으로 원래의 net/http 를 사용하는 과정과 거의 비슷합니다. 프로세스는 요청 본문에서 파일 스트림을 읽은 다음 로컬에 저장하는 것입니다.

단일 파일 업로드

go
func main() {
  e := gin.Default()
  e.POST("/upload", uploadFile)
  log.Fatalln(e.Run(":8080"))
}

func uploadFile(ctx *gin.Context) {
  // 파일 가져오기
  file, err := ctx.FormFile("file")
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // 로컬에 저장
  err = ctx.SaveUploadedFile(file, "./"+file.Filename)
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // 결과 반환
  ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}

테스트

curl --location --request POST 'http://localhost:8080/upload' \
--form 'file=@"/C:/Users/user/Pictures/Camera Roll/a.jpg"'

결과

upload a.jpg size:1424 byte successfully!

TIP

일반적으로 파일 업로드의 MethodPOST 를 지정하며 일부 회사는 PUT 을 사용하는 것을 선호할 수 있습니다. 전자는 간단한 HTTP 요청이고 후자는 복잡한 HTTP 요청입니다. 구체적인 차이는 설명하지 않겠습니다. 후자를 사용하는 경우 특히 프론트엔드와 백엔드가 분리된 프로젝트에서는 해당 CORS 처리가 필요하며 Gin 의 기본 구성은 CORS 를 지원하지 않습니다 [CORS 구성](#CORS 구성).

다중 파일 업로드

go
func main() {
   e := gin.Default()
   e.POST("/upload", uploadFile)
   e.POST("/uploadFiles", uploadFiles)
   log.Fatalln(e.Run(":8080"))
}

func uploadFiles(ctx *gin.Context) {
  // gin 이 파싱한 multipart 폼 가져오기
  form, _ := ctx.MultipartForm()
  // 키 값에서 해당 파일 목록 가져오기
  files := form.File["files"]
  // 파일 목록 순회하며 로컬에 저장
  for _, file := range files {
    err := ctx.SaveUploadedFile(file, "./"+file.Filename)
    if err != nil {
      ctx.String(http.StatusBadRequest, "upload failed")
      return
    }
  }
  // 결과 반환
  ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}

테스트

curl --location --request POST 'http://localhost:8080/uploadFiles' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/a.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/123.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/girl.jpg"'

출력

upload 3 files successfully!

파일 다운로드

파일 다운로드 부분에 대해 Gin 은 기존 표준 라이브러리 API 를 다시 한번 캡슐화하여 파일 다운로드를 매우 간단하게 만들었습니다.

go
func main() {
  e := gin.Default()
  e.POST("/upload", uploadFile)
  e.POST("/uploadFiles", uploadFiles)
  e.GET("/download/:filename", download)
  log.Fatalln(e.Run(":8080"))
}

func download(ctx *gin.Context) {
    // 파일명 가져오기
  filename := ctx.Param("filename")
    // 해당 파일 반환
  ctx.FileAttachment(filename, filename)
}

테스트

curl --location --request GET 'http://localhost:8080/download/a.jpg'

결과

Content-Disposition: attachment; filename="a.jpg"
Date: Wed, 21 Dec 2022 08:04:17 GMT
Last-Modified: Wed, 21 Dec 2022 07:50:44 GMT

너무 간단하다고 생각되지 않으십니까? 프레임워크 방법을 사용하지 않고 직접 프로세스를 작성해 보겠습니다.

go
func download(ctx *gin.Context) {
   // 파라미터 가져오기
   filename := ctx.Param("filename")

   // 요청 응답 객체와 요청 객체
   response, request := ctx.Writer, ctx.Request
   // 응답 헤더 쓰기
   // response.Header().Set("Content-Type", "application/octet-stream")  이진 스트림으로 파일 전송
   response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // 파일명 안전 이스케이프 처리
   response.Header().Set("Content-Transfer-Encoding", "binary")                                            // 전송 인코딩
   http.ServeFile(response, request, filename)
}

사실 net/http 도 이미 충분히 잘 캡슐화되어 있습니다.

TIP

Engine.MaxMultipartMemory 를 통해 파일 전송 최대 메모리를 설정할 수 있으며 기본값은 32 << 20 // 32 MB입니다.

라우팅 관리

라우팅 관리 는 시스템에서 매우 중요한 부분으로 모든 요청이 올바른 함수에 매핑되도록 해야 합니다.

라우팅 그룹

라우팅 그룹을 생성하는 것은 인터페이스를 분류하는 것으로 서로 다른 카테고리의 인터페이스는 서로 다른 기능에 해당하며 관리하기도 더 쉽습니다.

go
func Hello(c *gin.Context) {

}

func Login(c *gin.Context) {

}

func Update(c *gin.Context) {

}

func Delete(c *gin.Context) {

}

위와 같은 네 개의 인터페이스가 있다고 가정하고暫時 내부 구현은 무시합니다. Hello, Login 은 한 그룹이고 Update, Delete 는 한 그룹입니다.

go
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup

그룹을 생성할 때 그룹의 루트 라우팅에 핸들러를 등록할 수도 있지만 대부분의 경우 그렇게 하지 않습니다.

go
func main() {
  e := gin.Default()
  v1 := e.Group("v1")
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("v2")
  {
    v2.POST("/update", Update)
    v2.DELETE("/delete", Delete)
  }
}

v1, v2 두 그룹으로 나누었으며 중괄호 {} 는 규범을 위한 것일 뿐이며 중괄호 내에 등록된 핸들러가 동일한 라우팅 그룹에 속함을 나타냅니다. 기능상 아무런 역할이 없습니다. 마찬가지로 gin 은 중첩 그룹도 지원하며 방법은 위 예제와 동일하므로 여기서는 시연하지 않습니다.

404 라우팅

gin 의 Engine 구조체는 접근하는 URL 이 존재하지 않을 때如何处理하는지 설정하는 NoRoute 메서드를 제공합니다. 개발자는 이 메서드에 로직을 작성하여 라우팅을 찾지 못했을 때 자동으로 호출되도록 할 수 있으며 기본적으로 404 상태 코드를 반환합니다.

go
func (engine *Engine) NoRoute(handlers ...HandlerFunc)

이전 예제를 들어보겠습니다.

go
func main() {
   e := gin.Default()
   v1 := e.Group("v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   // 핸들러 등록
   e.NoRoute(func(context *gin.Context) { // 여기는 시연일 뿐이니 프로덕션 환경에서 직접 HTML 코드를 반환하지 마십시오
      context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
   })
   log.Fatalln(e.Run(":8080"))
}

아무 요청이나 보내기

curl --location --request GET 'http://localhost:8080/'
<h1>404 Page Not Found</h1>

405 라우팅

Http 상태 코드에서 405 는 현재 요청의 메서드 타입이 허용되지 않음을 나타냅니다. gin 은 다음과 같은 메서드를 제공합니다.

go
func (engine *Engine) NoMethod(handlers ...HandlerFunc)

핸들러를 등록하여 발생 시 자동으로 호출되도록 하며 전제는 Engine.HandleMethodNotAllowed = true로 설정하는 것입니다.

go
func main() {
   e := gin.Default()
   // true 로 설정 필요
   e.HandleMethodNotAllowed = true
   v1 := e.Group("/v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   e.NoRoute(func(context *gin.Context) {
      context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
   })
   // 핸들러 등록
   e.NoMethod(func(context *gin.Context) {
      context.String(http.StatusMethodNotAllowed, "method not allowed")
   })
   log.Fatalln(e.Run(":8080"))
}

구성 완료 후 gin 의 기본 header 는 OPTION 요청을 지원하지 않습니다. 테스트해 보기

curl --location --request OPTIONS 'http://localhost:8080/v2/delete'
method not allowed

이제 구성이 완료되었습니다.

리디렉션

gin 의 리디렉션은 매우 간단하며 gin.Context.Redirect() 메서드를 호출하면 됩니다.

go
func main() {
  e := gin.Default()
  e.GET("/", Index)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
  c.Redirect(http.StatusMovedPermanently, "/hello")
}

func Hello(c *gin.Context) {
  c.String(http.StatusOK, "hello")
}

테스트

curl --location --request GET 'http://localhost:8080/'

출력

hello

미들웨어

gin 은 매우 가볍고 유연하며 확장성이 매우 높고 미들웨어에 대한 지원도 매우 우호적입니다. Gin 에서 모든 인터페이스 요청은 미들웨어를 거치며 개발자는 미들웨어를 통해 많은 기능과 로직을 사용자 정의하여 구현할 수 있습니다. gin 은 자체 기능이 매우 적지만 서드파티 커뮤니티에서 개발한 gin 확장 미들웨어가 매우 풍부합니다.

미들웨어는 본질적으로 여전히 인터페이스 핸들러입니다.

go
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

어떤 의미에서 보면 각 요청에 해당하는 핸들러도 미들웨어이지만 작용 범위가 매우 작은 로컬 미들웨어입니다.

go
func Default() *Engine {
   debugPrintWARNINGDefault()
   engine := New()
   engine.Use(Logger(), Recovery())
   return engine
}

gin 의 소스 코드를 보면 Default 함수에서 반환되는 기본 Engine 은 두 개의 기본 미들웨어 Logger(), Recovery() 를 사용합니다. 기본 미들웨어를 사용하고 싶지 않다면 gin.New() 를 대신 사용할 수 있습니다.

전역 미들웨어

전역 미들웨어는 작용 범위가 전역이며 전체 시스템의 모든 요청이 이 미들웨어를 거칩니다.

go
func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("전역 미들웨어 실행됨...")
   }
}

클로저 함수를 생성하여 미들웨어를 만든 다음 Engine.Use() 를 통해 전역 미들웨어를 등록합니다.

go
func main() {
   e := gin.Default()
   // 전역 미들웨어 등록
   e.Use(GlobalMiddleware())
   v1 := e.Group("/v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

테스트

curl --location --request GET 'http://localhost:8080/v1/hello'

출력

[GIN-debug] Listening and serving HTTP on :8080
전역 미들웨어 실행됨...
[GIN] 2022/12/21 - 11:57:52 | 200 |       538.9µs |             ::1 | GET      "/v1/hello"

로컬 미들웨어

로컬 미들웨어는 작용 범위가 로컬이며 시스템의 로컬 요청이 이 미들웨어를 거칩니다. 로컬 미들웨어는 단일 라우팅에 등록할 수 있지만 대부분의 경우 라우팅 그룹에 등록합니다.

go
func main() {
   e := gin.Default()
   // 전역 미들웨어 등록
   e.Use(GlobalMiddleware())
   // 라우팅 그룹 로컬 미들웨어 등록
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // 단일 라우팅 로컬 미들웨어 등록
      v2.POST("/update", LocalMiddleware(), Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

테스트

curl --location --request POST 'http://localhost:8080/v2/update'

출력

전역 미들웨어 실행됨...
로컬 미들웨어 실행됨
[GIN] 2022/12/21 - 12:05:03 | 200 |       999.9µs |             ::1 | POST     "/v2/update"

미들웨어 원리

Gin 미들웨어의 사용과 사용자 정의는 매우 쉽습니다. 내부 원리도 비교적 간단하며后续 학습을 위해 내부 원리를 간단히 이해해야 합니다. Gin 의 미들웨어는 실제로 책임 연쇄 패턴을 사용했으며 ContextHandlersChain 을 유지하며 본질적으로 []HandlerFunc 이고 indexint8 타입입니다. Engine.handlerHTTPRequest(c *Context) 메서드에는 호출 과정을 나타내는 코드가 있습니다. gin 이 라우팅 트리에서 해당 라우팅을 찾은 후 Next() 메서드를 호출합니다.

go
if value.handlers != nil {
   // 호출 체인을 Context 에 할당
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // 미들웨어 호출
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

Next() 호출이 핵심이며 Next() 는 라우팅의 handlers 에 있는 HandlerFunc 를 순회하며 실행합니다. 이때 index 의 역할은 미들웨어 호출 위치를 기록하는 것입니다. 해당 라우팅에 등록된 인터페이스 함수도 handlers 내에 있으며 이것이 앞서 인터페이스도 미들웨어라고 말한 이유입니다.

go
func (c *Context) Next() {
   // 들어오자마자 +1 하는 것은 재귀 무한 루프에 빠지지 않기 위함이며 기본 값은 -1
   c.index++
   for c.index < int8(len(c.handlers)) {
      // HandlerFunc 실행
      c.handlers[c.index](c)
      // 실행 완료 후 index+1
      c.index++
   }
}

Hello() 로직을 수정하여 실제로 그런지 확인해 보겠습니다.

go
func Hello(c *gin.Context) {
   fmt.Println(c.HandlerNames())
}

출력 결과는 다음과 같습니다.

[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]

미들웨어 호출 체인의 순서는 Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello이며 호출 체인의 마지막 요소가 실제로 실행될 인터페이스 함수이고 앞의 것들은 모두 미들웨어입니다.

TIP

로컬 라우팅을 등록할 때 다음과 같은 어설션이 있습니다.

go
finalSize := len(group.Handlers) + len(handlers) // 미들웨어 총 수
assert1(finalSize < int(abortIndex), "too many handlers")

여기서 abortIndex int8 = math.MaxInt8 >> 1 값은 63 입니다. 즉 시스템 사용 시 라우팅 등록 수가 63 개를 초과하지 않아야 합니다.

타이머 미들웨어

위의 미들웨어 원리를 알았으므로 간단한 요청 시간 통계 미들웨어를 작성할 수 있습니다.

go
func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // 시작 시간 기록
      start := time.Now()
      // 후속 호출 체인 실행
      context.Next()
      // 시간 간격 계산
      duration := time.Since(start)
      // 나노초 출력하여 결과 관찰
      fmt.Println("요청 소요 시간: ", duration.Nanoseconds())
   }
}

func main() {
  e := gin.Default()
  // 전역 미들웨어 등록, 타이머 미들웨어
  e.Use(GlobalMiddleware(), TimeMiddleware())
  // 라우팅 그룹 로컬 미들웨어 등록
  v1 := e.Group("/v1", LocalMiddleware())
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("/v2")
  {
    // 단일 라우팅 로컬 미들웨어 등록
    v2.POST("/update", LocalMiddleware(), Update)
    v2.DELETE("/delete", Delete)
  }
  log.Fatalln(e.Run(":8080"))
}

테스트

curl --location --request GET 'http://localhost:8080/v1/hello'

출력

요청 소요 시간:  517600

간단한 타이머 미들웨어가 이미 작성되었으며 이후에는 자신의 탐색을 통해 일부 기능을 더 실용적인 미들웨어로 작성할 수 있습니다.

서비스 구성

기본 구성만 사용하는 것은远远不够하지 않으며 대부분의 경우 많은 서비스 구성을 수정해야 요구 사항을 충족할 수 있습니다.

Http 구성

net/http 를 통해 Server 를 생성하여 구성할 수 있으며 Gin 자체도 네이티브 API 와 마찬가지로 Gin 을 사용하는 것을 지원합니다.

go
func main() {
   router := gin.Default()
   server := &http.Server{
      Addr:           ":8080",
      Handler:        router,
      ReadTimeout:    10 * time.Second,
      WriteTimeout:   10 * time.Second,
      MaxHeaderBytes: 1 << 20,
   }
   log.Fatal(server.ListenAndServe())
}

정적 리소스 구성

정적 리소스는 과거에 기본적으로 서버에서 없어서는 안 될 부분이었습니다. 현재 사용 비율이 점차 줄어들고 있지만 여전히 많은 시스템이 모놀리식 아키텍처를 사용하는 경우가 있습니다.

Gin 은 정적 리소스를 로드하기 위해 세 가지 메서드를 제공합니다.

go
// 특정 정적 폴더 로드
func (group *RouterGroup) Static(relativePath, root string) IRoutes

// 특정 fs 로드
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes

// 특정 정적 파일 로드
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes

TIP

relativePath 는 웹 URL 에 매핑되는 상대 경로이며 root 는 프로젝트 내 파일의 실제 경로입니다.

프로젝트 디렉토리가 다음과 같다고 가정합니다.

root
|
|-- static
|  |
|  |-- a.jpg
|  |
|  |-- favicon.ico
|
|-- view
  |
  |-- html
go
func main() {
   router := gin.Default()
   // 정적 파일 디렉토리 로드
   router.Static("/static", "./static")
   // 정적 파일 디렉토리 로드
   router.StaticFS("/view", http.Dir("view"))
   // 정적 파일 로드
   router.StaticFile("/favicon", "./static/favicon.ico")

   router.Run(":8080")
}

CORS 구성

Gin 자체는 CORS 구성에 대한 어떠한 처리도 하지 않으므로 직접 미들웨어를 작성하여 해당 요구 사항을 구현해야 합니다. 실제로 난이도도 크지 않으며 HTTP 프로토콜을 조금이라도熟悉한 사람이라면 일반적으로 작성할 수 있으며 로직은 기본적으로 동일합니다.

go
func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // 프로덕션 환경의 서버는 일반적으로 * 를 채우지 않으며 지정된 도메인 이름을 작성해야 합니다
         c.Header("Access-Control-Allow-Origin", origin)
         // 허용되는 HTTP METHOD
         c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
         // 허용되는 요청 헤더
         c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
         // 클라이언트가 접근할 수 있는 응답 헤더
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // 인증 정보 필요 여부 Credentials 는 cookies, authorization headers 또는 TLS client certificates 일 수 있습니다
         // true 로 설정 시 Access-Control-Allow-Origin 은 * 일 수 없습니다
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // OPTION 요청 허용但 후속 메서드 실행 안 함
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // 허용
      c.Next()
   }
}

미들웨어를 전역 미들웨어로 등록하면 됩니다.

세션 제어

현재 시대에서 인기 있는 세 가지 웹 세션 제어는 총 세 가지입니다: cookie, session, JWT.

Cookie 의 정보는 브라우저에 키 - 값 쌍 형태로 저장되며 브라우저에서 직접 데이터를 볼 수 있습니다.

장점:

  • 구조가 간단함
  • 데이터 영속성

단점:

  • 크기 제한
  • 평문 저장
  • CSRF 공격에 취약
go
import (
    "fmt"

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

func main() {

    router := gin.Default()

    router.GET("/cookie", func(c *gin.Context) {

         // 해당 cookie 가져오기
        cookie, err := c.Cookie("gin_cookie")

        if err != nil {
            cookie = "NotSet"
            // cookie 설정 파라미터: key, val, 존재 시간, 디렉토리, 도메인, 타인이 js 를 통해 cookie 에 접근 허용 여부, http 만
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

        fmt.Printf("Cookie value: %s \n", cookie)
    })

    router.Run()
}

순수한 cookie 는 5-6 년 전에 많이 사용되었지만 저자는 일반적으로 순수한 cookie 를 사용하여 세션 제어를 하지 않습니다. 이렇게 하는 것이确实 안전하지 않습니다.

Session

session 은 서버에 저장된 후 브라우저에 저장되는 cookie 를发送하며 cookie 에 저장되는 것은 session_id 입니다. 이후 매번 요청 시 서버는 session_id 를 통해 해당 session 정보를 가져올 수 있습니다.

장점:

  • 서버端에 저장되어 보안성 증가, 관리 용이

단점:

  • 서버端에 저장되어 서버 오버헤드 증가, 성능 저하
  • cookie 기반 식별, 안전하지 않음
  • 분산 상황에서 인증 정보 동기화되지 않음

Session 과 Cookie 는 분리될 수 없으며 Session 을 사용할 때마다 기본적으로 Cookie 를 사용하는 것입니다. Gin 은 기본적으로 Session 을 지원하지 않습니다. Cookie 는 Http 프로토콜의 내용이지만 Session 은 그렇지 않기 때문입니다. 하지만 서드파티 미들웨어 지원이 있으며 의존성을 설치하면 됩니다. 저장소 주소: gin-contrib/sessions: Gin middleware for session management (github.com)

go get github.com/gin-contrib/sessions

cookie, Redis, MongoDB, GORM, PostgreSQL 지원

go
func main() {
   r := gin.Default()
   // Cookie 기반 스토어 엔진 생성
   store := cookie.NewStore([]byte("secret"))
   // Session 미들웨어 설정, mysession 은 session 이름이며 cookie 의 이름이기도 함
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // session 초기화
      session := sessions.Default(c)
      var count int
      // 값 가져오기
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // 설정
      session.Set("count", count)
      // 저장
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

일반적으로 Cookie 를 통해 Session 을 저장하는 것은 권장하지 않으며 Redis 를 사용하는 것을 권장합니다. 다른 예제는 공식 저장소에서 직접 확인하시기 바랍니다.

JWT

장점:

  • JSON 기반, 다국어 공통 사용
  • 비민감 정보 저장 가능 -占用 작아 전송 용이
  • 서버端 저장 불필요, 분산 확장 용이

단점:

  • Token 갱신 문제
  • 일단 발급하면 적극적으로 제어 불가

프론트엔드 혁명 이후 프론트엔드 개발자는 더 이상 단순히 "페이지 작성하는 사람"이 아니었으며 프론트엔드와 백엔드 분리 추세가 더욱 심화되었습니다. JWT 는 프론트엔드와 백엔드 분리 및 분산 시스템에서 세션 제어를 하는 데 가장 적합하며 큰 천연 우위를 가지고 있습니다. JWT 가 이미 완전히 Gin 의 내용을 벗어났고 미들웨어 지원이 없기 때문에 JWT 는 어떤 프레임워크나 언어에도 국한되지 않기 때문에 여기서는 자세한 설명을 하지 않습니다. 다른 문서를 참조하시기 바랍니다: JWT

로그 관리

Gin 이 기본적으로 사용하는 로그 미들웨어는 os.Stdout 를 사용하며 가장 기본적인 기능만 있습니다. Gin 은 웹 서비스에만 집중하기 때문입니다. 대부분의 경우 더 성숙한 로그 프레임워크를 사용해야 하지만 이는 이 장의 논의 범위를 벗어납니다. Gin 의 확장성이 매우 높아 다른 프레임워크를 쉽게 통합할 수 있으므로 여기서는 자체 로그 서비스만 논의합니다.

콘솔 색상

go
gin.DisableConsoleColor() // 콘솔 로그 색상 끄기

개발 시를 제외하고는 대부분의 경우 이 항목을 켜는 것을 권장하지 않습니다.

로그 파일 쓰기

go
func main() {
  e := gin.Default()
    // 콘솔 색상 끄기
  gin.DisableConsoleColor()
    // 두 개의 로그 파일 생성
  log1, _ := os.Create("info1.log")
  log2, _ := os.Create("info2.log")
    // 동시에 두 개의 로그 파일에 기록
  gin.DefaultWriter = io.MultiWriter(log1, log2)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

gin 자체 로그는 여러 파일에 쓰는 것을 지원하지만 내용은 동일하여 사용하기가 그리 편리하지 않으며 요청 로그를 파일에 쓰지 않습니다.

go
func main() {
  router := gin.New()
  // LoggerWithFormatter 미들웨어는 로그를 gin.DefaultWriter 에 씁니다
  // 기본 gin.DefaultWriter = os.Stdout
  router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        //TODO 해당 파일에 쓰는 로직
        ......
    // 사용자 정의 형식 출력
    return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
        param.ClientIP,
        param.TimeStamp.Format(time.RFC1123),
        param.Method,
        param.Path,
        param.Request.Proto,
        param.StatusCode,
        param.Latency,
        param.Request.UserAgent(),
        param.ErrorMessage,
    )
  }))
  router.Use(gin.Recovery())
  router.GET("/ping", func(c *gin.Context) {
    c.String(200, "pong")
  })
  router.Run(":8080")
}

사용자 정의 미들웨어를 통해 로그를 파일에 쓸 수 있습니다.

라우팅 디버그 로그 형식

여기서 수정하는 것은 시작 시 라우팅 정보 로그 출력입니다.

go
func main() {
   e := gin.Default()
   gin.SetMode(gin.DebugMode)
   gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
      if gin.IsDebugging() {
         log.Printf("라우팅 %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
      }
   }
   e.GET("/hello", Hello)
   log.Fatalln(e.Run(":8080"))
}

출력

2022/12/21 17:19:13 라우팅 GET /hello main.Hello 3

맺음말: Gin 은 Go 언어 웹 프레임워크 중에서 가장 배우기 쉬운 것입니다. Gin 은 실제로 책임을 최소화했으며 단순히 웹 서비스만 담당할 뿐 다른 인증 로직, 데이터 캐시 등의 기능은 개발자에게自行 완료하도록 맡겼기 때문입니다. 크고 완전한 프레임워크에 비해 경량이고 간결한 Gin 은 초보자에게 더 적합하고 배워야 할 가치가 있습니다. Gin 은 어떤 규범도 강제하지 않으며 프로젝트를 어떻게 구축할지, 어떤 구조를 사용할지는自行 고려해야 하므로 초보자에게는 능력을 기르는 데 더 좋습니다.

Golang by www.golangdev.cn edit