상세 컨텐츠

본문 제목

Golang - map[string]interface{}, map[string]any{}

Programming Language/Go

by Yongari 2023. 4. 12. 10:11

본문

Go에서  map[string]interface{} 란 무엇이며, 왜 그렇게 유용한가요? 프로그램에서 문자열과 인터페이스{}의 맵을 어떻게 처리할까요? 인터페이스{}는 도대체 무엇일까요? 알아봅시다.

Golang ‘map string interface’ example

다음은 map[string]interface{} 리터럴의 예시입니다:

 

package main

import "fmt"

func main() {
	foods := map[string]interface{}{
		"bacon": "delicious",
		"eggs": struct {
			source string
			price  float64
		}{"chicken", 1.75},
		"steak": true,
	}

	yongari := map[string]interface{}{
		"dynamic": "nft",
		"game": struct {
			fun      string
			gametime int
		}{"starcraft", 19},
		"bibim noodle": "paldo",
		"exercise":     "soccer",
	}

	fmt.Println("foods", foods)

	fmt.Println("yongari", yongari)
}

 

go run test_interface.go 
foods map[bacon:delicious eggs:{chicken 1.75} steak:true]
yongari map[bibim noodle:paldo dynamic:nft exercise:soccer game:{starcraft 19}]

 

 

What is a map[string]interface{}?

위 예제에서 foods 변수의 유형은 키는 문자열이고 값은 인터페이스{} 유형인 맵입니다.

이게 뭐죠? Go 인터페이스는 그 자체로 튜토리얼 시리즈의 가치가 있지만, 실제보다 훨씬 더 복잡해 보이는 주제 중 하나이기 때문에 처음에는 대부분의 사람들에게 조금 생소할 뿐입니다.

여기서 인터페이스는 유형을 지정하지 않고 값을 참조하는 방법이라고 설명하는 것으로 충분합니다. 대신 인터페이스는 어떤 메서드를 가지고 있는지 지정합니다. 예를 들어 널리 사용되는 io.Reader 인터페이스 유형은 해당 유형의 값에 특정 서명을 가진 Read() 메서드가 있음을 알려줍니다.

그렇다면 인터페이스{}란 무엇일까요? '빈 인터페이스'라고 발음되는 이 인터페이스는 메서드를 전혀 지정하지 않는 인터페이스입니다! 이것은 인터페이스{} 값에 메서드가 없어야 한다는 뜻이 아니라, 어떤 메서드가 있을 수도 있고 없을 수도 있는지에 대해 전혀 언급하지 않는다는 뜻입니다. 바둑 속담에 따르면 interface{}는 아무 말도 하지 않습니다.

map[string]any in Go

그렇다면 빈 인터페이스를 만족시킬 수 있는 데이터 유형은 무엇일까요? 뭐든 가능합니다. interface{}는 허용하는 값에 전혀 제약을 두지 않으므로 어떤 유형이든 괜찮습니다. 이것이 바로 Go가 인터페이스{}의 동의어로 미리 선언된 식별자 any를 최근에 추가한 이유입니다.

임의의 유형의 임의의 값 모음을 저장해야 하는 경우 문자열로 식별되는 map[string]interface{} 또는 map[string]any가 이상적인 선택입니다.

 

 

Why is interface{} so useful?

값에 대해 아무 것도 알려주지 않는다면 인터페이스{}의 의미가 무엇일까요? 바로 그 점이 바로 유용한 이유입니다. 무엇이든 참조할 수 있기 때문입니다! 인터페이스{}(또는 이제부터는 any)라는 타입은 모든 값에 적용됩니다.

interface{}로 선언된 변수는 문자열 값, 정수, 모든 종류의 구조체, os.File에 대한 포인터 등 여러분이 생각할 수 있는 모든 것을 저장할 수 있습니다.

전달된 값을 출력하는 함수를 작성해야 하는데 이 값이 어떤 유형일지 미리 알 수 없다고 가정해 봅시다. 이것이 빈 인터페이스를 위한 작업입니다:

func printAnything(v any)

실제로 fmt.Println은 바로 이러한 이유로 매우 유사한 방식으로 정의됩니다:

func Println(a ...any) { ... }

Println 예시

더보기
더보기

Println 예시

Println은 a라는 가변 인자를 받는 함수입니다. ...any 구문은 가변 인자를 나타냅니다. 이를 통해 함수가 인자의 개수를 예측할 필요 없이 다양한 개수의 인자를 받을 수 있습니다.

즉, Println 함수는 여러 개의 인자를 출력하며, 각 인자는 자동으로 공백으로 구분되고, 출력이 끝나면 자동으로 새로운 줄을 추가합니다.

예를 들어, 다음과 같이 함수를 호출할 수 있습니다.


Println("Hello", "world", 123)

이 코드는 다음과 같이 출력됩니다.

Hello world 123

 

 

테스트코드:

더보기
더보기
package main

import "fmt"

func printAnything(v any) any {
	fmt.Println("any test", v)
	return v
}

