회사 프로젝트에서 2가지 방법으로 http response를 읽고 decode 하고 있다는 걸 발견했다. 둘 다 제대로 서비스가 동작하고 있는데 내부 동작이 어떻게 다른지 차이점이 궁금해서 공부해보았다.
json.Decoder, Decode, Unmarshal 그리고 io.ReadAll 을 중심으로 살펴볼 예정이다.
코드 비교
우선 코드부터 살펴보자.
두 코드 모두 client.Do()를 이용해 response(*http.Response 타입)를 가져온다.
차이점은 response body(io.ReadCloser 타입)를 읽고 원하는 go 변수에 값을 넣어주는 방법이다.
if resp, err := client.Do(req); err != nil {
return err
} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
return err
} else if err = json.Unmarshal(body, &v); err != nil {
return err
}
1. ioutil.ReadAll() 함수를 이용해 response body를 읽는다. 이 때, 리턴되는 body 변수는 []byte 타입이다.
2. body를 담기 원하는 go 변수인 v의 주소값에 바이트 배열 body를 json.Unmarshal() 함수를 통해 파싱해준다.
if resp, err := client.Do(req); err != nil {
return err
} else if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
return err
}
1. json.NewDecoder() 함수를 통해 response body를 읽는다. 이때 리턴값은 json 패키지의 Decoder 타입이다.
2. json.Decoder{} 객체의 메소드인 Decode()를 통해 v에 값을 넣어준다.
Marshal, Unmarshal & Encode, Decode
코드를 자세히 살펴보기에 앞서 Marshal, Unmarshal & Encode, Decode 개념을 간단히 짚고 넘어가겠다.
Encoding: 사람이 인지할 수 있는 문자(언어)를 약속된 규칙에 따라 컴퓨터가 이해하는 언어(0, 1 binary format)로 이루어진 코드로 바꾸는 것을 통틀어 일컫음. 즉, 정해진 규칙에 따라 코드화, 암호화, 부호화하는 것을 말함. 이렇게 인코딩하는 이유는 정보의 형태 표준화, 보안, 저장 공간 및 전송 시간 절약 등을 위함.
Decoding: 인코딩과 반대되는 개념. 복호화, 역코드화의 의미. 컴퓨터가 이해하는 언어를 사람이 이해할 수 있도록 바꿔주는 것. 즉, 바이트형식을 문자(문자열) 등으로 변환.
Marshal: 메모리에 지정된 객체를 다른 시스템에 전송하기 적합한 데이터 형식으로 변환하는 과정
Unmarshal: 전송받은 데이터를 시스템이 쉽게 처리할 수 있는 객체로 변환하는 과정
Marshal, Unmarshal 과정은 정보의 상호운용성이 목적이다.따라서 널리 사용하며, 사용하기 쉽고, 범용적인 포맷을 사용해야 한다. 특히 인터넷 어플리케이션이라면 오픈 표준 포맷을 사용해야 한다. 가장 널리 사용하는 포맷이 XML과 JSON이고, 요즘에는 특히 JSON을 널리 사용한다.
그렇다면 Golang에서는 어떻게 동작할까?
- Marshal, Encoder는 value type을 []byte로 변환
- 인코딩 값을 인메모리 바이트 슬라이스로 반환하는 json.Marshal
- 값을 io.Writer로 인코딩하는 스트림 기반 json.Encoder
- Unmarshal, Decoder는 []byte를 value type으로 변환
- 바이트 슬라이스로부터 디코딩할 수 있는 json.Unmarshal
- io.Reader로부터 디코딩할 수 있는 스트림 기반 json.Decoder
// 마셜링. Go value를 []byte로 반환한다.
func Marshal(v interface{}) ([]byte, error)
// 언마셜링. []byte를 Go value로 변환한다.
func Unmarshal(data []byte, v interface{}) error
// 인코딩. Go value를 json 문자열(JSON encoding of v + 개행문자)로 변환한다.
type Encoder struct
func NewEncoder(w io.Writer) *Encoder // 인코더 생성
func (enc *Encoder) Encode(v interface{}) error // 인코더가 인코딩하는 함수
// 디코딩. json 문자열(JSON-encoded value)을 Go value로 변환한다.
type Decoder struct
func NewDecoder(r io.Reader) *Decoder // 디코더 생성
func (dec *Decoder) Decode(v interface{}) error // 디코더가 디코딩하는 함수
여기서 Encoder, Decoder & Marshal, Unmarshal 의 차이는 무엇일까?
표준 입출력이나 파일 같은 Reader/Writer 인터페이스를 사용하여 스트림 기반으로 동작하려면 Encoder, Decoder를 이용한다. (공식 문서에도 스트림을 통해 읽고 쓴다고 명시되어 있다.)
또한 처리 속도에서 거의 50% 정도 더 빠른 성능을 내기 때문에 데이터량이 많다면 Encoder, Decoder 사용이 권장된다. (출처: Go 언어를 활용한 마이크로 서비스 개발 - 에이콘)
그 외 바이트 슬라이스나 문자열을 사용할 때, 데이터 크기가 작다면 성능차이를 체감할 수 없기 때문에 Marshal, Unmarshal이 사용된다.
ioutil.ReadAll()
먼저 ioutil 패키지의 ReadAll() 함수를 살펴보자.
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
//
// As of Go 1.16, this function simply calls io.ReadAll.
func ReadAll(r io.Reader) ([]byte, error) {
return io.ReadAll(r)
}
함수 내부에서 io 패키지의 ReadAll을 호출하고만 있다. 그 이유는 Go 1.16의 릴리즈 노트를 읽어보면 알 수 있는데, io/util 패키지를 deprecated 시켰기 때문에 io 패키지의 사용을 권장하고 있다.
The io/ioutil package has turned out to be a poorly defined and hard to understand collection of things. All functionality provided by the package has been moved to other packages. The io/ioutil package remains and will continue to work as before, but we encourage new code to use the new definitions in the io and os packages.
: io/ioutil 패키지는 제대로 정의가 되지 않았으며, 일반적으로 이해하기 어려운 것으로 보인다. 해당 패키지에서 제공하던 모든 기능을 다른 패키지로 이동한다. 물론 호환성을 위해 io/ioutil 패키지는 그대로 남아는 있고, 이전과 같이 동작은 한다. 그래도 앞으로 코드를 짜게 된다면 io 패키지와 os 패키지에 새롭게 정의된 메서드를 활용하는 것을 추천하였다.
io 패키지에 정의된 ReadAll 함수의 코드는 다음과 같다.
ioutil.ReadAll()과 io.ReadAll()의 로직은 같고, ioutil.ReadAll()을 사용하면 괜히 function call, return 스택만 낭비하는 꼴이기 때문에 io.ReadAll()을 사용하는게 좋을 것 같다. -> 레거시 코드 리팩토링 완료
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error) {
b := make([]byte, 0, 512)
for {
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == EOF {
err = nil
}
return b, err
}
}
}
(golang slice의 length와 capacity 속성에 대해서는 곧 포스팅할 예정이다.)
해당 함수는 단순하게 읽고자하는 파일의 EOF(End Of File)를 만날 때 까지 golang의 바이트 배열에 값을 넣어주고 있다.
스트림에서 바이트를 읽기 위한 기본 구조체는 Reader 인터페이스다. (Reader와 Read 메소드에 대해서는 io 패키지 문서 참고)
http 패키지의 response.Body의 타입인 ReadCloser 인터페이스는 Reader 인터페이스(+Closer 인터페이스)를 가지고 있다.
따라서 Reader로서 변수 r로 들어갈 수 있고, Read 메소드를 호출할 수 있는 것이다.
// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
Reader
Closer
}
Reader는 기본 Read 메서드를 감싸는 인터페이스다.
type Reader interface {
Read(p []byte) (n int, err error)
}
Reader에 대해 더 자세히 알아보기 위해 Reader 함수의 주석 부분을 번역했다. 회색 부분이 번역한 부분이다.
Reader 인터페이스의 한 가지 문제점은 애매한 규칙들을 가지고 있는 것이다.
Read는 최대 len(p)바이트를 p로 읽어들인다. 이는 읽은 바이트 수 (0 <= n <= len(p))와 발생한 모든 오류를 반환한다. Read가 n < len(p)를 반환하더라도 호출 중에 p를 모두 스크래치 공간(?!)으로 사용할 수 있다. 일부 데이터는 사용 가능하지만 len(p)바이트가 아닌 경우, Read는 보통 더 기다리지 않고 사용 가능한 데이터를 반환한다.
-> 첫째, 버퍼가 채워질거라는 보장이 없다. 만약 여러분이 8바이트 슬라이스를 전달한다면 여러분은 0부터 8바이트 사이의 그 어떤 값으로든 돌려 받게 된다. 부분 읽기를 다루는 것은 지저분하고 에러가 발생하기 쉽다. 다행히도 이 문제를 해결하기 위한 헬퍼 함수가 있는데, 바로 ReadFull() 함수이다.
Read가 오류나 파일 끝(EOF) 조건을 만나면, 성공적으로 n > 0바이트를 읽은 후, 읽은 바이트 수를 반환한다. 이는 동일한 호출에서 (nil이 아닌) 오류를 반환하거나 후속 호출에서 오류 (이 때, n == 0)를 반환할 수 있다.
입력 스트림 끝에서 0바이트를 반환하는 Reader의 인스턴스는 err == EOF 또는 err == nil 중 하나를 반환할 수 있다. 다음 Read는 무조건 0 혹은 EOF를 반환해야 한다. 그렇기 때문에 호출자는 오류 err를 고려하기 전에 항상 반환된 n > 0바이트를 처리해야 한다. 이렇게 하면 일부 바이트를 읽은 후 발생하는 I/O 오류 및 허용된 EOF 동작(allowed EOF behaviors), 양쪽 모두를 올바르게 처리할 수 있다.
Read의 구현은 len(p) == 0일 때를 제외하고 nil 오류와 0바이트를 반환하는 것이 권장되지 않는다. 호출자는 0과 nil을 반환하는 것은 아무 일도 일어나지 않았음을 나타내는 것으로 처리해야 한다. 특히 이는 EOF를 나타내지 않는다.
-> 둘째, 이는 스트림이 완료되면 io.EOF 에러를 정상적인 동작을 하는 것처럼 반환한다. 이는 초보자에게는 혼란을 가져올 수 있다.
구현은 p를 보존해서는 안된다.
-> Reader는 동일한 바이트를 재사용할 수 있도록 버퍼(p)를 Read() 메서드에 전달함으로써 동작한다. 만약 Read()가 바이트 슬라이스를 하나의 인자로 받는 대신 이를 반환하게 되면 Reader는 Read()를 호출할 때마다 새로운 바이트 슬라이스를 할당해야 할 것이다. 이는 가비지 컬렉터에 안좋은 영향을 끼친다.
json.Unmarshal()
ioutil.ReadAll()을 통해 스트림에서 바이트 슬라이스를 읽어 왔다면 해당 바이트 배열을 go 변수에 파싱을 해줘야 한다.
이 때 프로젝트에서는 json 패키지의 Unmarshal 함수를 사용했다.
Golang의 json 패키지의 unmarshal 함수 코드는 다음과 같다.
func Unmarshal(data []byte, v interface{}) error {
// Check for well-formedness.
// Avoids filling out half a data structure
// before discovering a JSON syntax error.
var d decodeState
err := checkValid(data, &d.scan)
if err != nil {
return err
}
d.init(data)
return d.unmarshal(v)
}
큰 맥락을 먼저 짚어보자.
Unmarshal은 JSON으로 인코딩된 데이터를 파싱(parsing, 구문 분석)하고, v가 nil이거나 포인터가 아닌 경우, Unmarshal은 InvalidUnmarshalError를 반환한다.
굳이 궁금해져서 자세한 함수들을 파고 들어봤는데 정리하고 보니 투 머치한 느낌이라 더보기란 안으로 정리한다.
1. checkValid()
func checkValid(data []byte, scan *scanner) error {
scan.reset()
for _, c := range data {
scan.bytes++
if scan.step(scan, c) == scanError {
return scan.err
}
}
if scan.eof() == scanError {
return scan.err
}
return nil
}
checkValid는 JSON 문법 에러를 발견하기 전에 데이터 구조의 절반을 채우는걸 방지한다. 따라서 데이터가 유효한 JSON 인코딩 데이터인지 확인하고, 할당(allocation)을 피하기 위해 대신, checkValid에서 사용할 수 있는 scan을 전달한다. (전달 받은 변수의 메모리 공간에 데이터를 넣어주기 위해, 별도의 메모리 할당을 피한다는 의미로 이해했다)
type decodeState
type decodeState struct {
data []byte
off int // next read offset in data
opcode int // last read result
scan scanner
errorContext struct { // provides context for type errors
Struct reflect.Type
FieldStack []string
}
savedError error
useNumber bool
disallowUnknownFields bool
}
decodeState는 JSON 값을 디코딩하는 동안의 상태를 나타낸다.
type scanner
type scanner struct {
// The step is a func to be called to execute the next transition.
// Also tried using an integer constant and a single func
// with a switch, but using the func directly was 10% faster
// on a 64-bit Mac Mini, and it's nicer to read.
step func(*scanner, byte) int
// Reached end of top-level value.
endTop bool
// Stack of what we're in the middle of - array values, object keys, object values.
parseState []int
// Error that happened, if any.
err error
// total bytes consumed, updated by decoder.Decode (and deliberately
// not set to zero by scan.reset)
bytes int64
}
scanner는 JSON 스캐닝 상태 머신이다. (scanning state machine)
호출자는 scan.reset() 함수를 호출한 다음, 각 바이트에 대해 scan.step(&scan, c)을 호출하여 한 번에 하나의 바이트를 전달한다.
opcode라고 하는 반환 값은 호출자가 원하는 경우 따라갈 수 있도록 beginning and ending literals, objects 그리고 arrays 와 같은 중요한 파싱 이벤트에 대해 호출자에게 알려준다.
반환값인 scanEnd는 방금 전달된 바이트 *이전*에 단일 최상위(single top-level)의 JSON 값이 완료되었음을 나타낸다. (숫자의 끝을 인식하려면 표시(indication)이 반드시 지연되어야 한다; 123은 전체 값일 수도 12345e+67의 시작 값일 수 있다)
2. decodeState.init()
func (d *decodeState) init(data []byte) *decodeState {
d.data = data
d.off = 0
d.savedError = nil
d.errorContext.Struct = nil
// Reuse the allocated space for the FieldStack slice.
d.errorContext.FieldStack = d.errorContext.FieldStack[:0]
return d
}
unmarshal 함수에서 사용할 decode state의 필드들을 초기화해준다.
여기서 scan 필드는 초기화 되지 않는데, checkValid에서 스캔한 JSON 데이터를 가지고 있다는걸 잊지 말자!
scanner.reset()
func (s *scanner) reset() {
s.step = stateBeginValue
s.parseState = s.parseState[0:0]
s.err = nil
s.endTop = false
}
위 init 함수와 헷갈린 scanner의 reset() 함수다! (심지어 리시버도 다른데 말이다!)
reset 함수는 scanner를 사용하기 위한 준비를 하는 함수다.
scanner.step() 함수를 호출하기 전 반드시 먼저 호출이 되어야 한다.
3. decodeState.unmarshal()
func (d *decodeState) unmarshal(v interface{}) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return &InvalidUnmarshalError{reflect.TypeOf(v)}
}
d.scan.reset()
d.scanWhile(scanSkipSpace)
// We decode rv not rv.Elem because the Unmarshaler interface
// test must be applied at the top level of the value.
err := d.value(rv)
if err != nil {
return d.addErrorContext(err)
}
return d.savedError
}
json의 unmarshal 함수는 사실 decodeStated의 메소드인 unmarshal() 함수다.
여기서 개인적으로 checkValid() 함수에서 스캔을 하기 전에 scan.reset()을 시켰는데 왜 여기서 다시 한번 scan.reset()을 하는지 의문이 남아 있다. -> scanWhile() 함수 내부에 s.step() 함수를 사용하고 있기 때문에 초기화가 필요하다!
decodeState.scanWhile()
// scanWhile processes bytes in d.data[d.off:] until it
// receives a scan code not equal to op.
func (d *decodeState) scanWhile(op int) {
s, data, i := &d.scan, d.data, d.off
for i < len(data) {
newOp := s.step(s, data[i])
i++
if newOp != op {
d.opcode = newOp
d.off = i
return
}
}
d.off = len(data) + 1 // mark processed EOF with len+1
d.opcode = d.scan.eof()
}
1. 데이터의 길이 만큼 for 문을 돌면서 데이터를 's', 즉 scan 필드에 넣는다.
2. step() 함수를 통해 opcode를 얻는다.
step() 함수는 다음 전환(the next transition)을 실행하기 위해 호출되는 함수다. 참고로 정수 상수(an integer constant)와 스위치가 있는 단일 func를 사용해 보았지만 func를 직접 사용하는 것이 64비트 Mac Mini에서 10% 더 빨랐고 읽기에도 더 좋았다고 한다.
3. 다음 데이터를 읽기 위해서 i를 증가시킨다.
4. 만약 기존 opcode와 다른 opcode라면, decodeState의 opcode, off를 마지막 읽은 데이터로 업데이트하고, 함수를 종료한다.
5. 데이터가 모두 같은 opcode라서 데이터를 모두 성공적으로 스캔했다면, off, opcode 에 EOF를 기록하고 함수를 종료한다.
아래 값은 scanWhile() 함수에서 받는 opcode(명령어) 값의 종류다. 호출자가 알아야 할 현재 스캔 상태에 대한 세부 정보를 제공한다.
이 값들은 scanner.state에 할당된 상태 전이 함수(state transition functions)와 scanner.eof 메서드가 반환하는 값이다. scanner.state의 특정 호출의 반환 값을 무시해도 괜찮다고 한다. 하나의 호출이 scanError를 반환하면 이후의 모든 호출도 scanError를 반환한다.
const (
// Continue.
scanContinue = iota // uninteresting byte
scanBeginLiteral // end implied by next result != scanContinue
scanBeginObject // begin object
scanObjectKey // just finished object key (string)
scanObjectValue // just finished non-last object value
scanEndObject // end object (implies scanObjectValue if possible)
scanBeginArray // begin array
scanArrayValue // just finished array value
scanEndArray // end array (implies scanArrayValue if possible)
scanSkipSpace // space byte; can skip; known to be last "continue" result
// Stop.
scanEnd // top-level value ended *before* this byte; known to be first "stop" result
scanError // hit an error, scanner.err.
)
decodeState.Value()
func (d *decodeState) value(v reflect.Value) error {
switch d.opcode {
default:
panic(phasePanicMsg)
case scanBeginArray:
if v.IsValid() {
if err := d.array(v); err != nil {
return err
}
} else {
d.skip()
}
d.scanNext()
case scanBeginObject:
if v.IsValid() {
if err := d.object(v); err != nil {
return err
}
} else {
d.skip()
}
d.scanNext()
case scanBeginLiteral:
// All bytes inside literal return scanContinue op code.
start := d.readIndex()
d.rescanLiteral()
if v.IsValid() {
if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil {
return err
}
}
}
return nil
}
value() 함수는 d.data[d.off-1:]의 JSON 값을 소비하여(consumes) v로 디코딩하고 다음 바이트를 미리 읽는다.
v가 유효하지 않으면 값은 삭제된다. 값의 첫 번째 바이트는 이미 읽었다.
Unmarshal은 Marshal이 사용하는 인코딩의 역(inverse)을 사용하여 필요에 따라 맵, 슬라이스 및 포인터를 할당하며 다음 추가 규칙을 따른다. 더보기란 참고.
- JSON을 포인터로 파싱하기 위해 Unmarshal은 먼저 JSON이 JSON 리터럴 null인 경우를 처리하는데, 이 경우 Unmarshal은 포인터를 nil로 설정한다. 그렇지 않으면 Unmarshal은 JSON을 포인터가 가리키는 값으로 파싱하기 때문이다. 포인터가 nil인 경우, Unmarshal은 포인터가 가리킬 수 있도록 새 값을 할당한다.
- JSON을 Unmarshaler 인터페이스를 구현하는 값으로 언마샬링하기 위해서, Unmarshal은 해당 값의 UnmarshalJSON 메소드를 호출한다. 입력이 JSON null인 경우도 포함된다. 그렇지 않으면 값이 encoding.TextUnmarshaler를 구현하고, 입력이 JSON 따옴표로 묶인 문자열인 경우, Unmarshal은 인용되지 않은 문자열 형식을 사용하여(with the unquoted form of the string) 해당 값의 UnmarshalText 메서드를 호출한다.
- 구조체(struct)로 JSON을 파싱하기 위해 Unmarshal은 들어오는 객체 키를 Marshal에서 사용하는 키(구조체 필드 이름 또는 해당 태그)와 일치시킨다. 기본적으로 정확한 일치를 선호하지만 대소문자는 구분하지 않는다.(만약 처리하려는 속성값이 대소문자로 구분이 되어 있다면 주의 깊게 처리해야한다.) 기본적으로 구조체 필드와 대응되지 않는 객체 키는 무시된다. (대안으로 Decoder.DisallowUnknownFields를 참조하자)
- 인터페이스 값으로 JSON을 파싱하면, Unmarshal은 다음 중 하나를 인터페이스 값에 저장한다.
JSON booleans -> bool
JSON numbers -> float64
JSON strings -> string
JSON arrays -> []interface{}
JSON object -> map[string]interface{}
JSON null -> nil - JSON arrays을 슬라이스(slice)로 파싱하기 위해 Unmarshal은 슬라이스 길이를 0으로 재설정한다. 그리고 각 요소를 슬라이스에 추가한다. 빈 JSON 배열을 슬라이스로 파싱하기 위해 Unmarshal은 슬라이스를 새로운 빈 슬라이스로 대체한다.
- JSON arrays을 Go 배열(array)로 구문 분석하기 위해 Unmarshal은 JSON 배열 요소를 해당하는 Go 배열 요소로 디코딩한다. Go 배열이 JSON 배열보다 작으면 추가 JSON 배열 요소는 버려진다. JSON 배열이 Go 배열보다 작으면 추가 Go 배열 요소는 제로 값으로 설정된다.
- JSON object를 map으로 구문 분석하기 위해 Unmarshal은 먼저 사용할 맵을 설정한다. 맵이 nil인 경우, Unmarshal은 새 맵을 할당합니다. 그렇지 않으면 Unmarshal은 기존 맵을 재사용하고 기존 항목을 유지한다. Unmarshal은 그런 다음 JSON 객체에서 키-값 쌍을 맵에 저장합니다. 맵의 키 유형은 모든 문자열 유형, 정수이거나 json.Unmarshaler 혹은 encoding.TextUnmarshaler를 구현해야 한다.
- JSON 값이 주어진 대상 유형에 적합하지 않거나 JSON 숫자가 대상 유형보다 크면(overflow), Unmarshal은 해당 필드를 건너뛰고 가능한 최선의 상태로 구문 분석을 완료한다. 이런 경우를 제외하고 심각한 오류가 발견되지 않으면 Unmarshal은 해당 유형 중 가장 빠른 UnmarshalTypeError를 반환한다. 모든 경우에 문제가 있는 필드 뒤에 있는 모든 나머지 필드이 타깃 객체에 파싱된다는 보장은 없다.
- JSON null 값은 인터페이스, 맵, 포인터 또는 슬라이스로 구문 분석될 때 해당 Go 값을 nil로 설정해서 파싱된다. null은 종종 JSON에서 "존재하지 않음"을 의미하기 때문에 JSON null을 다른 Go 유형으로 파싱해도 값에 영향을 미치지 않고 오류도 발생하지 않는다.
- 따옴표로 묶인 문자열을 파싱할 때, 잘못된 UTF-8 또는 UTF-16 대리 쌍(surrogate pairs)은 오류로 처리되지 않는다. 대신, 유니코드 대체 문자 U+FFFD로 대체된다.
아무튼 여기서 핵심은 unmarshal은 []byte 데이터를 포인터에 할당시켜주는 함수라는 점이다.
if resp, err := client.Do(req); err != nil {
return err
} else if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
return err
}
이번에는 NewDecoder()와 Decode() 함수에 대해 알아보자.
json.NewDecoder()
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r: r}
}
NewDecoder() 함수는 'r'을 읽는(reads from r) 새로운 디코더를 반환해주는 함수다.
디코더는 자체 버퍼링을 도입하며 요청된 JSON 값 이상으로 'r'에서 데이터를 읽을 수 있다.(read data from r beyond the JSON values requested.)
type Decoder struct {
r io.Reader
buf []byte
d decodeState
scanp int // start of unread data in buf
scanned int64 // amount of data already scanned
scan scanner
err error
tokenState int
tokenStack []int
}
Decoder는 input stream에서 JSON 값을 읽고 해독(decode)한다.
즉, Decoder 객체를 만들어 input stream인 r 값을 넣어준다. 실제 JSON을 읽는 건 Decoder의 Decode 메소드이다.
Decoder.Decode()
func (dec *Decoder) Decode(v interface{}) error {
if dec.err != nil {
return dec.err
}
if err := dec.tokenPrepareForDecode(); err != nil {
return err
}
if !dec.tokenValueAllowed() {
return &SyntaxError{msg: "not at beginning of value", Offset: dec.InputOffset()}
}
// Read whole value into buffer.
n, err := dec.readValue()
if err != nil {
return err
}
dec.d.init(dec.buf[dec.scanp : dec.scanp+n])
dec.scanp += n
// Don't save err from unmarshal into dec.err:
// the connection is still usable since we read a complete JSON
// object from it before the error happened.
err = dec.d.unmarshal(v)
// fixup token streaming state
dec.tokenValueEnd()
return err
}
Decoder는 입력에서 JSON 인코딩된 다음 값을 읽고, v가 가리키는 값에 저장한다.
JSON을 Go 값으로 변환하는 방법에 대한 자세한 내용은 Unmarshal의 설명을 참조하자.(와우! 결국 내부에서 unmarshal 방법을 쓰고 있었다!)
함수 로직에 대해 설명하면 아래와 같다.
- Decoder 에 에러가 있는지 확인한다. 있다면 에러를 리턴한다.
- Decoder.tokenPrepareForDecode(): decode가 될 준비를 한다.
여기서 token 이라는 값이 쓰이는데 token 아래 타입에 대한 정보를 가지고 있다.
// Delim, for the four JSON delimiters [ ] { }
// bool, for JSON booleans
// float64, for JSON numbers
// Number, for JSON numbers
// string, for JSON string literals
// nil, for JSON null
type Token interface{}
const (
tokenTopValue = iota
tokenArrayStart
tokenArrayValue
tokenArrayComma
tokenObjectStart
tokenObjectKey
tokenObjectColon
tokenObjectValue
tokenObjectComma
)
Decoder.tokenPrepareForDecode()
func (dec *Decoder) tokenPrepareForDecode() error {
// Note: Not calling peek before switch, to avoid
// putting peek into the standard Decode path.
// peek is only called when using the Token API.
switch dec.tokenState {
case tokenArrayComma:
c, err := dec.peek()
if err != nil {
return err
}
if c != ',' {
return &SyntaxError{"expected comma after array element", dec.InputOffset()}
}
dec.scanp++
dec.tokenState = tokenArrayValue
case tokenObjectColon:
c, err := dec.peek()
if err != nil {
return err
}
if c != ':' {
return &SyntaxError{"expected colon after object key", dec.InputOffset()}
}
dec.scanp++
dec.tokenState = tokenObjectValue
}
return nil
}
해당 함수에서는 토큰 값을 구분 기호 상태에서(a separator state) 값 상태(value state)로 발전시킨다.
위와 같이 토큰은 단순히 JSON 데이터가 어떤 타입인지에 대한 정보만 가지고 있었다면, 어떤 값인지에 대한 정보를 갖고 있도록 바꿔준다.
이 함수에서는 특별히 array와 object에 대한 처리만을 하는 듯 하다. 나머지는 nil 리턴.
Decoder.peek()
func (dec *Decoder) peek() (byte, error) {
var err error
for {
for i := dec.scanp; i < len(dec.buf); i++ {
c := dec.buf[i]
if isSpace(c) {
continue
}
dec.scanp = i
return c, nil
}
// buffer has been scanned, now report any error
if err != nil {
return 0, err
}
err = dec.refill()
}
}
peek() 함수는 space가 있다면 스캔하지 않고 continue 하면서 데이터를 읽고 있다. space가 아닐 경우에 c를 리턴한다. 타입에 맞는 값인지 검증하기 위해 c가 쓰인다.
함수의 큰 맥락은 아래와 같다.
1. dec.scanp는 dec.buf에서 읽지 않은 데이터의 시작이다. for문을 돌며 scanp 를 i 로 업데이트 하고 있다.
2. 데이터를 읽는 동안 에러가 있었는지 확인하고, 에러가 있으면 에러를 리턴한다.
3. 버퍼 공간이 부족하진 않은지 확인하고 부족하면 에러를 리턴한다.
Decoder.refill()
func (dec *Decoder) refill() error {
// Make room to read more into the buffer.
// First slide down data already consumed.
if dec.scanp > 0 {
dec.scanned += int64(dec.scanp)
n := copy(dec.buf, dec.buf[dec.scanp:])
dec.buf = dec.buf[:n]
dec.scanp = 0
}
// Grow buffer if not large enough.
const minRead = 512
if cap(dec.buf)-len(dec.buf) < minRead {
newBuf := make([]byte, len(dec.buf), 2*cap(dec.buf)+minRead)
copy(newBuf, dec.buf)
dec.buf = newBuf
}
// Read. Delay error for next iteration (after scan).
n, err := dec.r.Read(dec.buf[len(dec.buf):cap(dec.buf)])
dec.buf = dec.buf[0 : len(dec.buf)+n]
return err
}
버퍼가 충분하지 않을 때 버퍼의 양을 필요한 만큼 늘려주는 함수인 듯하다.
3. Decoder.tokenValueAllowed(): tokenState를 통해 데이터가 올바르게 시작하는지 확인한다.
func (dec *Decoder) tokenValueAllowed() bool {
switch dec.tokenState {
case tokenTopValue, tokenArrayStart, tokenArrayValue, tokenObjectValue:
return true
}
return false
}
4. Decoder.readValue(): readValue() 함수는 JSON 값 전부를 dec.buf로 읽고, 인코딩 길이를 반환한다.
func (dec *Decoder) readValue() (int, error) {
dec.scan.reset()
scanp := dec.scanp
var err error
Input:
// help the compiler see that scanp is never negative, so it can remove
// some bounds checks below.
for scanp >= 0 {
// Look in the buffer for a new value.
for ; scanp < len(dec.buf); scanp++ {
c := dec.buf[scanp]
dec.scan.bytes++
switch dec.scan.step(&dec.scan, c) {
case scanEnd:
// scanEnd is delayed one byte so we decrement
// the scanner bytes count by 1 to ensure that
// this value is correct in the next call of Decode.
dec.scan.bytes--
break Input
case scanEndObject, scanEndArray:
// scanEnd is delayed one byte.
// We might block trying to get that byte from src,
// so instead invent a space byte.
if stateEndValue(&dec.scan, ' ') == scanEnd {
scanp++
break Input
}
case scanError:
dec.err = dec.scan.err
return 0, dec.scan.err
}
}
// Did the last read have an error?
// Delayed until now to allow buffer scan.
if err != nil {
if err == io.EOF {
if dec.scan.step(&dec.scan, ' ') == scanEnd {
break Input
}
if nonSpace(dec.buf) {
err = io.ErrUnexpectedEOF
}
}
dec.err = err
return 0, err
}
n := scanp - dec.scanp
err = dec.refill()
scanp = dec.scanp + n
}
return scanp - dec.scanp, nil
}
5. Decoder.decodeState.init(): Unmarshal 함수의 init()과 동일하다.
6. dec.scanp += n : scanp는 버퍼로 읽지 않은 데이터의 시작 값을 가지고 있다. 여기에 n(인코딩 길이)를 더한다.
7. Decoder.decodeState.unmarshal(): Unmarshal 함수의 d.unmarshal()과 동일하다.
8. Decoder.tokenValueEnd(): tokenState를 End를 의미하는 값으로 바꾼다.
func (dec *Decoder) tokenValueEnd() {
switch dec.tokenState {
case tokenArrayStart, tokenArrayValue:
dec.tokenState = tokenArrayComma
case tokenObjectValue:
dec.tokenState = tokenObjectComma
}
}
정리
if resp, err := client.Do(req); err != nil {
return err
} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
return err
} else if err = json.Unmarshal(body, &v); err != nil {
return err
}
if resp, err := client.Do(req); err != nil {
return err
} else if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
return err
}
지금까지 글에서 알 수 있듯, Decoder 객체 필드 중에 decodeState가 있고, 해당 필드를 통해 init(), unmarshal() 메소드를 내부에서 호출하고 있다.
즉, 객체에 값을 할당하는 방식은 unmarshal()로 동일하다
그리고 읽는 방법도 사실 동일하다 볼 수 있다. 아래 함수들을 보면 유사점을 발견할 수 있다.
- decodeState.Value() 함수 & Decoder.tokenPrepareForDecode() 함수
- scanner.scanWhile(scanSkipSpace) 함수 & Decoder.peek() 함수
- decodeState.Value() 함수 & decoder.readValue() 함수
json.Decoder의 Decode() 함수 구현부분을 보면, JSON 값을 Go 값으로 unmarshal 하기 전에 메모리에 버퍼링 한다. 따라서 대부분의 경우, 메모리에 비효율적이다.
결론은 아래와 같다.
- json.Decoder: 데이터가 io.Reader 스트림 형식으로 올 때 혹은 데이터 스트림을 여러 값으로 디코딩해야할 때 사용하는게 좋다.
- json.Unmarshal: JSON 데이터가 이미 메모리에 있을 때 사용하는게 좋다.
이렇게 보면 http request를 읽을 때는 스트림으로부터 데이터를 읽어야하기 때문에 json.Decoder를 사용하는게 좋아보인다.
다만 이 글에 따르면, json.NewDecoder(resp.Body).Decode(&data) 는 전체 응답을 바로(right away) 읽지 않는다고 한다. 그렇게 되면 읽지 못한 데이터들 때문에 response body를 닫지 못해 memory-leak 이 생길 수 있으므로 되도록 ioutil.ReadAll을 사용하는 것이 좋을 것 같다. -> 왜 바로 읽지 않는지 차이점을 코드에서 찾아보자!
여담
같은 JSON 패키지 이지만 decoder를 사용한 코드가 더 읽기 쉽다고 느꼈다. 컴퓨터 공학을 배우고 개발자로 일하면서 이 코드가 어떻게 작동하겠다는 예측이 있는데, decoder가 나의 예측과 더 유사했기 때문이다. decodeState를 이용한 경우는 어떻게 이렇게 동작을 할 수 있지? 라는 의문이 많이 들었다. (ex. value를 scan 만 하는 것 같은데 assign을 어떻게 하고 있는거지? for문이 있어야 말이 되는 것 같은데 왜 없지? 이 구현을 보고 싶은데 C 코드로 되어 있어서 지금 내가 찾을 수 없는 건가? 등)
사실 decodeState 코드가 decoder 코드 내부에도 쓰이고, unmarshal의 핵심 코드이기 때문에 그냥 내 지식이 아직 많이 부족해 이해를 제대로 하지 못했다는 생각이 든다. 시간이 되는 선에서 차분히 이 코드들을 공부해보고 싶다. 이렇게까지 파고드는게 실무에서 어떤 쓸모가 있을지 아직 모르지만, 글쎄 언젠가 도움이 되지 않을까? 그리고 이 조악한 글이 다른 누구에게는 도움이 되는 글이 됐으면 좋겠다.
사실 너무 깊게 파고들면서 살짝 루즈해져 제대로 정리를 못한 부분이 있다. 추후 이 글을 읽어보면서 부족한 부분을 채워 넣도록 하겠다!
참고 자료
'Language > Go' 카테고리의 다른 글
[Go] field 이름을 기준으로 field 초기화하기 with reflect (3) | 2024.11.08 |
---|---|
[Go] Decimal decoder : Decimal을 MongoDB에 바로 String으로 저장 (3) | 2024.06.10 |
회사 프로젝트에서 2가지 방법으로 http response를 읽고 decode 하고 있다는 걸 발견했다. 둘 다 제대로 서비스가 동작하고 있는데 내부 동작이 어떻게 다른지 차이점이 궁금해서 공부해보았다.
json.Decoder, Decode, Unmarshal 그리고 io.ReadAll 을 중심으로 살펴볼 예정이다.
코드 비교
우선 코드부터 살펴보자.
두 코드 모두 client.Do()를 이용해 response(*http.Response 타입)를 가져온다.
차이점은 response body(io.ReadCloser 타입)를 읽고 원하는 go 변수에 값을 넣어주는 방법이다.
if resp, err := client.Do(req); err != nil {
return err
} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
return err
} else if err = json.Unmarshal(body, &v); err != nil {
return err
}
1. ioutil.ReadAll() 함수를 이용해 response body를 읽는다. 이 때, 리턴되는 body 변수는 []byte 타입이다.
2. body를 담기 원하는 go 변수인 v의 주소값에 바이트 배열 body를 json.Unmarshal() 함수를 통해 파싱해준다.
if resp, err := client.Do(req); err != nil {
return err
} else if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
return err
}
1. json.NewDecoder() 함수를 통해 response body를 읽는다. 이때 리턴값은 json 패키지의 Decoder 타입이다.
2. json.Decoder{} 객체의 메소드인 Decode()를 통해 v에 값을 넣어준다.
Marshal, Unmarshal & Encode, Decode
코드를 자세히 살펴보기에 앞서 Marshal, Unmarshal & Encode, Decode 개념을 간단히 짚고 넘어가겠다.
Encoding: 사람이 인지할 수 있는 문자(언어)를 약속된 규칙에 따라 컴퓨터가 이해하는 언어(0, 1 binary format)로 이루어진 코드로 바꾸는 것을 통틀어 일컫음. 즉, 정해진 규칙에 따라 코드화, 암호화, 부호화하는 것을 말함. 이렇게 인코딩하는 이유는 정보의 형태 표준화, 보안, 저장 공간 및 전송 시간 절약 등을 위함.
Decoding: 인코딩과 반대되는 개념. 복호화, 역코드화의 의미. 컴퓨터가 이해하는 언어를 사람이 이해할 수 있도록 바꿔주는 것. 즉, 바이트형식을 문자(문자열) 등으로 변환.
Marshal: 메모리에 지정된 객체를 다른 시스템에 전송하기 적합한 데이터 형식으로 변환하는 과정
Unmarshal: 전송받은 데이터를 시스템이 쉽게 처리할 수 있는 객체로 변환하는 과정
Marshal, Unmarshal 과정은 정보의 상호운용성이 목적이다.따라서 널리 사용하며, 사용하기 쉽고, 범용적인 포맷을 사용해야 한다. 특히 인터넷 어플리케이션이라면 오픈 표준 포맷을 사용해야 한다. 가장 널리 사용하는 포맷이 XML과 JSON이고, 요즘에는 특히 JSON을 널리 사용한다.
그렇다면 Golang에서는 어떻게 동작할까?
- Marshal, Encoder는 value type을 []byte로 변환
- 인코딩 값을 인메모리 바이트 슬라이스로 반환하는 json.Marshal
- 값을 io.Writer로 인코딩하는 스트림 기반 json.Encoder
- Unmarshal, Decoder는 []byte를 value type으로 변환
- 바이트 슬라이스로부터 디코딩할 수 있는 json.Unmarshal
- io.Reader로부터 디코딩할 수 있는 스트림 기반 json.Decoder
// 마셜링. Go value를 []byte로 반환한다.
func Marshal(v interface{}) ([]byte, error)
// 언마셜링. []byte를 Go value로 변환한다.
func Unmarshal(data []byte, v interface{}) error
// 인코딩. Go value를 json 문자열(JSON encoding of v + 개행문자)로 변환한다.
type Encoder struct
func NewEncoder(w io.Writer) *Encoder // 인코더 생성
func (enc *Encoder) Encode(v interface{}) error // 인코더가 인코딩하는 함수
// 디코딩. json 문자열(JSON-encoded value)을 Go value로 변환한다.
type Decoder struct
func NewDecoder(r io.Reader) *Decoder // 디코더 생성
func (dec *Decoder) Decode(v interface{}) error // 디코더가 디코딩하는 함수
여기서 Encoder, Decoder & Marshal, Unmarshal 의 차이는 무엇일까?
표준 입출력이나 파일 같은 Reader/Writer 인터페이스를 사용하여 스트림 기반으로 동작하려면 Encoder, Decoder를 이용한다. (공식 문서에도 스트림을 통해 읽고 쓴다고 명시되어 있다.)
또한 처리 속도에서 거의 50% 정도 더 빠른 성능을 내기 때문에 데이터량이 많다면 Encoder, Decoder 사용이 권장된다. (출처: Go 언어를 활용한 마이크로 서비스 개발 - 에이콘)
그 외 바이트 슬라이스나 문자열을 사용할 때, 데이터 크기가 작다면 성능차이를 체감할 수 없기 때문에 Marshal, Unmarshal이 사용된다.
ioutil.ReadAll()
먼저 ioutil 패키지의 ReadAll() 함수를 살펴보자.
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
//
// As of Go 1.16, this function simply calls io.ReadAll.
func ReadAll(r io.Reader) ([]byte, error) {
return io.ReadAll(r)
}
함수 내부에서 io 패키지의 ReadAll을 호출하고만 있다. 그 이유는 Go 1.16의 릴리즈 노트를 읽어보면 알 수 있는데, io/util 패키지를 deprecated 시켰기 때문에 io 패키지의 사용을 권장하고 있다.
The io/ioutil package has turned out to be a poorly defined and hard to understand collection of things. All functionality provided by the package has been moved to other packages. The io/ioutil package remains and will continue to work as before, but we encourage new code to use the new definitions in the io and os packages.
: io/ioutil 패키지는 제대로 정의가 되지 않았으며, 일반적으로 이해하기 어려운 것으로 보인다. 해당 패키지에서 제공하던 모든 기능을 다른 패키지로 이동한다. 물론 호환성을 위해 io/ioutil 패키지는 그대로 남아는 있고, 이전과 같이 동작은 한다. 그래도 앞으로 코드를 짜게 된다면 io 패키지와 os 패키지에 새롭게 정의된 메서드를 활용하는 것을 추천하였다.
io 패키지에 정의된 ReadAll 함수의 코드는 다음과 같다.
ioutil.ReadAll()과 io.ReadAll()의 로직은 같고, ioutil.ReadAll()을 사용하면 괜히 function call, return 스택만 낭비하는 꼴이기 때문에 io.ReadAll()을 사용하는게 좋을 것 같다. -> 레거시 코드 리팩토링 완료
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error) {
b := make([]byte, 0, 512)
for {
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == EOF {
err = nil
}
return b, err
}
}
}
(golang slice의 length와 capacity 속성에 대해서는 곧 포스팅할 예정이다.)
해당 함수는 단순하게 읽고자하는 파일의 EOF(End Of File)를 만날 때 까지 golang의 바이트 배열에 값을 넣어주고 있다.
스트림에서 바이트를 읽기 위한 기본 구조체는 Reader 인터페이스다. (Reader와 Read 메소드에 대해서는 io 패키지 문서 참고)
http 패키지의 response.Body의 타입인 ReadCloser 인터페이스는 Reader 인터페이스(+Closer 인터페이스)를 가지고 있다.
따라서 Reader로서 변수 r로 들어갈 수 있고, Read 메소드를 호출할 수 있는 것이다.
// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
Reader
Closer
}
Reader는 기본 Read 메서드를 감싸는 인터페이스다.
type Reader interface {
Read(p []byte) (n int, err error)
}
Reader에 대해 더 자세히 알아보기 위해 Reader 함수의 주석 부분을 번역했다. 회색 부분이 번역한 부분이다.
Reader 인터페이스의 한 가지 문제점은 애매한 규칙들을 가지고 있는 것이다.
Read는 최대 len(p)바이트를 p로 읽어들인다. 이는 읽은 바이트 수 (0 <= n <= len(p))와 발생한 모든 오류를 반환한다. Read가 n < len(p)를 반환하더라도 호출 중에 p를 모두 스크래치 공간(?!)으로 사용할 수 있다. 일부 데이터는 사용 가능하지만 len(p)바이트가 아닌 경우, Read는 보통 더 기다리지 않고 사용 가능한 데이터를 반환한다.
-> 첫째, 버퍼가 채워질거라는 보장이 없다. 만약 여러분이 8바이트 슬라이스를 전달한다면 여러분은 0부터 8바이트 사이의 그 어떤 값으로든 돌려 받게 된다. 부분 읽기를 다루는 것은 지저분하고 에러가 발생하기 쉽다. 다행히도 이 문제를 해결하기 위한 헬퍼 함수가 있는데, 바로 ReadFull() 함수이다.
Read가 오류나 파일 끝(EOF) 조건을 만나면, 성공적으로 n > 0바이트를 읽은 후, 읽은 바이트 수를 반환한다. 이는 동일한 호출에서 (nil이 아닌) 오류를 반환하거나 후속 호출에서 오류 (이 때, n == 0)를 반환할 수 있다.
입력 스트림 끝에서 0바이트를 반환하는 Reader의 인스턴스는 err == EOF 또는 err == nil 중 하나를 반환할 수 있다. 다음 Read는 무조건 0 혹은 EOF를 반환해야 한다. 그렇기 때문에 호출자는 오류 err를 고려하기 전에 항상 반환된 n > 0바이트를 처리해야 한다. 이렇게 하면 일부 바이트를 읽은 후 발생하는 I/O 오류 및 허용된 EOF 동작(allowed EOF behaviors), 양쪽 모두를 올바르게 처리할 수 있다.
Read의 구현은 len(p) == 0일 때를 제외하고 nil 오류와 0바이트를 반환하는 것이 권장되지 않는다. 호출자는 0과 nil을 반환하는 것은 아무 일도 일어나지 않았음을 나타내는 것으로 처리해야 한다. 특히 이는 EOF를 나타내지 않는다.
-> 둘째, 이는 스트림이 완료되면 io.EOF 에러를 정상적인 동작을 하는 것처럼 반환한다. 이는 초보자에게는 혼란을 가져올 수 있다.
구현은 p를 보존해서는 안된다.
-> Reader는 동일한 바이트를 재사용할 수 있도록 버퍼(p)를 Read() 메서드에 전달함으로써 동작한다. 만약 Read()가 바이트 슬라이스를 하나의 인자로 받는 대신 이를 반환하게 되면 Reader는 Read()를 호출할 때마다 새로운 바이트 슬라이스를 할당해야 할 것이다. 이는 가비지 컬렉터에 안좋은 영향을 끼친다.
json.Unmarshal()
ioutil.ReadAll()을 통해 스트림에서 바이트 슬라이스를 읽어 왔다면 해당 바이트 배열을 go 변수에 파싱을 해줘야 한다.
이 때 프로젝트에서는 json 패키지의 Unmarshal 함수를 사용했다.
Golang의 json 패키지의 unmarshal 함수 코드는 다음과 같다.
func Unmarshal(data []byte, v interface{}) error {
// Check for well-formedness.
// Avoids filling out half a data structure
// before discovering a JSON syntax error.
var d decodeState
err := checkValid(data, &d.scan)
if err != nil {
return err
}
d.init(data)
return d.unmarshal(v)
}
큰 맥락을 먼저 짚어보자.
Unmarshal은 JSON으로 인코딩된 데이터를 파싱(parsing, 구문 분석)하고, v가 nil이거나 포인터가 아닌 경우, Unmarshal은 InvalidUnmarshalError를 반환한다.
굳이 궁금해져서 자세한 함수들을 파고 들어봤는데 정리하고 보니 투 머치한 느낌이라 더보기란 안으로 정리한다.
1. checkValid()
func checkValid(data []byte, scan *scanner) error {
scan.reset()
for _, c := range data {
scan.bytes++
if scan.step(scan, c) == scanError {
return scan.err
}
}
if scan.eof() == scanError {
return scan.err
}
return nil
}
checkValid는 JSON 문법 에러를 발견하기 전에 데이터 구조의 절반을 채우는걸 방지한다. 따라서 데이터가 유효한 JSON 인코딩 데이터인지 확인하고, 할당(allocation)을 피하기 위해 대신, checkValid에서 사용할 수 있는 scan을 전달한다. (전달 받은 변수의 메모리 공간에 데이터를 넣어주기 위해, 별도의 메모리 할당을 피한다는 의미로 이해했다)
type decodeState
type decodeState struct {
data []byte
off int // next read offset in data
opcode int // last read result
scan scanner
errorContext struct { // provides context for type errors
Struct reflect.Type
FieldStack []string
}
savedError error
useNumber bool
disallowUnknownFields bool
}
decodeState는 JSON 값을 디코딩하는 동안의 상태를 나타낸다.
type scanner
type scanner struct {
// The step is a func to be called to execute the next transition.
// Also tried using an integer constant and a single func
// with a switch, but using the func directly was 10% faster
// on a 64-bit Mac Mini, and it's nicer to read.
step func(*scanner, byte) int
// Reached end of top-level value.
endTop bool
// Stack of what we're in the middle of - array values, object keys, object values.
parseState []int
// Error that happened, if any.
err error
// total bytes consumed, updated by decoder.Decode (and deliberately
// not set to zero by scan.reset)
bytes int64
}
scanner는 JSON 스캐닝 상태 머신이다. (scanning state machine)
호출자는 scan.reset() 함수를 호출한 다음, 각 바이트에 대해 scan.step(&scan, c)을 호출하여 한 번에 하나의 바이트를 전달한다.
opcode라고 하는 반환 값은 호출자가 원하는 경우 따라갈 수 있도록 beginning and ending literals, objects 그리고 arrays 와 같은 중요한 파싱 이벤트에 대해 호출자에게 알려준다.
반환값인 scanEnd는 방금 전달된 바이트 *이전*에 단일 최상위(single top-level)의 JSON 값이 완료되었음을 나타낸다. (숫자의 끝을 인식하려면 표시(indication)이 반드시 지연되어야 한다; 123은 전체 값일 수도 12345e+67의 시작 값일 수 있다)
2. decodeState.init()
func (d *decodeState) init(data []byte) *decodeState {
d.data = data
d.off = 0
d.savedError = nil
d.errorContext.Struct = nil
// Reuse the allocated space for the FieldStack slice.
d.errorContext.FieldStack = d.errorContext.FieldStack[:0]
return d
}
unmarshal 함수에서 사용할 decode state의 필드들을 초기화해준다.
여기서 scan 필드는 초기화 되지 않는데, checkValid에서 스캔한 JSON 데이터를 가지고 있다는걸 잊지 말자!
scanner.reset()
func (s *scanner) reset() {
s.step = stateBeginValue
s.parseState = s.parseState[0:0]
s.err = nil
s.endTop = false
}
위 init 함수와 헷갈린 scanner의 reset() 함수다! (심지어 리시버도 다른데 말이다!)
reset 함수는 scanner를 사용하기 위한 준비를 하는 함수다.
scanner.step() 함수를 호출하기 전 반드시 먼저 호출이 되어야 한다.
3. decodeState.unmarshal()
func (d *decodeState) unmarshal(v interface{}) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return &InvalidUnmarshalError{reflect.TypeOf(v)}
}
d.scan.reset()
d.scanWhile(scanSkipSpace)
// We decode rv not rv.Elem because the Unmarshaler interface
// test must be applied at the top level of the value.
err := d.value(rv)
if err != nil {
return d.addErrorContext(err)
}
return d.savedError
}
json의 unmarshal 함수는 사실 decodeStated의 메소드인 unmarshal() 함수다.
여기서 개인적으로 checkValid() 함수에서 스캔을 하기 전에 scan.reset()을 시켰는데 왜 여기서 다시 한번 scan.reset()을 하는지 의문이 남아 있다. -> scanWhile() 함수 내부에 s.step() 함수를 사용하고 있기 때문에 초기화가 필요하다!
decodeState.scanWhile()
// scanWhile processes bytes in d.data[d.off:] until it
// receives a scan code not equal to op.
func (d *decodeState) scanWhile(op int) {
s, data, i := &d.scan, d.data, d.off
for i < len(data) {
newOp := s.step(s, data[i])
i++
if newOp != op {
d.opcode = newOp
d.off = i
return
}
}
d.off = len(data) + 1 // mark processed EOF with len+1
d.opcode = d.scan.eof()
}
1. 데이터의 길이 만큼 for 문을 돌면서 데이터를 's', 즉 scan 필드에 넣는다.
2. step() 함수를 통해 opcode를 얻는다.
step() 함수는 다음 전환(the next transition)을 실행하기 위해 호출되는 함수다. 참고로 정수 상수(an integer constant)와 스위치가 있는 단일 func를 사용해 보았지만 func를 직접 사용하는 것이 64비트 Mac Mini에서 10% 더 빨랐고 읽기에도 더 좋았다고 한다.
3. 다음 데이터를 읽기 위해서 i를 증가시킨다.
4. 만약 기존 opcode와 다른 opcode라면, decodeState의 opcode, off를 마지막 읽은 데이터로 업데이트하고, 함수를 종료한다.
5. 데이터가 모두 같은 opcode라서 데이터를 모두 성공적으로 스캔했다면, off, opcode 에 EOF를 기록하고 함수를 종료한다.
아래 값은 scanWhile() 함수에서 받는 opcode(명령어) 값의 종류다. 호출자가 알아야 할 현재 스캔 상태에 대한 세부 정보를 제공한다.
이 값들은 scanner.state에 할당된 상태 전이 함수(state transition functions)와 scanner.eof 메서드가 반환하는 값이다. scanner.state의 특정 호출의 반환 값을 무시해도 괜찮다고 한다. 하나의 호출이 scanError를 반환하면 이후의 모든 호출도 scanError를 반환한다.
const (
// Continue.
scanContinue = iota // uninteresting byte
scanBeginLiteral // end implied by next result != scanContinue
scanBeginObject // begin object
scanObjectKey // just finished object key (string)
scanObjectValue // just finished non-last object value
scanEndObject // end object (implies scanObjectValue if possible)
scanBeginArray // begin array
scanArrayValue // just finished array value
scanEndArray // end array (implies scanArrayValue if possible)
scanSkipSpace // space byte; can skip; known to be last "continue" result
// Stop.
scanEnd // top-level value ended *before* this byte; known to be first "stop" result
scanError // hit an error, scanner.err.
)
decodeState.Value()
func (d *decodeState) value(v reflect.Value) error {
switch d.opcode {
default:
panic(phasePanicMsg)
case scanBeginArray:
if v.IsValid() {
if err := d.array(v); err != nil {
return err
}
} else {
d.skip()
}
d.scanNext()
case scanBeginObject:
if v.IsValid() {
if err := d.object(v); err != nil {
return err
}
} else {
d.skip()
}
d.scanNext()
case scanBeginLiteral:
// All bytes inside literal return scanContinue op code.
start := d.readIndex()
d.rescanLiteral()
if v.IsValid() {
if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil {
return err
}
}
}
return nil
}
value() 함수는 d.data[d.off-1:]의 JSON 값을 소비하여(consumes) v로 디코딩하고 다음 바이트를 미리 읽는다.
v가 유효하지 않으면 값은 삭제된다. 값의 첫 번째 바이트는 이미 읽었다.
Unmarshal은 Marshal이 사용하는 인코딩의 역(inverse)을 사용하여 필요에 따라 맵, 슬라이스 및 포인터를 할당하며 다음 추가 규칙을 따른다. 더보기란 참고.
- JSON을 포인터로 파싱하기 위해 Unmarshal은 먼저 JSON이 JSON 리터럴 null인 경우를 처리하는데, 이 경우 Unmarshal은 포인터를 nil로 설정한다. 그렇지 않으면 Unmarshal은 JSON을 포인터가 가리키는 값으로 파싱하기 때문이다. 포인터가 nil인 경우, Unmarshal은 포인터가 가리킬 수 있도록 새 값을 할당한다.
- JSON을 Unmarshaler 인터페이스를 구현하는 값으로 언마샬링하기 위해서, Unmarshal은 해당 값의 UnmarshalJSON 메소드를 호출한다. 입력이 JSON null인 경우도 포함된다. 그렇지 않으면 값이 encoding.TextUnmarshaler를 구현하고, 입력이 JSON 따옴표로 묶인 문자열인 경우, Unmarshal은 인용되지 않은 문자열 형식을 사용하여(with the unquoted form of the string) 해당 값의 UnmarshalText 메서드를 호출한다.
- 구조체(struct)로 JSON을 파싱하기 위해 Unmarshal은 들어오는 객체 키를 Marshal에서 사용하는 키(구조체 필드 이름 또는 해당 태그)와 일치시킨다. 기본적으로 정확한 일치를 선호하지만 대소문자는 구분하지 않는다.(만약 처리하려는 속성값이 대소문자로 구분이 되어 있다면 주의 깊게 처리해야한다.) 기본적으로 구조체 필드와 대응되지 않는 객체 키는 무시된다. (대안으로 Decoder.DisallowUnknownFields를 참조하자)
- 인터페이스 값으로 JSON을 파싱하면, Unmarshal은 다음 중 하나를 인터페이스 값에 저장한다.
JSON booleans -> bool
JSON numbers -> float64
JSON strings -> string
JSON arrays -> []interface{}
JSON object -> map[string]interface{}
JSON null -> nil - JSON arrays을 슬라이스(slice)로 파싱하기 위해 Unmarshal은 슬라이스 길이를 0으로 재설정한다. 그리고 각 요소를 슬라이스에 추가한다. 빈 JSON 배열을 슬라이스로 파싱하기 위해 Unmarshal은 슬라이스를 새로운 빈 슬라이스로 대체한다.
- JSON arrays을 Go 배열(array)로 구문 분석하기 위해 Unmarshal은 JSON 배열 요소를 해당하는 Go 배열 요소로 디코딩한다. Go 배열이 JSON 배열보다 작으면 추가 JSON 배열 요소는 버려진다. JSON 배열이 Go 배열보다 작으면 추가 Go 배열 요소는 제로 값으로 설정된다.
- JSON object를 map으로 구문 분석하기 위해 Unmarshal은 먼저 사용할 맵을 설정한다. 맵이 nil인 경우, Unmarshal은 새 맵을 할당합니다. 그렇지 않으면 Unmarshal은 기존 맵을 재사용하고 기존 항목을 유지한다. Unmarshal은 그런 다음 JSON 객체에서 키-값 쌍을 맵에 저장합니다. 맵의 키 유형은 모든 문자열 유형, 정수이거나 json.Unmarshaler 혹은 encoding.TextUnmarshaler를 구현해야 한다.
- JSON 값이 주어진 대상 유형에 적합하지 않거나 JSON 숫자가 대상 유형보다 크면(overflow), Unmarshal은 해당 필드를 건너뛰고 가능한 최선의 상태로 구문 분석을 완료한다. 이런 경우를 제외하고 심각한 오류가 발견되지 않으면 Unmarshal은 해당 유형 중 가장 빠른 UnmarshalTypeError를 반환한다. 모든 경우에 문제가 있는 필드 뒤에 있는 모든 나머지 필드이 타깃 객체에 파싱된다는 보장은 없다.
- JSON null 값은 인터페이스, 맵, 포인터 또는 슬라이스로 구문 분석될 때 해당 Go 값을 nil로 설정해서 파싱된다. null은 종종 JSON에서 "존재하지 않음"을 의미하기 때문에 JSON null을 다른 Go 유형으로 파싱해도 값에 영향을 미치지 않고 오류도 발생하지 않는다.
- 따옴표로 묶인 문자열을 파싱할 때, 잘못된 UTF-8 또는 UTF-16 대리 쌍(surrogate pairs)은 오류로 처리되지 않는다. 대신, 유니코드 대체 문자 U+FFFD로 대체된다.
아무튼 여기서 핵심은 unmarshal은 []byte 데이터를 포인터에 할당시켜주는 함수라는 점이다.
if resp, err := client.Do(req); err != nil {
return err
} else if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
return err
}
이번에는 NewDecoder()와 Decode() 함수에 대해 알아보자.
json.NewDecoder()
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r: r}
}
NewDecoder() 함수는 'r'을 읽는(reads from r) 새로운 디코더를 반환해주는 함수다.
디코더는 자체 버퍼링을 도입하며 요청된 JSON 값 이상으로 'r'에서 데이터를 읽을 수 있다.(read data from r beyond the JSON values requested.)
type Decoder struct {
r io.Reader
buf []byte
d decodeState
scanp int // start of unread data in buf
scanned int64 // amount of data already scanned
scan scanner
err error
tokenState int
tokenStack []int
}
Decoder는 input stream에서 JSON 값을 읽고 해독(decode)한다.
즉, Decoder 객체를 만들어 input stream인 r 값을 넣어준다. 실제 JSON을 읽는 건 Decoder의 Decode 메소드이다.
Decoder.Decode()
func (dec *Decoder) Decode(v interface{}) error {
if dec.err != nil {
return dec.err
}
if err := dec.tokenPrepareForDecode(); err != nil {
return err
}
if !dec.tokenValueAllowed() {
return &SyntaxError{msg: "not at beginning of value", Offset: dec.InputOffset()}
}
// Read whole value into buffer.
n, err := dec.readValue()
if err != nil {
return err
}
dec.d.init(dec.buf[dec.scanp : dec.scanp+n])
dec.scanp += n
// Don't save err from unmarshal into dec.err:
// the connection is still usable since we read a complete JSON
// object from it before the error happened.
err = dec.d.unmarshal(v)
// fixup token streaming state
dec.tokenValueEnd()
return err
}
Decoder는 입력에서 JSON 인코딩된 다음 값을 읽고, v가 가리키는 값에 저장한다.
JSON을 Go 값으로 변환하는 방법에 대한 자세한 내용은 Unmarshal의 설명을 참조하자.(와우! 결국 내부에서 unmarshal 방법을 쓰고 있었다!)
함수 로직에 대해 설명하면 아래와 같다.
- Decoder 에 에러가 있는지 확인한다. 있다면 에러를 리턴한다.
- Decoder.tokenPrepareForDecode(): decode가 될 준비를 한다.
여기서 token 이라는 값이 쓰이는데 token 아래 타입에 대한 정보를 가지고 있다.
// Delim, for the four JSON delimiters [ ] { }
// bool, for JSON booleans
// float64, for JSON numbers
// Number, for JSON numbers
// string, for JSON string literals
// nil, for JSON null
type Token interface{}
const (
tokenTopValue = iota
tokenArrayStart
tokenArrayValue
tokenArrayComma
tokenObjectStart
tokenObjectKey
tokenObjectColon
tokenObjectValue
tokenObjectComma
)
Decoder.tokenPrepareForDecode()
func (dec *Decoder) tokenPrepareForDecode() error {
// Note: Not calling peek before switch, to avoid
// putting peek into the standard Decode path.
// peek is only called when using the Token API.
switch dec.tokenState {
case tokenArrayComma:
c, err := dec.peek()
if err != nil {
return err
}
if c != ',' {
return &SyntaxError{"expected comma after array element", dec.InputOffset()}
}
dec.scanp++
dec.tokenState = tokenArrayValue
case tokenObjectColon:
c, err := dec.peek()
if err != nil {
return err
}
if c != ':' {
return &SyntaxError{"expected colon after object key", dec.InputOffset()}
}
dec.scanp++
dec.tokenState = tokenObjectValue
}
return nil
}
해당 함수에서는 토큰 값을 구분 기호 상태에서(a separator state) 값 상태(value state)로 발전시킨다.
위와 같이 토큰은 단순히 JSON 데이터가 어떤 타입인지에 대한 정보만 가지고 있었다면, 어떤 값인지에 대한 정보를 갖고 있도록 바꿔준다.
이 함수에서는 특별히 array와 object에 대한 처리만을 하는 듯 하다. 나머지는 nil 리턴.
Decoder.peek()
func (dec *Decoder) peek() (byte, error) {
var err error
for {
for i := dec.scanp; i < len(dec.buf); i++ {
c := dec.buf[i]
if isSpace(c) {
continue
}
dec.scanp = i
return c, nil
}
// buffer has been scanned, now report any error
if err != nil {
return 0, err
}
err = dec.refill()
}
}
peek() 함수는 space가 있다면 스캔하지 않고 continue 하면서 데이터를 읽고 있다. space가 아닐 경우에 c를 리턴한다. 타입에 맞는 값인지 검증하기 위해 c가 쓰인다.
함수의 큰 맥락은 아래와 같다.
1. dec.scanp는 dec.buf에서 읽지 않은 데이터의 시작이다. for문을 돌며 scanp 를 i 로 업데이트 하고 있다.
2. 데이터를 읽는 동안 에러가 있었는지 확인하고, 에러가 있으면 에러를 리턴한다.
3. 버퍼 공간이 부족하진 않은지 확인하고 부족하면 에러를 리턴한다.
Decoder.refill()
func (dec *Decoder) refill() error {
// Make room to read more into the buffer.
// First slide down data already consumed.
if dec.scanp > 0 {
dec.scanned += int64(dec.scanp)
n := copy(dec.buf, dec.buf[dec.scanp:])
dec.buf = dec.buf[:n]
dec.scanp = 0
}
// Grow buffer if not large enough.
const minRead = 512
if cap(dec.buf)-len(dec.buf) < minRead {
newBuf := make([]byte, len(dec.buf), 2*cap(dec.buf)+minRead)
copy(newBuf, dec.buf)
dec.buf = newBuf
}
// Read. Delay error for next iteration (after scan).
n, err := dec.r.Read(dec.buf[len(dec.buf):cap(dec.buf)])
dec.buf = dec.buf[0 : len(dec.buf)+n]
return err
}
버퍼가 충분하지 않을 때 버퍼의 양을 필요한 만큼 늘려주는 함수인 듯하다.
3. Decoder.tokenValueAllowed(): tokenState를 통해 데이터가 올바르게 시작하는지 확인한다.
func (dec *Decoder) tokenValueAllowed() bool {
switch dec.tokenState {
case tokenTopValue, tokenArrayStart, tokenArrayValue, tokenObjectValue:
return true
}
return false
}
4. Decoder.readValue(): readValue() 함수는 JSON 값 전부를 dec.buf로 읽고, 인코딩 길이를 반환한다.
func (dec *Decoder) readValue() (int, error) {
dec.scan.reset()
scanp := dec.scanp
var err error
Input:
// help the compiler see that scanp is never negative, so it can remove
// some bounds checks below.
for scanp >= 0 {
// Look in the buffer for a new value.
for ; scanp < len(dec.buf); scanp++ {
c := dec.buf[scanp]
dec.scan.bytes++
switch dec.scan.step(&dec.scan, c) {
case scanEnd:
// scanEnd is delayed one byte so we decrement
// the scanner bytes count by 1 to ensure that
// this value is correct in the next call of Decode.
dec.scan.bytes--
break Input
case scanEndObject, scanEndArray:
// scanEnd is delayed one byte.
// We might block trying to get that byte from src,
// so instead invent a space byte.
if stateEndValue(&dec.scan, ' ') == scanEnd {
scanp++
break Input
}
case scanError:
dec.err = dec.scan.err
return 0, dec.scan.err
}
}
// Did the last read have an error?
// Delayed until now to allow buffer scan.
if err != nil {
if err == io.EOF {
if dec.scan.step(&dec.scan, ' ') == scanEnd {
break Input
}
if nonSpace(dec.buf) {
err = io.ErrUnexpectedEOF
}
}
dec.err = err
return 0, err
}
n := scanp - dec.scanp
err = dec.refill()
scanp = dec.scanp + n
}
return scanp - dec.scanp, nil
}
5. Decoder.decodeState.init(): Unmarshal 함수의 init()과 동일하다.
6. dec.scanp += n : scanp는 버퍼로 읽지 않은 데이터의 시작 값을 가지고 있다. 여기에 n(인코딩 길이)를 더한다.
7. Decoder.decodeState.unmarshal(): Unmarshal 함수의 d.unmarshal()과 동일하다.
8. Decoder.tokenValueEnd(): tokenState를 End를 의미하는 값으로 바꾼다.
func (dec *Decoder) tokenValueEnd() {
switch dec.tokenState {
case tokenArrayStart, tokenArrayValue:
dec.tokenState = tokenArrayComma
case tokenObjectValue:
dec.tokenState = tokenObjectComma
}
}
정리
if resp, err := client.Do(req); err != nil {
return err
} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
return err
} else if err = json.Unmarshal(body, &v); err != nil {
return err
}
if resp, err := client.Do(req); err != nil {
return err
} else if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
return err
}
지금까지 글에서 알 수 있듯, Decoder 객체 필드 중에 decodeState가 있고, 해당 필드를 통해 init(), unmarshal() 메소드를 내부에서 호출하고 있다.
즉, 객체에 값을 할당하는 방식은 unmarshal()로 동일하다
그리고 읽는 방법도 사실 동일하다 볼 수 있다. 아래 함수들을 보면 유사점을 발견할 수 있다.
- decodeState.Value() 함수 & Decoder.tokenPrepareForDecode() 함수
- scanner.scanWhile(scanSkipSpace) 함수 & Decoder.peek() 함수
- decodeState.Value() 함수 & decoder.readValue() 함수
json.Decoder의 Decode() 함수 구현부분을 보면, JSON 값을 Go 값으로 unmarshal 하기 전에 메모리에 버퍼링 한다. 따라서 대부분의 경우, 메모리에 비효율적이다.
결론은 아래와 같다.
- json.Decoder: 데이터가 io.Reader 스트림 형식으로 올 때 혹은 데이터 스트림을 여러 값으로 디코딩해야할 때 사용하는게 좋다.
- json.Unmarshal: JSON 데이터가 이미 메모리에 있을 때 사용하는게 좋다.
이렇게 보면 http request를 읽을 때는 스트림으로부터 데이터를 읽어야하기 때문에 json.Decoder를 사용하는게 좋아보인다.
다만 이 글에 따르면, json.NewDecoder(resp.Body).Decode(&data) 는 전체 응답을 바로(right away) 읽지 않는다고 한다. 그렇게 되면 읽지 못한 데이터들 때문에 response body를 닫지 못해 memory-leak 이 생길 수 있으므로 되도록 ioutil.ReadAll을 사용하는 것이 좋을 것 같다. -> 왜 바로 읽지 않는지 차이점을 코드에서 찾아보자!
여담
같은 JSON 패키지 이지만 decoder를 사용한 코드가 더 읽기 쉽다고 느꼈다. 컴퓨터 공학을 배우고 개발자로 일하면서 이 코드가 어떻게 작동하겠다는 예측이 있는데, decoder가 나의 예측과 더 유사했기 때문이다. decodeState를 이용한 경우는 어떻게 이렇게 동작을 할 수 있지? 라는 의문이 많이 들었다. (ex. value를 scan 만 하는 것 같은데 assign을 어떻게 하고 있는거지? for문이 있어야 말이 되는 것 같은데 왜 없지? 이 구현을 보고 싶은데 C 코드로 되어 있어서 지금 내가 찾을 수 없는 건가? 등)
사실 decodeState 코드가 decoder 코드 내부에도 쓰이고, unmarshal의 핵심 코드이기 때문에 그냥 내 지식이 아직 많이 부족해 이해를 제대로 하지 못했다는 생각이 든다. 시간이 되는 선에서 차분히 이 코드들을 공부해보고 싶다. 이렇게까지 파고드는게 실무에서 어떤 쓸모가 있을지 아직 모르지만, 글쎄 언젠가 도움이 되지 않을까? 그리고 이 조악한 글이 다른 누구에게는 도움이 되는 글이 됐으면 좋겠다.
사실 너무 깊게 파고들면서 살짝 루즈해져 제대로 정리를 못한 부분이 있다. 추후 이 글을 읽어보면서 부족한 부분을 채워 넣도록 하겠다!
참고 자료
'Language > Go' 카테고리의 다른 글
[Go] field 이름을 기준으로 field 초기화하기 with reflect (3) | 2024.11.08 |
---|---|
[Go] Decimal decoder : Decimal을 MongoDB에 바로 String으로 저장 (3) | 2024.06.10 |