Go언어로 개발하면서 사용할 수 있는 유용한 정보와 기법들을 소개한다.
문자열을 증가 시킬 땐 strings.Builder 사용하기
문자열에 다른 문자열을 결합하면서 문자열을 시킬 때, golang에서는 여러가지 방법이 있으나 대표적으로 더하기 연산자를 사용할 수 있다. 간단하며 직관적이지만 용량이 커질수록 처리 속도가 느려진다. 따라서 golang에서는 strings.Builder를 사용하는 것이 좋다. java에서는 문자열의 더하기 연산자를 컴파일 단계에서 자동으로 StringBuilder와 append를 사용하여 최적화 하지만 golang에서는 동일하지 않다. 크기가 작거나 가독성을 위해 필요한 경우 더하기 연산자를 사용하고 대용량 문자열 처리를 위해서는 strings.Builder 사용을 권장한다.
//더하기 연산자 사용
url := "https://blog.huny.dev"
if url[len(url) - 1] != '/'{
url += "/"
}
//strings.Builder 사용
million := make([]string, 1000*1000)
//million에 문자열이 있다고 가정
var sb strings.Builder
for i, line := range million{
sb.WriteString(fmt.Sprintf("%60d", i))
sb.WriteString(line)
sb.WriteRune('\n')
}
io.Pipe를 사용하여 동기화 스트리밍 구현하기
프로세스 내에서 스트리밍 데이터 처리가 필요할 시기가 있다. 메모리 효율성을 위해 데이터를 소모(read)하는데 맞춰 데이터를 전송(write)하면 유용할 것이다. io.Pipe를 사용하면 PipeReader, PIpeWriter가 반환 된다. Reader 입장에서는 Write가 될 때까지 자동으로 대기하고, Writer 입장에서는 Read가 될 때까지 자동으로 대기할 수 있다.
r, w := io.Pipe()
go func() {
buf := make([]byte, 1024)
for {
_, err := r.Read(buf)
if err != nil {
break
}
fmt.Println("Received Message:", string(buf))
}
}()
scan := bufio.NewScanner(os.Stdin)
for scan.Scan() {
input := scan.Bytes()
w.Write(input)
fmt.Println("Send Message:", string(input))
}
// Output
/*
안녕<Enter>
Send Message: 안녕
Received Message: 안녕
하세요<Enter>
Send Message: 하세요
Received Message: 하세요
반갑습니다.<Enter>
Received Message: 반갑습니다.
Send Message: 반갑습니다.
*/
go routine에서의 panic을 핸들링 하기 위해 사용하는 recover의 응답은 panic 호출 시 전달한 인자와 동일하다.
// panic 으로 전달된 error 수신
defer func() {
r := recover()
if r != nil {
err := r.(error)
fmt.Println(err.Error())
}
}()
// panic 호출 시 error 전달
panic(errors.New("error message"))
go routine에서 panic 핸들링(recover)은 해당 go routine에서만 유효하다.
main 에서 defer를 통해 recover를 호출했더라도 panic에 대한 핸들링은 해당 go routine에서만 가능하다. 따라서 새로 생성한 go routine에서 panic이 발생했을 때, 해당 go routine에서 panic 핸들링이 구현되어 있지 않을 경우 프로그램 종료를 유발하다. 프로그램이 종료되기를 원하지 않을 경우 해당 go routine에서 defer recover 를 구현해 주어야 한다.
func main() {
CanHandlePanic()
CannotHandlePanic()
select {}
}
func CanHandlePanic() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recover:", r)
}
}()
a := 10
b := 0
fmt.Println(a / b)
}
func CannotHandlePanic() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recover:", r)
}
}()
go func() {
a := 10
b := 0
fmt.Println(a / b)
}()
}
//Output
/*
recover: runtime error: integer divide by zero
panic: runtime error: integer divide by zero
goroutine 6 [running]:
main.CannotHandlePanic.func2()
...
exit status 2
*/
fmt.Println 와 fmt.Print 의 차이
fmt.Println과 fmt.Print의 차이는 마지막 개행(LF, ‘\n’) 뿐만 아니라 가변적 요소 값을 출력할 때 공백을 주는 것도 차이가 있다. Println은 출력할 항목 사이에 공백(’ ‘)을 삽입한다. fmt.Print은 출력할 항목이 string이면 string 변수 이전/이후에 공백(’ ‘)을 삽입하지 않는다.
fmt.Println('c', "s", 1, 0.1, true)
//output: 99 s 1 0.1 true\n
fmt.Print('c', "s", 1, 0.1, true)
//output: 99s1 0.1 true
golang은 gob라는 데이터 직렬화(Data serialization) 패키지를 제공한다.
json, xml 처럼 구조체를 human readable 하게 구조화 하는 것이 아닌, google protobuf와 같이 binary화 한다. 데이터 자체를 암호화 하는 것은 아니므로 데이터 전송, 구조체 저장 등에 활용할 수 있다. 물론 golang에서는 json, xml 과 같은 기본적인 포맷에 대한 인코딩/디코딩도 지원한다. 구현 방향에 맞는 기능을 사용하면 된다. gob는 golang 구조체에 대해서만 유효하며, python의 pickle과 비슷하다고 할 수 있다.
type MyStruct struct {
Integer int
Float float64
String string
Bool bool
}
func main() {
s := &MyStruct{1, 0.1, "hello", true}
buf := bytes.NewBuffer(nil)
enc := gob.NewEncoder(buf)
if err := enc.Encode(s); err != nil {
panic(err)
}
fmt.Println(buf.Bytes())
//Output
/*
[63 127 3 ... 1 1 0]
*/
ss := &MyStruct{}
dec := gob.NewDecoder(buf)
if err := dec.Decode(ss); err != nil {
panic(err)
}
fmt.Println(*ss)
//Output
/*
{1 0.1 hello true}
*/
}
bytes 패키지와 strings 패키지는 동일한 기능을 하는 함수가 있다.
파일로부터 byte array를 읽어 특정 문자열을 치환하고 다시 저장해야 한다면 굳이 string으로 변환할 필요 없이 bytes로 replace 함수를 사용할 수 있다.
buf, err := os.ReadFile("text.txt")
if err != nil {
panic(err)
}
buf = bytes.ReplaceAll(
buf, []byte("dev"), []byte("DEV"))
os.WriteFile("text.txt", buf, 0755)
rate패키지를 사용하여 손쉽게 RateLimiter를 구현할 수 있다. 단위 시간 당 로직 실행 횟수를 조절하거나 데이터 전송 시 throughput를 제한하는데 활용할 수 있다.
func main() {
limit := rate.Limit(4)
limiter := rate.NewLimiter(limit, 4)
for i := 0; i < 4; i++ {
go func(idx int) {
for {
limiter.Wait(context.Background())
fmt.Println("[", idx, "]", "Do something")
}
}(i)
}
select {}
}
type은 기본 데이터 타입을 재정의 하는데 사용할 수 있다.
type은 struct나 interface를 정의하는 용도 외에도 c의 typedef 같은 형식으로도 사용할 수 있다. 기본 타입을 재정의 하면 해당 타입에 함수를 정의할 수 있어서 유용하다
type errno int
var errorlist = map[errno]string{
1: "error",
}
func (no errno) Error() string {
if no == 0 {
return "success"
}
return errorlist[no]
}
func main() {
var err error = errno(1)
fmt.Println(err) // error
err = errno(0)
fmt.Println(err) // success
}
json을 go 구조체로 변환할 때 기본 데이터 타입을 nil-able 하도록 만들고 싶다면 포인터 타입을 사용한다.
단 기본 데이터 타입을 포인터 타입으로 사용하는 것이므로, 데이터에 접근 시 값에 접근할 수 있도록 변수명 앞에 Asterisk(’*’)를 사용하는 것을 잊지 않도록 해야 한다.
type MyStruct struct {
Integer *int `json:"integer"`
String *string `json:"string"`
}
func main() {
s := &MyStruct{}
if err := json.Unmarshal([]byte(
`{"string": "integer is nil"}`,
), &s); err != nil {
panic(err)
}
fmt.Printf("Integer: %v, String: %v\n",
s.Integer, *s.String) // 값을 참조할 때 주의
}