Go에서의 맵 순회 순서
TL;DR: 순서 보장하지 않음
사내 API 서버를 외부에서 사용하기 위해서는 API 게이트웨이를 통해 hmac인증을 해야 한다. 요청을 보내야 하는 url에 해시로 만든 새로운 파라미터를 뒤에 이어붙여 보내면 외부에서 API를 사용할 수 있게 된다.
hmac이야 간단해서 만드는 것은 문제가 없었는데 자꾸 에러가 발생했다. 만들어낸 주소를 python의 requests에서 테스트해도 문제가 없고, 웹 브라우저를 통해 테스트해봐도 문제가 발생하지 않는다. go에서의 gorequest에서만 문제가 발생하는 것이었다. 잘 이해가 가지 않아서 생각을 해보니, 결국 에러가 발생하는 이유는 gorequest에서 뭔가를 다르게 처리한다는 것이므로 깃허브에서 코드를 좀 살펴보니, 요청을 보내기 전에 MakeRequest를 통해 요청을 만든다는 것을 알게 되었다. 코드를 계속 살펴보다 의심되는 부분을 찾게 되어 확인차, 요청을 보낸 후 http.request의 URL을 찍어 보았다. 원인은 hmac 생성에 사용한 파라미터의 순서와 실제 요청에 사용한 쿼리 파라미터의 순서가 달랐던 것.
net/url의 values.Encode()에서 쿼리의 순서를 정렬하여 변경한다. 해당 부분의 코드는 다음과 같다.
// Encode encodes the values into ``URL encoded'' form
// ("bar=baz&foo=quux") sorted by key.
func (v Values) Encode() string {
if v == nil {
return ""
}
var buf bytes.Buffer
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
vs := v[k]
prefix := QueryEscape(k) + "="
for _, v := range vs {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(prefix)
buf.WriteString(QueryEscape(v))
}
}
return buf.String()
}
Map?
url.Values는 뭐냐 하면 아래와 같이 정의된다.
type Values map[string][]string
처음에는 map의 key를 순회할 때 순서가 항상 다르다는 것을 예상하지 못했다. 결국 간단한 코드를 작성해 보고 나서야 이를 알게 됐는데 결과는 다음과 같다.
$ cat testmap.go
package main
import "fmt"
func main() {
testmap := map[string]string{
"a": "aa",
"b": "bb",
"c": "cc",
"d": "dd",
"e": "ee",
}
for i := 0; i < 10; i++ {
keys := []string{}
for k, _ := range testmap {
keys = append(keys, k)
}
fmt.Println(keys)
}
}
$ go run testmap.go
[d e a b c]
[e a b c d]
[a b c d e]
[e a b c d]
[e a b c d]
[a b c d e]
[d e a b c]
[a b c d e]
[a b c d e]
[b c d e a]
실제로 go blog를 조금 살펴보면 다음과 같은 내용을 확인할 수 있다.
When iterating over a map with a range loop, the iteration order is not specified and is not guaranteed to be the same from one iteration to the next. Since Go 1 the runtime randomizes map iteration order, as programmers relied on the stable iteration order of the previous implementation. If you require a stable iteration order you must maintain a separate data structure that specifies that order. This example uses a separate sorted slice of keys to print a map[int]string in key order:
결국 map에서는 순서 보장 안하니까, 일정한 순서를 원하면 키를 정렬해서 쓰라는 것이다. 만약 net/url의 url.Values.Encode()에서 정렬을 하지 않게 된다면 사용자가 요청하는 주소는 매번 다르게 될 것이니, 아마 그런 이유로 정렬을 했던 것이 아닐까?
그러고 보면 python 3.6 이상부터는 dict의 순서를 보장하도록 변경되었는데, 물론 그 전에는 dict도 순서가 항상 이상하게 나와서 collection의 OrderedDict를 썼던 기억이 있다.