이야기박스

Go. 왜 빈 struct{}를 context.Value()의 키로 사용할까? 본문

Programming Language

Go. 왜 빈 struct{}를 context.Value()의 키로 사용할까?

박스님 2022. 5. 19. 11:35
반응형

Go린이로써 가끔 git에서 context의 key로 빈 struct{}를 사용하는 것을 보고 궁금한 점이 늘 있었습니다. 그래서 오늘은 이 내용에 관한 포스팅을 작성해보려고 합니다.

type key struct{}
ctx = context.WithValue(ctx, key{}, "my value") // Set value
myValue, ok := ctx.Value(key{}).(string) // Get value

 

이번 포스팅은 아래의 문서를 가져와 작성하였습니다.

https://gist.github.com/ww9/4ad7b2ddfb94816a30dfdf2218e02f48

 

Why use empty struct{} and not int or string for context.Value() key types in #go

Why use empty struct{} and not int or string for context.Value() key types in #go - context_keys_as_struct_not_int_or_string.md

gist.github.com

 

Sample Code

package main

import (
	"fmt"
	"unsafe"
)

type StructKey1 struct{}
type StructKey2 struct{}

type IntKey1 int
type IntKey2 int

// Why use empty struct{} and not int as context.Value() key types:
func main() {

	// First of all, plain string keys look simple but they do not protect context
	// values from being tampered by other packages:
	// Also they cause allocation, so you probably don't want to use them.
	compareKeys("dontUseString", "dontUseString") // true

	// Now let's see how struct{} types behave.
	// Same struct{} keys are always equal:
	compareKeys(StructKey1{}, StructKey1{}) // true
	// Different struct{} keys are never equal even if they appear to be of the same struct{} type:
	compareKeys(StructKey1{}, StructKey2{}) // false

	// This also applies to int keys. Same type means equal:
	compareKeys(IntKey1(0), IntKey1(0)) // true
	// And different keys are never equal even though they have 0 value:
	compareKeys(IntKey1(0), IntKey2(0)) // false
	// However, unlike struct{}, an int typed key allows for mistakes with the value:
	compareKeys(IntKey1(0), IntKey1(1)) // false
	// To add to why you shouldn't use int typed keys, when seeing that the package define a
	// key as int, the user might be tempted to pass a primitive 0 instead. Which doesn't work:
	compareKeys(IntKey1(0), 0) // false

	// Whereas with struct{} there's no ambiguity or margin for missuse.
	// So if for any reason your package wants to allow other packages to set/get a
	// context.Value() that's specific to your package, either export a struct{} key
	// like `type StructKey1 struct{}` or, even better, export methods that interact
	// with these values but keep the key unexported:

	// func WithUserIP(ctx context.Context, userIP net.IP) context.Context {
	// 	return context.WithValue(ctx, userIPKey{}, userIP)
	// }
	// func UserIPFromContext(ctx context.Context) (net.IP, bool) {
	// 	userIP, ok := ctx.Value(userIPKey{}).(net.IP)
	// 	return userIP, ok
	// }
}

func compareKeys(key1 interface{}, key2 interface{}) {
	type ifaceHdr struct {
		T unsafe.Pointer
		V unsafe.Pointer
	}

	fmt.Println("\nkey1 == key2?", key1 == key2)
	fmt.Printf("key1 %+v\n", *(*ifaceHdr)(unsafe.Pointer(&key1)))
	fmt.Printf("key2 %+v\n", *(*ifaceHdr)(unsafe.Pointer(&key2)))
}

 

struct{} 비교

// Now let's see how struct{} types behave.
// Same struct{} keys are always equal:
compareKeys(StructKey1{}, StructKey1{}) // true
// Different struct{} keys are never equal even if they appear to be of the same struct{} type:
compareKeys(StructKey1{}, StructKey2{}) // false

StructKey1과 StructKey2는 모두 빈 struct{} 이지만, 이 둘을 비교하면 다르다고 나오게 됩니다. 

 

IntKey 비교

// This also applies to int keys. Same type means equal:
compareKeys(IntKey1(0), IntKey1(0)) // true
// And different keys are never equal even though they have 0 value:
compareKeys(IntKey1(0), IntKey2(0)) // false
// However, unlike struct{}, an int typed key allows for mistakes with the value:
compareKeys(IntKey1(0), IntKey1(1)) // false
// To add to why you shouldn't use int typed keys, when seeing that the package define a
// key as int, the user might be tempted to pass a primitive 0 instead. Which doesn't work:
compareKeys(IntKey1(0), 0) // false

위와 마찬가지로 struct로 정의되게 되면, 내부 값이 같더라도 두 개의 key가 다르다는 결과값이 노출됩니다. 

 

정리

`struct{}`를 키를 사용하는 것이, 다른 타입을 키로 이용하는 것보다 오사용할 가능성을 줄일 수 있어서 보편적으로 사용되는 것 같습니다.

추가로 내부에 어떠한 구성요소도 없기 때문에, 메모리 사용 측면에서도 보다 효율적이라고 하는 것 같습니다.

 

후기

앞으로 종종 Go린이 입장에서 이러한 포스팅을 하게 될 것 같습니다. 함께 공부해 나갑시다.

반응형

'Programming Language' 카테고리의 다른 글

심심하여 시작한, Mac에서 Flutter 설치 및 실행  (0) 2023.06.10
Go. gRPC 사용해보기  (0) 2022.05.11
Go Test 간단하게 리뷰  (0) 2022.02.10