Huny's Dev Blog

Golang 알아두면 좋은 Tip - 2

3 min read|
Golang 알아두면 좋은 Tip - 2

Go언어로 개발하면서 사용할 수 있는 유용한 정보와 기법들을 소개한다.

string변수 encoding 변환 결과는 string 변수에 할당 가능

Golang은 기본적으로 유니코드 캐릭터셋인 UTF-8을 사용한다. encoding을 변환하는 경우(예를 들어 euc-kr) 일반적으로 string이 아닌 []byte 변수에 변환된 문자열을 넣어둔다. Golang에서는 변환된 문자열을 그대로 string에 할당할 수 있으며, 이는 string 타입 자체가 []byte를 사용하기 때문이다. 즉 encoding 함수를 사용하여 euc-kr로 변환한 값을 string에 넣고 저장하면 에디터에서 euc-kr 인코딩셋 보기모드에서 정상출력되는 것을 볼 수 있다.

package main

import (
	"os"

	"golang.org/x/text/encoding/korean"
)

func main() {
	str := "안녕12345"

	os.WriteFile("utf8.txt", []byte(str), 0755)

	euckr, err := korean.EUCKR.NewEncoder().String(str)
	if err != nil {
		panic(err)
	}

	os.WriteFile("euckr.txt", []byte(euckr), 0755)
}
go
utf8.txt
utf8.txt
euckr.txt
euckr.txt

커스텀 에러

Golang에서 error는 int, string과 같은 기본 타입이라고 생각할 수 있지만 error는 interface이며 type error interface{ Error() string } 으로 정의하고 있다. 따라서 var Error() string 만 정의되어 있으면 어떤 타입이든 error가 될 수 있다.

//builtin/builtin.go

type error interface {
	Error() string
}
go

//custom_error.go

type MyError struct{}
func (me *MyError) Error() string{
  return "error message"
}

type Errno int 
func (no Errno) Error() string{
	return "error message"
}
go

삼항연산자

Golang에서는 삼항연산자를 지원하지 않는다. if-else 구문을 사용하지 않고 참거짓에 따라 값을 대입하기만 하는 간단한 기능이 필요할 때, interface{}를 사용하여 타입에 자유로운 삼항연산 함수를 만들 수 있다. 이 때, return 타입에 대해서 명확하게 인지하는 경우에만 사용하는 것이 좋다. 그렇지 않으면 interface{}에 대한 타입확인 절차가 필요하므로, if-else를 사용하는 것이 오히려 간단하기 때문이다.

func Ternary(condition bool, truth, falsehood interface{}) interface{} {
	if condition {
		return truth
	}

	return falsehood
}

func main() {
	left := 0
	right := 1
	value := Ternary(a > b, a, b).(int)

	fmt.Println(value)
}


go

blank import활용한 초기화 함수 실행

패키지를 통틀어 단 한 번만 실행하고 싶은 로직이 있을 경우, 패키지(디렉터리) 단위로 func init을 구현, 예를 들어, init/init.go 에 func init()으로 로직을 구현해 뒀다면 build 단위에서 import _ init을 몇번을 했던 init/init.go에 정의된 init()은 한 번만 실행된다. 실행 방법도 import _ "init"과 같이 blank import 구문을 사용한다.

//init/init.go
import "fmt"

func init(){
	fmt.Println("BANNER")
}
go
//main.go

import _ "init"

func main(){
	Println("Main")
}

// Output
//BANNER
//Main
go

구조체 배열의 타입 구성

for문을 통해 구조체 배열을 자주 순회하는 경우, 구조체 포인터 배열을 사용하는 것이 효율적이다. 단, 배열 내 아이템 변경은 원본 변경을 야기하므로 이를 충분히 인지하고 사용해야 한다.

//구조체 값을 사용하는 경우, 구조체 복사가 일어나므로 규모가 큰 구조체에는 비효율적이다.
list := make([]MyStruct, 0) // 또는 []MyStruct{}

//list에 아이템 삽입

for _, item := range list{
	// something to do
}


