jeonghwan-kim.github.io/dev/2019/01/18/go-encoding-json.html
godoc.org/encoding/json#pkg-index
소프트웨어는 바이트 단위로 데이터를 인식한다. 그리고 이 byte를 다른 논리적 구조로 변환하는 것을 "인코딩"이라고 한다. 여기서 ascii니 utf-8이니 하는 것들이 나오는 것이다. ascii를 예로 들자면 97이란 바이트 값을 정수로 보면 97이지만 문자로 보면 "a"다.
Go의 encoding이 이를 담당하는 기본 패키지다. 실제로는 인터페이스 타입만 정의 되어 있고 데이터 형태에 따라 서브 패키지로 기능을 제공한다. 따라서 우리가 이번 포스트에서 살펴봐야할 것은 encoding/json 패키지이다.
encoding/json 패키지는 왜 필요한가?
당연히 JSON을 쓰기 위해 필요하다. node.js 만 사용해본 코더라면 이 과정이 왜 필요한지 모를 것이다. 필자는 강타입 언어인 Dart에서 json을 사용하기 위해서는 일종의 변환이 필요하다는 것을 경험했고 이는 Go에서도 마찬가지였다.
흔히 REST API로 data를 주고 받을 때 json 형식으로 오고갈텐데 javascript에서는 아무 생각 없이 가져다 쓰면 됐지만 Go에서는 변환이 필요하다. javascript에 이런 면에서는 편하긴하다.
마셸링, 언마셸링
특정한 값을 로우 바이트로 변경하는 것을 '마셸링(Marshaling)' 이라고 한다.
바이트 슬라이스나 문자열을 논리적 자료 구조로 변경하는 것을 언마셸링(Unmashaling)이라고 한다.
실용적인 측면에서 Go에서는 marshal로 struct를 json으로 만들고 unmarshal로 json을 struct로 만든다.
// 마셜링. 어떤 값을 []byte로 반환한다.
func Marshal(v interface{}) ([]byte, error)
// 마셜링Index. 가독성을 높이고 싶다면 사용하자
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
// 언마셜링. []byte를 특정 값으로 변환한다.
func Unmarshal(data []byte, v interface{}) error
* 참고로 interface{}는 'any' 타입이다. 무엇이든 올 수 있다는 것. 패키지 내부를 살펴보다보면 interface{}가 많이 보일 것이다.
마셸링
구조체를 슬라이스에 담아 마셸링해보았다. byte로의 변환이 조금 이상하다. 그리고 왜 string으로 변환하면 아무 값도 반환되지 않는 것일까?
type person struct {
first string
last string
}
func main() {
p1 := person{first: "neil", last: "reduman"}
p2 := person{first: "pro", last: "tagonist"}
people := []person{p1, p2}
// struct를 marshalling
b1, err := json.Marshal(people)
if err != nil {
log.Fatalln(err) // kill the program
}
fmt.Println(b1) // [91 123 125 44 123 125 93] byte 변환 치고는 너무 적은데?
fmt.Println(string(b1)) // [{}, {}] 왜 아무 것도 없지?
}
제대로 변환이 되지 않은 이유는 외부에서 접근 가능한 필드를 선언해야 하기 때문이다. 필드의 앞글자를 모두 대문자로 바꿔주면 정상적으로 마셸링이 된 것을 확인할 수 있다. `` (raw string) 사용한 것에 주의하자.
type person struct {
First string
Last string
}
func main() {
p1 := person{First: "neil", Last: "reduman"}
p2 := person{First: "pro", Last: "tagonist"}
people := []person{p1, p2}
// struct를 marshalling
b1, err := json.Marshal(people)
if err != nil {
log.Fatalln(err) // kill the program
}
fmt.Println(b1) // [91 123 34 70 ... 생략. 매우 길어짐]
fmt.Println(string(b1)) // [{"First":"neil","Last":"reduman"},{"First":"pro","Last":"tagonist"}]
}
한가지 팁으로, 일반적으로 json의 key는 소문자 문자열이다. 아래처럼 태그를 달아주면 변환시 태그에서 지정한 이름으로 변경된다.
type person struct {
First string `json:"first"`
Last string `json:"last"`
}
특수한 tag로, - 를 지정하면 마셸링 시 무시된다. (lowdash _ 가 아니다 - 다)
type person struct {
First string `json:"-"`
Last string `json:"last"`
}
참고로 string은 byte의 sequence이기 때문에 딱히 json은 아니지만 다음과 같이 사용할수도 있다.
word := "simple"
// string을 marshalling
b2, err := json.Marshal(word)
if err != nil {
log.Fatalln(err) // kill the program
}
fmt.Println(b2) // [34 115 105 109 112 108 101 34]
fmt.Println(string(b2)) // "simple"
언마셸링
언마셸링한 결과물을 담을 struct 배열을 미리 선언해두고, 해당 배열의 포인터를 Unmarshal의 두번째 인자로 넘겨주도록 합시다.
type person struct {
First string `json:"first"`
Last string `json:"last"`
}
func main() {
js := `[{"first":"neil","last":"reduman"},{"first":"pro","last":"tagonist"}]`
bs := []byte(js)
fmt.Printf("%T\n", js) // string
fmt.Printf("%T\n", bs) // []uint8
people := []person{}
err := json.Unmarshal(bs, &people)
if err != nil {
log.Fatalln(err)
}
fmt.Println(people) // [{neil reduman} {pro tagonist}] struct로 들어옴
}
인코더, 디코더
Go를 json 문자열로 바꾸는 것을 인코딩(Encoding), json을 Go 밸류로 바꾸는 것을 디코딩(Decoding)이라 한다.
마셜링/언마셜링과 조응한다. 다른 점이 있다면 인코딩/디코딩은 스트림(Stream)이라는 것이다.
GoDoc에 따르면
An Encoder writes JSON values to an output stream.
A Decoder reads and decodes JSON values from an input stream.
type Encoder struct
func NewEncoder(w io.Writer) *Encoder // 인코더 생성
func (enc *Encoder) Encode(v interface{}) error // 인코더가 인코딩하는 함수
type Decoder struct
func NewDecoder(r io.Reader) *Decoder // 디코더 생성
func (dec *Decoder) Decode(v interface{}) error // 디코더가 디코딩하는 함수
io.Writer, io.Reader
여기서 인코딩에서는 인자로 io.Wirter, 디코딩에서는 io.Reader가 존재하는 것을 볼 수 있다.
Go 언어는 io.Reader, io.Writer 인터페이스를 활용하여 다양한 방법으로 입출력을 할 수 있습니다.
기본 입출력 인터페이스
- io.Reader
- io.Writer
자주 쓰는 구현체는 이런 것들이 있습니다.
- bytes.Buffer: 이게 바이트 버퍼 기본입니다.
- strings.Builder: 전 이 녀석을 아주 좋아해요. bytes.Buffer 상위 호환입니다.
- os.File: 표준 입출력을 비롯한 파일들을 열어볼 때 씁니다.
Write writes len(p) bytes from p to the underlying data stream.
interface이므로 Write 메서드를 가진 struct는 모두 Writer 타입이라고 할 수 있다.
json.NewEncoder 함수로 인코더를 생성하고 json.Encode 함수로 Go 밸류를 JSON으로 변환한다.
type Writer interface {
Write(p []byte) (n int, err error)
}
Read reads up to len(p) bytes into p.
interface이므로 Rad 메서드를 가진 struct는 모두 Writer 타입이라고 할 수 있다.
json.NewDecoder 함수로 디코더를 만들고 json.Decode 메소드로 JSON 문자열을 Go 밸류로 변경한다.
type Reader interface {
Read(p []byte) (n int, err error)
}
너무 이론적인 면만 살펴봤는데 코드를 살펴보자
Encoding
package main
import (
"encoding/json"
"os"
)
type person struct {
Name string `json:"name"`
}
func main() {
me := person{"darren"}
// writer를 받으니까 표준 출력 os.Stdout을 전달.
// 이제 이 인코더는 앞으로 입력할 데이터를 표준 출력으로 연결하는 스트림을 갖는다
enc := json.NewEncoder(os.Stdout)
enc.Encode(me) // {"name":"darren"}
}
NewEncoder로 인코더를 만든 후
func main() {
me := person{"darren"}
f, _ := os.Create("out.txt")
// 이제 이 인코더는 앞으로 입력할 데이터를 파일에 인코딩된 텍스트 기록된다
enc := json.NewEncoder(f)
enc.Encode(me)
}
Decoding
type person struct {
Name string `json:"name"`
}
func main() {
var me person
dec := json.NewDecoder(os.Stdin) // {"name":"martin"} 콘솔창에서 입력
dec.Decode(&me)
fmt.Printf("%+v\n", me) // {Name:martin} 출력 // %+v adds field names
}
Marshal/Unmarshal vs Encoder/Decoder 뭘 써야 함?
바이트 슬라이스나 문자열을 사용하려면 Marshal/Unmarshal 함수가 적합하다. 만약 표준 입출력이나 파일 같은 Reader/Writer 인터페이스를 사용하여 스트림 기반으로 동작하려면 Encoder/Decoder를 사용한다.
처리 속도는 스트림 방식이 더 낫다. 데이터 크기가 작다면 성능차이를 체감할 수 없지만 비교적 큰 데이터를 다룬다면 스트림 기반의 Encoder/Decoder가 거의 50% 정도 더 빠른 성능을 낸다
(출처: Go 언어를 활용한 마이크로서비스 개발 - 에이콘)
'Programming Language > 🐿️ Go (Golang)' 카테고리의 다른 글
Go 패키지 & Go 모듈 (0) | 2021.05.13 |
---|---|
문자, 문자열 특집 : rune 타입과 string 관련 trick (0) | 2021.05.11 |
Go Routine (2) : 내부 쓰레드 통신 Channels (0) | 2020.12.11 |
Go Routine (1) : concurrency를 위한 고루틴의 원리 및 간단한 활용 + 멀티 코어 (0) | 2020.12.11 |
Go stdlib (2) : sort (0) | 2020.12.08 |