가변 인자를 보통 넣어서 사용시 ( ... )로 사용되는데 이를 Variadic 이라고 한다. 

예를 보자. 

func sum(numbers ...int){
    sum := 0
    for _, num := range numbers {
        sum += num
    }
    return sum
}
sum(1,2,3) //6
sum([]int{1,2,3}...) //6
sum(1) //1

Sum 함수는 int 인자값에 대한 가변적으로 가능하다. 

sum([]int{1,2,3}...) //6

의 경우 뒤에 ... 을 붙여서 할수 있는데 더 예를 보자면

func f(ids []int){
//
}
func service(id int){
    f([]int{id})
}
func service2(id []int){
    f(id)
}

위를 Variadic 을 적용해보면

func f(ids ...int){
//
}
func service(id int){
    f(id)
}
func service2(id []int){
    f(id...)
}

이렇게 타입도 가변적으로 바꿔서 넣을 수 있다. 

Boltdb 연동

공식 Github

빠르고 효율적인 저레벨 DB 중 하나인 bolt db 이다. key, value 로 되어있고 byte로 값을 넣을 수 있다는 장점이 있다.

우선 설치는

$ go get github.com/boltdb/bolt/...
// DB 여는 작업
package main

import (
	"log"

	"github.com/boltdb/bolt"
)

func main() {
	// Open the my.db data file in your current directory.
	// It will be created if it doesn't exist.
	db, err := bolt.Open("my.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	...
}

다른 프로세스가 여기 DB를 열때 락이 걸려있어서 못 연다 그러면 기존 프로세스가 DB를 닫을 때까지 무한대기를 할수 밖에 없는 상황이 발생한다. 이럴때 시간이 지나면 자동으로 대기를 풀어버릴수 있다.

db, err := bolt.Open("my.db", 0600, &bolt.Options{Timeout: 1 * time.Second})

기본적으로 3가지 함수를 가진다.

  • 읽기-쓰기
  • err := db.Update(func(tx *bolt.Tx) error {
    	...
    	return nil
    })
    
  • 읽기전용
  • err := db.View(func(tx *bolt.Tx) error {
    	...
    	return nil
    })
    
  • 배치(다중 업데이트)
  • err := db.Batch(func(tx *bolt.Tx) error {
    	...
    	return nil
    })
    

버킷을 만드는 소스

CreateBucketIfNotExists은 버킷이 없는 경우 생성하는 함수이다.

package main

import (
	"fmt"
	"github.com/boltdb/bolt"
	"log"
)

func main() {
	fmt.Println("test")
	db, err := bolt.Open("my.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}

	db.Update(func(tx *bolt.Tx) error {
		_, err := tx.CreateBucketIfNotExists([]byte("MyBucket"))
		if err != nil {
			return fmt.Errorf("Create bucker: %s", err)
		}
		fmt.Println("update done")
		return nil
	})

	defer db.Close()
}

그럼 값을 넣어보고 조회를 해보자.

주의 할 내용은 모든 key / value 은 byte로 넣어야 한다.

package main

import (
	"fmt"
	"github.com/boltdb/bolt"
	"log"
)

func main() {
	fmt.Println("test")
	db, err := bolt.Open("my.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}

	db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("MyBucket"))
		err := b.Put([]byte("answer"), []byte("42"))
		return err;
	})

	db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("MyBucket"))
		v := b.Get([]byte("answer"))
		fmt.Printf("The answer is : %s\n", v)
		return nil
	})

	defer db.Close()
}

////////////
test
The answer is : 42

만약 키값을 시퀸스로 계속 올리고 싶다면??

NextSequence() 을 사용하면 가능하다.

// CreateUser saves u to the store. The new user ID is set on u once the data is persisted.
func (s *Store) CreateUser(u *User) error {
    return s.db.Update(func(tx *bolt.Tx) error {
        // Retrieve the users bucket.
        // This should be created when the DB is first opened.
        b := tx.Bucket([]byte("users"))

        // Generate ID for the user.
        // This returns an error only if the Tx is closed or not writeable.
        // That can't happen in an Update() call so I ignore the error check.
        id, _ := b.NextSequence()
        u.ID = int(id)

        // Marshal user data into bytes.
        buf, err := json.Marshal(u)
        if err != nil {
            return err
        }

        // Persist bytes to users bucket.
        return b.Put(itob(u.ID), buf)
    })
}

