Go 언어에서 데이터 타입 간 변환과 JSON 스트림 처리는 실무에서 자주 마주치는 과제입니다. 본 문서에서는 uint32와 바이트 배열 간 변환, 불완전한 JSON 데이터 처리, 그리고 리소스 초기화 패턴을 다룹니다.
uint32와 []byte 상호 변환
바이너리 데이터를 처리할 때 부호 없는 정수와 바이트 슬라이스 간 변환은 기본적인 연산입니다. 다음 예제는 encoding/binary 패키지를 사용하여 Big Endian 방식으로 변환하는 방법을 보여줍니다.
package main
import (
"encoding/binary"
"fmt"
)
func main() {
// uint32 값을 바이트 슬라이스로 변환
original := uint32(305419896) // 0x12345678
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, original)
fmt.Printf("원본 uint32: %d, 바이트 슬라이스: %#v\n", original, buf)
// 바이트 슬라이스를 uint32로 복원
recovered := binary.BigEndian.Uint32(buf)
fmt.Printf("복원된 uint32: %d\n", recovered)
// 음수 값을 표현하려면 int32로 캐스팅
negativeBytes := []byte{0xFF, 0xFF, 0xFF, 0xFF} // -1의 2의 보수
asInt32 := int32(binary.BigEndian.Uint32(negativeBytes))
fmt.Printf("바이트 슬라이스 %#v 는 int32로 %d 입니다\n", negativeBytes, asInt32)
}
불완전한 JSON 스트림 처리
직렬 포트나 네트워크 스트림에서 JSON 데이터를 읽을 때 첫 번째 줄이 불완전한 경우가 있습니다. bufio.Reader와 json.Decoder를 조합하여 첫 번째 불완전한 줄을 건너뛰고 나머지 데이터를 처리할 수 있습니다. 아래 예제는 첫 줄이 깨진 JSON 스트림에서 유효한 레코드만 추출하는 방법을 보여줍니다.
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"strings"
)
type Message struct {
Name string `json:"name"`
Text string `json:"text"`
}
func main() {
// 첫 번째 줄이 불완전한 JSON 스트림
const jsonStream = `ft yourself!"}
{"name": "Ed", "text": "Knock knock."}
{"name": "Sam", "text": "Who's there?"}
{"name": "Ed", "text": "Go fmt."}
`
// bufio.Reader로 첫 줄을 읽어서 무시
strReader := strings.NewReader(jsonStream)
bufReader := bufio.NewReader(strReader)
_, _, err := bufReader.ReadLine()
if err != nil {
panic("unable to skip first line: " + err.Error())
}
// json.Decoder로 스트리밍 디코딩
jdec := json.NewDecoder(bufReader)
jdec.DisallowUnknownFields() // 엄격한 JSON 검증
for {
var msg Message
if err := jdec.Decode(&msg); err == io.EOF {
fmt.Println("모든 유효한 JSON 레코드를 처리했습니다.")
break
} else if err != nil {
fmt.Printf("디코딩 오류: %+v\n", err)
return
}
fmt.Printf("%+v\n", msg)
}
}
리소스 초기화 패턴
애플리케이션에서 직렬 포트와 같은 리소스를 사용할 때, 지연 초기화와 싱글톤 패턴을 결합하여 효율적으로 관리할 수 있습니다. 아래 예제는 MPort 구조체를 통해 직렬 포트 연결을 필요할 때만 생성하고 재사용하는 방법을 보여줍니다.
package main
import (
"fmt"
"sync"
"time"
"github.com/tarm/serial"
)
type MPort struct {
Port *serial.Port
mu sync.Mutex
}
var (
instance *MPort
once sync.Once
)
// newConn은 직렬 포트 연결을 생성합니다 (실제 구현은 환경에 맞게 수정)
func newConn() *serial.Port {
c := &serial.Config{
Name: "COM1",
Baud: 9600,
ReadTimeout: time.Second,
}
port, err := serial.OpenPort(c)
if err != nil {
fmt.Printf("직렬 포트 열기 실패: %v\n", err)
return nil
}
return port
}
// Write는 연결을 지연 초기화하고 데이터를 전송합니다
func (m *MPort) Write(buf []byte) (n int, err error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Port == nil {
m.Port = newConn()
if m.Port == nil {
return 0, fmt.Errorf("직렬 포트를 초기화할 수 없습니다")
}
}
return m.Port.Write(buf)
}
// GetMPort는 싱글톤 인스턴스를 반환합니다
func GetMPort() *MPort {
once.Do(func() {
instance = &MPort{}
})
return instance
}
func main() {
port := GetMPort()
n, err := port.Write([]byte("Hello, serial!"))
if err != nil {
fmt.Printf("쓰기 실패: %v\n", err)
return
}
fmt.Printf("%d 바이트 쓰기 성공\n", n)
}
이러한 패턴을 활용하면 바이너리 데이터 변환, 비정형 JSON 처리, 그리고 리소스 관리에서 코드의 안정성과 가독성을 높일 수 있습니다.