Go 언어의 reflect 패키지: 첫걸음부터 실전 활용까지
Go 언어를 사용하면서 reflect 패키지에 대해 어려움을 느끼는 분들이 많을 것 같은데요.
저도 처음에는 잘 사용하지 않았는데, encoding/json과 같은 패키지를 이용해 Excel에 데이터를 입출력하는 기능을 구현하면서 어느 정도 이해하게 되었답니다.
여기서는 최소한의 코드로 reflect 패키지를 간단히 설명해보겠습니다.
reflect 패키지란?
reflect 패키지는 런타임에 반사를 구현하여 프로그램이 임의의 타입을 가진 객체를 조작할 수 있게 해줍니다.
일반적으로는 정적 타입 interface{}의 값을 받아 해당 값의 동적 타입 정보를 추출하는 데 사용되는데요.
TypeOf를 호출하면 Type을 반환하고, ValueOf를 호출하면 런타임 데이터를 나타내는 Value를 반환합니다.
Zero는 Type을 받아 해당 타입의 제로 값을 나타내는 Value를 반환합니다.
자세한 내용은 reflect 패키지 문서를 참고해보세요.
샘플 코드와 출력 결과
다음은 샘플 코드입니다.
package main
import (
"fmt"
"reflect"
)
type XXX struct {
Fullname string `json:"name"`
Number int `json:"number,omitempty"`
}
func main() {
x := XXX{Fullname: "aaa", Number: 123}
rt := reflect.TypeOf(x)
rv := reflect.ValueOf(x)
fmt.Printf("%s{\n", rt.Name())
for i := 0; i < rv.Type().NumField(); i++ {
k := rt.Field(i)
v := rv.Field(i)
switch v.Kind() {
case reflect.String:
fmt.Printf(" %s: string(%q), // tag %q\n", k.Name, v.String(), k.Tag.Get(`json`))
case reflect.Int:
fmt.Printf(" %s: int(%d), // tag %q\n", k.Name, v.Int(), k.Tag.Get(`json`))
default:
}
}
fmt.Printf("}\n")
}
출력 결과는 다음과 같습니다.
XXX{
Fullname: string("aaa"), // tag "name"
Number: int(123), // tag "number,omitempty"
}
구조체의 각 필드의 타입 정보 (reflect.Type)
위 코드에서 rt.Field(i)
의 반환값은 구조체의 각 필드 정보입니다.
즉, var x XXX
에 대해 x.Fullname
이나 x.Number
로 참조되는 필드 자체의 정보입니다.
타입 정보이기 때문에, 여기서 추출한 reflect.Type에는 Tag 정보가 포함되어 있으며, k.Tag.Get()
으로 이를 가져올 수 있습니다.
위 코드의 TypeOf 관련 부분은 대략 다음과 같습니다.
x := XXX{Fullname: "aaa", Number: 123}
rt := reflect.TypeOf(x)
k := rt.Field(0) // Fullname string `json:"name"`에 대응
fmt.Printf("%q : tag %q\n", k.Name, k.Tag.Get(`json`))
// -> "Fullname" : tag "name" 출력
구조체의 각 필드의 값 정보 (reflect.Value)
위 코드에서 rv.Field(i)
의 반환값은 구조체의 각 필드에 저장된 값의 정보입니다.
reflect.Value를 사용하여 예를 들어 x.Fullname
에 접근 가능한 "aaa"라는 값과 해당 타입(string)을 추출할 수 있습니다.
x := XXX{Fullname: "aaa", Number: 123}
rv := reflect.ValueOf(x)
v := rv.Field(0) // x.Fullname에 대응
fmt.Printf("%s : %q\n", v.Kind().String(), v.String())
// -> string : "aaa" 출력
무엇을 할 수 있는가
위 정보를 바탕으로 다음과 같은 코드를 작성하여 encoding/excel과 같은 패키지를 초기 구현해봤습니다.
package main
import (
"io/ioutil"
"github.com/sago35/go-eexcel"
)
type XXX struct {
Name string `eexcel:"name"`
Number int `eexcel:"number"`
}
func main() {
x := XXX{Name: "aaa", Number: 123}
b, _ := eexcel.Marshal(x)
ioutil.WriteFile("out.xlsx", b, 0644)
// -> out.xlsx로 출력
}
위 코드로 생성된 파일은 다음과 같은 형태입니다.
코드는 다음에서 확인할 수 있습니다.
package eexcel
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/360EntSecGroup-Skylar/excelize"
)
var (
DefaultSheetName = "defines"
)
func Marshal(v interface{}) ([]byte, error) {
sh := DefaultSheetName
xlsx := excelize.NewFile()
xlsx.NewSheet(sh)
xlsx.SetCellStr(sh, "A1", "key")
xlsx.SetCellStr(sh, "B1", "value")
rv := reflect.ValueOf(v)
rt := reflect.TypeOf(v)
row := 2
for i := 0; i < rv.Type().NumField(); i++ {
k := rt.Field(i)
v := rv.Field(i)
key := k.Name
if strings.ToLower(key) == key {
// skip private key-value
continue
}
tag := k.Tag.Get("eexcel")
if tag != "" {
key = tag
}
xlsx.SetCellStr(sh, fmt.Sprintf("A%d", row), key)
switch v.Kind() {
case reflect.String:
xlsx.SetCellStr(sh, fmt.Sprintf("B%d", row), v.String())
case reflect.Int:
xlsx.SetCellInt(sh, fmt.Sprintf("B%d", row), int(v.Int()))
default:
xlsx.SetCellStr(sh, fmt.Sprintf("B%d", row), v.Kind().String())
}
row++
}
buf := bytes.Buffer{}
xlsx.Write(&buf)
return buf.Bytes(), nil
}
func Unmarshal(data []byte, v interface{}) error {
sh := DefaultSheetName
xlsx, err := excelize.OpenReader(bytes.NewReader(data))
if err != nil {
return err
}
rv := reflect.ValueOf(v)
rt := reflect.TypeOf(v)
if rt.Kind() == reflect.Ptr {
rv = rv.Elem()
rt = rt.Elem()
}
row := 2
for {
key := xlsx.GetCellValue(sh, fmt.Sprintf("A%d", row))
if key == "" {
break
}
for i := 0; i < rv.Type().NumField(); i++ {
k := rt.Field(i)
v := rv.Field(i)
kk := k.Name
tag := k.Tag.Get("eexcel")
if tag != "" {
kk = tag
}
if kk != key {
continue
}
val := xlsx.GetCellValue(sh, fmt.Sprintf("B%d", row))
switch v.Kind() {
case reflect.String:
v.SetString(val)
case reflect.Int:
vv, err := strconv.ParseInt(val, 0, 0)
if err != nil {
return err
}
v.SetInt(vv)
default:
}
break
}
row++
}
return nil
}
요약
reflect를 사용한 코드 예제와 구현 예시를 보여드렸습니다. Go 언어에서 reflect는 피할 수도 있지만, 사용해보면 매우 강력하고 흥미롭습니다. reflect 패키지를 사용해본 적이 없다면, 꼭 한번 사용해보세요.
'Go' 카테고리의 다른 글
Go 언어의 리플렉션, 제대로 활용하는 방법! (1) | 2024.10.30 |
---|---|
Go 언어의 `:=` 연산자, 그 숨겨진 이야기! (1) | 2024.10.30 |
Goroutine에서 `os.Chdir()` 사용할 때 발생하는 문제와 해결 방법 (0) | 2024.10.06 |
Go 1.18에서 `any`로 더 간결하게: `interface{}`의 진화 (0) | 2024.10.06 |
Go 언어 `reflect` 패키지 완벽 가이드: 런타임 타입 처리를 마스터하자 (0) | 2024.10.06 |