//구조체 포인터를 사용하는 경우, 구조체 포인터 참조를 통해 큰 구조체에서도 효율적이다.
list := make([]*MyStruct, 0) // 또는 []*MyStruct{}

//list에 아이템 삽입

for _, item := range list{
 // something to do
}
go

Elapsed Time 출력하기

내부 로직(루틴)의 경과시간(elapsed time)을 구할 때 time.Since()를 사용한다. return 시 함수를 호출하는 경우 defer를 사용하여 경과시간을 구한다. time.Sub() 보다는 Since를 쓰는것이 경과 시간을 나타내는데 직관적이다.

func WorkA(){
	start := time.Now()
	defer func(){
		fmt.Println("Elapsed Time:", time.Since(start)
	}()
	
	//something to do
}

func WorkB(){
  //something to do
  
  start := time.Now()
  //something to do
	fmt.Println("Elapsed Time:", time.Since(start)
	
	//something to do
}
go

log.Print, fmt.Print에서 String() 제외하기

func String() string 함수를 구현한 구조체 또는 타입 재정의 변수를 log. Print, fmt.Print 등의 출력 함수에 전달하여 String() 값을 출력할 때, String()을 호출하지 않아도 된다.

type MyStruct struct{
	Value int
}

func (s *MyStruct) String() string{
	return "MyStruct"
}

func main(){
	fmt.Println(&MyStruct{}) // Output: MyStruct
	
	// String() 함수는 포인터에 대한 정의이므로, 아래와 같은 경우에는 멤버 요소를 출력한다.
	fmt.Println(MyStruct{}) // Output: { 0 }
}
go

For문 순회

for range 구문은 value를 사용하지 않을 때 index만 순회할 수 있다.
일반적으로 for i, item := range list 와 같이 사용하지만, for i := range list도 가능하다.

list := []string{ "A", "B", "C", "D", "E"}

for i, item := range list{
	fmt.Println(i, ":", item) // 0 : A, 1 : B, ...
}

for i := range list{
	fmt.Println(i) // 0, 1, 2, 3, 4
}

for range list{
	fmt.Println("has item")
}
go

log.Panic과 log.Fatal의 차이

log.Panic과 log.Fatal의 차이는 말 그대로 Panic은 로그 출력 후 panic을 발생시키므로 defer를 통해 recover가 가능하다. Fatal은 로그 출력 후 os.Exit(1)을 호출하므로 프로그램이 무조건 종료된다.

string의 문자 수 계산

string의 문자 개수를 계산할 때 len을 사용하지 않는다. len은 문자열의 바이트크기를 계산할 때 사용하며 Golang의 string은 utf-8 인코딩 텍스트를 나타내며, 예를 들어 한글의 경우 한 글자당 3바이트로 계산된다.

s := "한글12345"

fmt.Println(len(s)) // 11

fmt.Println(utf8.RuneCountInString(sentence)) // 7

//for 문을 사용하여 글자(rune) 수를 측정할 수도 있다.
//단 속도는 아래 방법보다 utf.RuneCountInString()가 더 빠르다.
count := 0
for range s{
	count++
}
fmt.Println(count) // 7
go

For문 index를 참조하는 Closure(클로저) 함수 주의사항

go routine으로 closure(클로저) 함수를 사용하는 경우 outer block에서 정의한 변수가 변경될 수 있는 경우 closure 함수에 인자를 전달하도록 구현해야 한다. 아래 예시는 주로 for 구문 안에서 go routine을 수행할 때 실수할 수 있는 loopclosure 문제에 대한 예시이다

//loopclosure example

//Println이 실행되는 시점의 i 값을 출력
for i := 0; i < 10; i++ {
	go func() {
		fmt.Println("i:", i)
	}()
}
//Output
// 항상 일정하지 않으나 i: 10 을 대부분 출력

//go routine이 실행되는 시점의 i 값을 출력
for i := 0; i < 10; i++ {
	go func(value int) {
		fmt.Println("i:", value)
	}(i)
}
//Output
// i: 0 ~ i: 9 까지 한 번씩 출력
go

Subscribe to our newsletter

Get the latest news and updates from our team