func main() {
	foods := map[string]interface{}{
		"bacon": "delicious",
		"eggs": struct {
			source string
			price  float64
		}{"chicken", 1.75},
		"steak": true,
	}

	yongari := map[string]interface{}{
		"dynamic": "nft",
		"game": struct {
			fun      string
			gametime int
		}{"starcraft", 19},
		"bibim noodle": "paldo",
		"exercise":     "soccer",
	}

	test := printAnything(0)

	fmt.Println("foods", foods)

	fmt.Println("yongari", yongari)

	fmt.Println("printAnything", test)

}

 

map[string]any and arbitrary data

마찬가지로, 임의의 데이터를 구성하는 편리한 방법인 문자열로 각각 식별되는 다양한 종류의 컬렉션을 원한다면 map[string]interface{}를 사용하여 이를 수행할 수 있습니다. 실제로 방금 JSON 객체의 스키마에 대해 설명했습니다. 이 원시 JSON 데이터를 예로 들어보겠습니다:

{
    "name":"John",
    "age":29,
    "hobbies":[
        "martial arts",
        "breakfast foods",
        "piano"
    ]
}

지금은 명백히 가상의 나이를 간과하고 있지만, 문자열 키로 식별되는 사물의 집합이라는 것을 알 수 있지만 어떤 종류의 사물이 있을까요? 문자열, 정수, 문자열 배열이 있습니다.

이를 Go 구조체 값으로 변환해야 한다고 가정하면 다음과 같이 유형을 정의할 수 있습니다:

type Person struct {
    Name    string
    Age     int
    Hobbies []string
}

좋아요. 하지만 이를 위해서는 객체의 스키마를 미리 알고 있어야 합니다. 만약 누군가 임의의 JSON 데이터를 제공해서 이를 Go 값으로 언마샬링해야 한다면 어떻게 해야 할까요? 모든 유형의 객체에 대한 문자열 맵이라는 것만 알고 있는데 어떻게 그렇게 할 수 있을까요?

 

Decoding JSON data to map[string]any

 

나에 대한 신상 정보가 담긴 JSON 데이터가 data라는 변수에 저장되어 있다고 가정해 보겠습니다. 이 데이터를 어떻게 Go 변수로 언마샬링하여 살펴볼 수 있을까요? 그 변수는 어떤 유형이어야 할까요?

 

p := map[string]any{}
err := json.Unmarshal(data, &p)
// check error

오류가 없다면 이제 p 변수에 임의의 데이터가 포함됩니다. 성공입니다! 하지만 맵에 있는 각 값의 유형에 대해 전혀 알지 못한다는 점을 감안할 때 이 데이터로 무엇을 유용하게 할 수 있을까요?

 

 

Using map[string]any data

한 가지 방법은 타입 스위치를 사용하여 값의 타입에 따라 다른 작업을 수행하는 것입니다. 다음은 예시입니다:

for k, v := range p {
    switch c := v.(type) {
    case string:
        fmt.Printf("Item %q is a string, containing %q\n", k, c)
    case float64:
        fmt.Printf("Looks like item %q is a number, specifically %f\n", k, c)
    default:
        fmt.Printf("Not sure what type item %q is, but I think it might be %T\n", k, c)
    }
}

특수 구문 스위치 c := v.(type)은 이것이 유형 스위치임을 알려주는데, 이는 Go가 스위치 문의 각 케이스에 v의 유형을 일치시키려고 시도한다는 것을 의미합니다. 예를 들어, v가 문자열인 경우 첫 번째 케이스가 실행됩니다:

Item "name" is a string, containing "John"

 

각각의 경우 변수 c는 v의 값을 받지만 관련 유형으로 변환됩니다. 따라서 문자열의 경우 c는 문자열 유형이 됩니다.
float64의 경우는 v가 float64일 때 일치합니다:

Looks like item "age" is a number, specifically 29.000000

정수 값 29가 float64로 마샬링 해제된 것이 의아할 수 있지만 이는 정상적인 현상입니다. 모든 JSON 숫자는 json.Unmarshal에 의해 float64로 처리됩니다. 이것은 Go의 숫자 유형 중 가장 일반적인 유형입니다.

마지막으로, 일치하는 다른 대소문자가 없으면 기본 대소문자가 활성화됩니다:

Not sure what type item "hobbies" is, but I think it might be []interface {}

형식 지정자 %T에서 fmt.Printf는 값의 유형을 인쇄하므로 가끔 유용합니다. 이 경우 '취미'의 값이 임의의 데이터 조각이라는 것을 알 수 있으며, 이는 의미가 있습니다.

 

When to use map[string]any

지금까지 살펴본 것처럼 "빈 인터페이스에 대한 문자열 맵" 유형은 예를 들어 스키마를 알 수 없는 임의의 JSON 데이터와 같이 Go 세계 외부에서 들어오는 데이터를 처리해야 할 때 매우 유용합니다. 예를 들어 많은 웹 API가 이와 같은 데이터를 반환합니다.