// itob returns an 8-byte big endian representation of v.
func itob(v int) []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(v))
    return b
}

type User struct {
    ID int
    ...
}

그 외에도 여러가지 옵션들이 있다. 공식 레포 에서 확인가능하다.

Golang 으로 CRUD Restfual Api 만들기

최근 블록체인을 공부하면서 이더리움 코어를 보고 싶다는 생각이 자주 들었다. 그리고 하이퍼레저에서 스마트 계약 개발시 Go 로 짜고 있는 걸 보고 고랭을 배워야 겠다는 마음을 먹고 하나씩 보고 있습니다.

언어를 제일 배우고 제일 먼저 해보는 건 무엇보다 게시판 하나 짜보는 거겠죠?

그래서 Restful Api 를 먼저 구성해보고 화면단을 만들어서 해보도록 합니다.

어설픈 TDD 방식으로 하나씩 짜보도록 하겠습니다.

우선 Go 가 아직 설치가 안되신 분은 https://golang.org/dl/ 으로 가셔서 받으시길 바랍니다.

그리고 환경 설정을 해줍니다.

혹시 Gopath 와 Gopath bin 설정이 안되신 분은 여기 에서 따라해보시면 됩니다.

현재 DB는 몽고디비를 사용중이며 도커로 연동중입니다.

그럼 우선 처음 시작은 main.go 로 구성해보죠.

# main.go

package main

import "fmt"

func main() {
    fmt.Println("Hello Go")
}

>>> Hello Go
> go run main.go //실행

그리고 App 파일을 만들어서 서버 관리를 위임하도록 합니다.

main.go 에서 App 인스턴스를 생성해서 서버쪽을 실행하고 DB도 연결하도록 합니다.

# app.go

package main

type App struct {
}

func (a *App) Initialize(serverIp, dbName, collectionName string) {

}
func (app App) Run(port string) {

}

그럼 main 에서는 어떻게 호출 하면 될까요?

package main

func main() {
    app := App{}
    app.Initialize(
        "192.168.99.100",
        "godb",
        "movies",
    )

    app.Run("8000")
}
  • App 인스턴스를 만들고
  • 초기화를 하고
  • App Run 함수를 통해서 실행을 합니다.

일단 제일 기본 뼈대는 진행된 것 같네요. 그럼 TDD로 먼저 테스팅을 작성해봅니다.

참고로 저도 고랭 처음이기도 해서 아주 어설프게 진행하니 양해부탁드립니다. ^^

그럼 main_test.go 를 하나 생성해줍니다. 그리고 샘플 예제를 하나 넣어서 테스트를 해봅니다.

혹시 유닛 테스팅 방법이 궁금하시면 여기 로 가셔서 참고하시길 바랍니다.

package main_test

import (
    "fmt"
    "github.com/restapi"
    "os"
    "testing"
)

var a main.App

var ip, dbName, collectionName = "192.168.99.100", "godb", "movies"

func TestMain(m *testing.M) {
    a = main.App{}
    a.Initialize(
        ip,
        dbName,
        collectionName,
    )

    prevTesting()

    code := m.Run()

    postTesting()

    os.Exit(code)
}

func prevTesting() {
    fmt.Println("prevTesting....")
}

func postTesting() {
    fmt.Println("postTesting....")
}

func TestExam(t *testing.T) {

    fmt.Println("TestExam inside")
}
prevTesting....
=== RUN   TestExam
TestExam inside
--- PASS: TestExam (0.00s)
PASS
postTesting....

내용은 보시면 아무것도 없습니다.

단지 테스팅을 하는데 TestExam을 실행시 항상 TestMain에서 전처리 후처리를 거쳐서 진행한다는 게 중요합니다.

자바로 생각하면 before와 after로 보시면 되겠네요. 보통 DB를 열어서 더미 데이트들을 넣어주고 마지막엔 지워주는 역할을 해도 되거나 DB연결을 도와줘도 됩니다.

일단 저는 다음에 쓰도록 하고 먼저 CRUD 서비스를 개발을 해보겠습니다.

