[TIL] Go에서 Bool 다루기

Go는 초기화되지 않은 변수에 zero values라고 하는 것이 담긴다.

예를 들어, bool 타입에는 false, int라면 0이 기본적으로 저장되는 식이다.

대충 아래와 같은 코드가 있다면, 우리는 클라이언트에서 optional하게 title이나 checked가 전달될 것을 예상하고 코드를 짜야 한다.

HTTP 메서드에서 PATCH는 일부만을 변경하기 위해 사용되는 메서드이기 때문에, title만 업데이트 하고 싶은 경우나 checked만 업데이트 하고 싶은 경우도 대응해야 하는 것이다.

type Body struct {
	Title   string `json:"title"`
	Checked bool  `json:"checked"`
}

func (handler *Handler) Update(c echo.Context) error {
	ID := c.Param("id")
	id, err := strconv.Atoi(ID)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	body := &Body{}
	item := &Item{}
	if err := c.Bind(body); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}
// ...
}

여기 위의 Bodybool 값을 그대로 받고 있는데, 만약 checked 값이 주어지지 않는다면 body에 Bind되는 과정에서 (JSON이 struct 객체로 언마샬(unmarshalling) 되는 과정에서) false가 들어가게 된다.

그렇게 되면 아주 애매한 문제가 생긴다. 즉, 런타임에서 '클라이언트가 false를 보낸 것인지', 'go가 zero value로 false로 초기화한 것인지' 알 수 없게 되는 것이다. 즉, 클라이언트가 명시적으로 false를 보낸 경우와, checked를 누락해서 보낸 경우가 구분되지 않는다.

간단한 해결방법은 struct에서 bool 대신 bool의 포인터(pointer)를 이용하는 것이다.

type Body struct {
	Title   string `json:"title"`
	Checked *bool  `json:"checked"`
}

이렇게 하면 클라이언트가 {"checked": false}를 전송한 경우, body.Checked = false가 담기게 되고, 클라이언트가 checked를 누락하고, 예컨데 {"title": "new title"}만 보낸 경우, body.Checked = nil이 된다.

그렇다면, DB에 update는 어떻게 할까? GORMecho/v4를 기준으로 정리해본다.

func (handler *Handler) Update(c echo.Context) error {
	ID := c.Param("id")
	id, err := strconv.Atoi(ID)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	body := &Body{}
	item := &Item{}
	if err := c.Bind(body); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	if err = c.Validate(body); err != nil {
		return err
	}

	if len(body.Title) == 0 && body.Checked == nil {
		return echo.NewHTTPError(http.StatusBadRequest, "At least title or checked should be provided")
	}

	updateDto := make(map[string]interface{}, 2)

	if len(body.Title) != 0 {
		updateDto["Title"] = body.Title
	}
	if body.Checked != nil {
		updateDto["Checked"] = body.Checked
	}

	err = handler.repo.Model(item).Where("id = ?", uint64(id)).Updates(updateDto).Error

	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
	}

	return c.JSON(http.StatusOK, item)
}

updateDto라는 map 객체를 생성하고, titlechecked가 있는 경우들을 조건분기하며 추가해 나가면 된다.