테라폼 프로바이더를 작성할 때에도 매우 흔하게 볼 수 있는데, 이는 테라폼 리소스도 본질적으로 임의의 데이터에 대한 문자열 맵이기 때문입니다. '임의의 데이터'는 또한 종종 더 많은 임의의 데이터에 대한 문자열의 맵이기도 합니다. map[string]interface{} 가 전부입니다!

구성 파일에도 일반적으로 이러한 종류의 스키마가 있습니다. YAML 또는 CUE 파일은 JSON과 마찬가지로 빈 인터페이스에 대한 문자열 매핑이라고 생각하면 됩니다. 따라서 모든 종류의 구조화된 데이터를 다룰 때 Go 프로그램에서 이러한 유형을 자주 사용하게 됩니다.

 

 

 

A map[string]any는 모든 종류의 소켓에 꽂을 수 있고 모든 전압에서 작동하는 범용 여행용 어댑터와 같습니다. 이를 사용하여 이상한 외계 데이터로 인한 손상으로부터 취약한 프로그램을 보호할 수 있습니다.

임의의 입력 데이터를 처리할 필요가 없는데도 자체 프로그램 내에서 map[string]interface{} 값을 사용해야 하나요? 아니요, 사용해서는 안 됩니다. 객체의 스키마를 명시적으로 정의할 필요가 없어 편리해 보일 수 있지만, 이는 모든 종류의 문제를 야기할 수 있습니다. 또한 제가 생각하는 바둑의 십계명 중 하나에도 위배됩니다.

우선, 인터페이스{}는 속담처럼 아무 말도 하지 않기 때문에 이 유형의 값을 다룰 때마다 패닉을 방지하기 위해 protective type assertions을 사용해야 합니다:

if _, ok := x.(string); !ok {
    log.Fatal("oh no")
}

다시 말해, 이러한 지도에서 작동하는 안전하고 신뢰할 수 있는 프로그램을 작성하는 것이 훨씬 더 어렵다는 뜻입니다. 라이브러리에서 이런 종류의 데이터를 생성하는 경우 사용자의 사랑을 받기 어렵습니다. 대신 컴파일 타임 타입 검사가 가능하고 처리하기가 훨씬 더 편리한 일반 구조체를 사용하는 것이 좋습니다. 간단하고, 직관적이며, 이해하기 쉬운 것, 이것이 바로 바둑의 도입니다.

여행용 어댑터처럼 map[string]any 는 집에 있을 때 사용하기에는 약간 불안정하고 어색하지만, 예상되는 전압과 핀 스키마가 모두 있는 소켓에 의존할 수 있습니다.

 

 

이 게시물을 정리히면서 테스트해본 코드

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name    string   `json:"name"`
	Age     int      `json:"age"`
	Hobbies []string `json:"hobbies"`
}

type Person2 struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func printAnything(v any) any {
	fmt.Println("any test", v)
	return v
}

func main() {
	foods := map[string]interface{}{
		"bacon": "delicious",
		"eggs": struct {
			source string
			price  float64
		}{"chicken", 1.75},
		"steak": true,
	}

	yongari := map[string]interface{}{
		"dynamic": "nft",
		"game": struct {
			fun      string
			gametime int
		}{"starcraft", 19},
		"bibim noodle": "paldo",
		"exercise":     "soccer",
	}

	p := Person{
		Name: "John",
		Age:  29,
		Hobbies: []string{
			"martial arts",
			"breakfast foods",
			"piano",
		},
	}

	newp := Person2{
		Name: "yongari",
		Age:  31,
	}

	jsonBytes, err := json.Marshal(newp)
	if err != nil {
		fmt.Println("err test")
	}

	p2 := map[string]any{}
	err = json.Unmarshal(jsonBytes, &p2)
	if err != nil {
		fmt.Println("err test")
	}

	for k, v := range p2 {
		switch c := v.(type) {
		case string:
			fmt.Printf("Item %q is a string, containing %q\n", k, c)
		case float64:
			fmt.Printf("Looks like item %q is a number, specifically %f\n", k, c)
		default:
			fmt.Printf("Not sure what type item %q is, but I think it might be %T\n", k, c)
		}
	}

	test := printAnything(0)

	fmt.Println("foods", foods)

	fmt.Println("yongari", yongari)

	fmt.Println("printAnything", test)

	fmt.Println("p", p)
}

 

위 코드 실행결과

go run map_string_interface.go 
Item "name" is a string, containing "yongari"
Looks like item "age" is a number, specifically 31.000000
any test 0
foods map[bacon:delicious eggs:{chicken 1.75} steak:true]
yongari map[bibim noodle:paldo dynamic:nft exercise:soccer game:{starcraft 19}]
printAnything 0
p {John 29 [martial arts breakfast foods piano]}

 

 

출처 : https://bitfieldconsulting.com/golang/map-string-interface

 

map[string]interface{} in Go — Bitfield Consulting

What is a map[string]interface{} in Go, and why is it so useful? How do we deal with maps of string to interface{} in our programs? What the heck is an interface{}, anyway? Let's find out, with renowned Go teacher John Arundel, aka @bitfield.

bitfieldconsulting.com

 

관련글 더보기