이 예제는 크게 5가지 기능을 가집니다.

  • AddMovie
  • GetMovie
  • UpdateMovie
  • DeleteMovie
  • GetMovies

그럼 테스팅 클래스에 먼저 TestAddMovie함수를 정의 해봅시다. 영화를 추가하는 기능을 테스팅합니다.

func TestAddMovie(t *testing.T) {
    //TODO 영화 더미 데이터 하나를 가져온다.
    //TODO String으로 Json을 이용해서 변환해준다.
    //TODO Body 에 넣어서 Post 로 보내본다.
    //TODO 그리고 결과가 정상적으로 StatusCreated 코드가 떨어지는 지 본다.
    //TODO 입력 값을 JSON 으로 출력해서 정상적으로 입력되었는지 하나씩 체크
}

그럼 실제 코드로 옮겨보면 다음과 같이 만들수 있겠네요.

func TestAddMovie(t *testing.T) {

    //TODO 영화 더미 데이터 하나를 가져온다.
    darkNight := getMovieDummy()

    //TODO String으로 Json을 이용해서 변환해준다.
    darkNightMarshel, err := json.Marshal(darkNight)

    if err != nil {
        panic(err)
    }

    //TODO Body 에 넣어서 Post 로 보내본다.
    req, _ := http.NewRequest("POST", "/movie", bytes.NewBufferString(string(darkNightMarshel)))
    response := executeRequest(req)

    //TODO 그리고 결과가 정상적으로 StatusCreated 코드가 떨어지는 지 본다.
    checkResponseCode(t, http.StatusCreated, response.Code)

    //TODO 입력 값을 JSON 으로 출력해서 정상적으로 입력되었는지 하나씩 체크
    var m main.Movie
    json.Unmarshal(response.Body.Bytes(), &m)

    if darkNight.Name != m.Name {
        t.Errorf("Its not name equal %v", m.Name)
    }

    if darkNight.Year != m.Year {
        t.Errorf("Its not Year equal %v", m.Year)
    }

    if !cmp.Equal(darkNight.Directors, m.Directors) {
        t.Errorf("Its not Directors equal %v", m.Directors)
    }

    if !cmp.Equal(darkNight.Writers, m.Writers) {
        t.Errorf("Its not Writers equal %v", m.Writers)
    }

    if !cmp.Equal(darkNight.BoxOffice, m.BoxOffice) {
        t.Errorf("Its not BoxOffice equal %v", m.BoxOffice)
    }
}

그리고 더미 데이터는 임의로 만들어서 리턴을 해줍니다.

func getMovieDummy() *main.Movie {
    return &main.Movie{
        Name:      "The Dark Knight",
        Year:      "2008",
        Directors: []string{"Christopher Nolan"},
        Writers:   []string{"Jonathan Nolan", "Christopher Nolan"},
        BoxOffice: main.BoxOffice{
            Budget: 185000000,
            Gross:  533316061,
        },
    }
}

이 데이터 방식은 몽고 디비 컬렉션에 들어갈 내용입니다.

그러면 DB 연동 하고 실제 AddMovie함수를 만들어봐야 겠군요.

app.go 에 DB 연동 부분을 추가를 해줍니다.

type App struct {
    Router     *mux.Router
    session    *mgo.Session
    collection *mgo.Collection
}

func (a *App) Initialize(serverIp, dbName, collectionName string) {
    session, err := mgo.Dial(serverIp)
    a.session = session
    a.collection = a.session.DB(dbName).C(collectionName)

    if err != nil {
        panic(err)
    }
    //defer session.Close()

    a.Router = mux.NewRouter()
    a.initializeRoutes()
}

하단에 라우팅을 추가하는 함수가 있네요.

func (a *App) initializeRoutes() {
    //영화 추가
    a.Router.HandleFunc("/movie", a.addMovie).Methods("POST")
}

그리고 지금 당장은 안쓰지만 후에 서버를 실행할려면 다음과 같이 하단에 넣어주셔야 합니다.

func (a *App) Run(port string) {
    log.Fatal(http.ListenAndServe(":"+port, a.Router))
}

현재 mux 와 몽고 디비 라이버리를 가져와서 사용을 하고 있습니다. 명령어를 통해서 소스를 받아와서 연동을 해주도록 합니다.

go get github.com/gorilla/mux
go get gopkg.in/mgo.v2

그럼 영화 추가 로직을 넣기 위해선 model.go 을 만들어서 실제 DB핸들링 하는 건 다시 빼서 위임을 시켜보도록 하죠.

# model.go

type Movie struct {
    ID        bson.ObjectId `json:"id" bson:"_id,omitempty"`
    Name      string        `json:"name" bson:"name"`
    Year      string        `json:"year" bson:"year"`
    Directors []string      `json:"directors" bson:"directors"`
    Writers   []string      `json:"writers" bson:"writers"`
    BoxOffice BoxOffice     `json:"boxOffice" bson:"boxOffice"`
}

type BoxOffice struct {
    Budget uint64 `json:"budget" bson:"budget"`
    Gross  uint64 `json:"gross" bson:"gross"`
}

우선 Movie와 BoxOffice를 정의를 합니다.

# model.go

//영화 정보를 입력함
func (m *Movie) AddMovie(db *App) (string, error) {

    m.ID = bson.NewObjectId()
    var name = db.collection.Name
    fmt.Println(name)
    err := db.collection.Insert(m)
    if err != nil {
        return "", err
    }

    return m.ID.Hex(), nil
}

아이디를 생성해서 DB에 입력하는 로직입니다.

그리고 추후 입력된 내용에 대해 ID 를 리턴해서 다시 핸들링 (업데이트 및 삭제) 하게끔 할수 있습니다.

그럼 이 모델 로직을 가져와서 사용하는 곳을 만들어봐야겠네요.

실제 라우팅되서 처리되고 모델에 있는 로직함수가 실행된다고 보면 됩니다.

Test -> main - app - model

# app.go

func (a *App) addMovie(w http.ResponseWriter, r *http.Request) {
    var movie Movie
    postBody, err := ioutil.ReadAll(r.Body)
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "parsing error")
        return
    }
    json.Unmarshal(postBody, &movie)

    defer r.Body.Close()

    if _, err := movie.AddMovie(a); err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }

    respondWithJSON(w, http.StatusCreated, movie)
}

응답에 대한 오류와 성공 처리는 respond를 통해서 하고 있습니다.

# app.go

func respondWithError(w http.ResponseWriter, code int, message string) {
    respondWithJSON(w, code, map[string]string{"error": message})
}

func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
    response, _ := json.Marshal(payload)

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    w.Write(response)
}

이제 AddMovie 쪽이 완성이 되었네요. 그럼 영화정보를 가져와야겠네요.

우선 테스팅 코드에 입력을 한다음

# main_test.go

func TestGetMovie(t *testing.T) {
    //id := bson.NewObjectId()
    id := addDommyMovie(getMovieDummy())
    req, _ := http.NewRequest("GET", "/movie/"+id, nil)
    response := executeRequest(req)
    checkResponseCode(t, http.StatusOK, response.Code)

    var m main.Movie
    json.Unmarshal(response.Body.Bytes(), &m)
}

라우팅 처리하는 곳 가서 GetMovie함수를 추가해줍니다.

func (a *App) getMovie(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)

    movie := Movie{ID: bson.ObjectIdHex(vars["id"])}
    if err := movie.GetMovie(a); err != nil {
        switch err {
        case mgo.ErrNotFound:
            respondWithError(w, http.StatusNotFound, err.Error())
        default:
            respondWithError(w, http.StatusInternalServerError, err.Error())
        }

        return
    }

    respondWithJSON(w, http.StatusOK, movie)
}

로직을 담당하는 model.go에서 GetMovie를 추가해줍니다.

//영화를 가져오는 함수
func (m *Movie) GetMovie(db *App) error {

    //var movie Movie
    err := db.collection.Find(bson.M{"_id": m.ID}).One(&m)
    if err != nil {
        return err
    }

    return nil
}

네. 사실 한줄 씩 테스팅 코드를 짜는 게 맞지만 대략적인 흐름만 알려드렸네요.

그럼 업데이트, 삭제, 전체 리스트 가져오기도 이런식으로 추가해서 진행할 수 있습니다.

그럼 다음에는 이 기능들을 이용해서 프론트단(React) 개발 및 연동을 해보겠습니다.

고랭 가즈아~~

+ Recent posts