Huny's Dev Blog

Golang 알아두면 좋은 Tip - 1

Hun Jang
Hun JangApr 3, 2024
2 min read|
Golang 알아두면 좋은 Tip - 1

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

time.Time의 시간을 변경할 때 결과 값을 대입하여 갱신한다

time.Time 타입의 변수 t가 있을 때, 한 시간 이후의 시간을 구하려면 t.Add(time.Hour)을 호출하면 된다. 이때 t의 시간은 Add에 의해서 변경되지 않으며 Add의 return값을 t에 다시 대입하는 것으로 한 시간이 추가된 시간값을 유지할 수 있다. Add의 결과를 별도의 변수에 할당하거나 갱신이 목적이 아닌 경우에는 해당하지 않는다.

t := time.Now()

//Hour를 1 늘리기
t.Add(time.Hour * 1) // t의 시간값을 변경하지 않음

t = t.Add(time.Hour * 1) // t의 시간값을 변경
go

map은 race condition(동시 접근 가능성)을 유발할 수 있으며, 고루틴에 의해 race condition을 배제하기 위해서 mutex를 사용하거나 sync.Map을 사용한다

m := map[string]string{}

f := func() {
	for {
		m["key"] = "value"
	}
}
go f()
go f()

//fatal error: concurrent map writes 에러 발생

//(1) Mutex 사용
mu := sync.Mutex{}
f := func() {
	for {
		mu.Lock()
		m["key"] = "value"
    mu.Unlock()
	}
}

//(2) sync.Map 사용
m := sync.Map{}

f := func() {
	for {
		m.Store("key", "value")
	}
}
go

map은 순서를 보장하지 않는다

Go언어의 기본 key:value 자료 형식인 map은 java의 HashMap과 유사하다. java에서는 LinkedHashMap 등을 사용하여 입력한 순서를 보장하고 keys를 순차적으로 사용할 수 있지만 Go언어 기본 map에서는 이를 지원하지 않는다.

m := map[string]interface{}{
	"A": 1,
	"B": 1.0,
	"C": true,
}

for k, v := range m {
	fmt.Sprintln(k, v)
}
// Output 아래의 순서를 보장하지 않음
// A, 1
// B, 1.0
// C, true 
go

len(nil)은 panic이 발생하지 않는다

초기화 되지 않았거나(길이가 0), 배열이 nil인 경우 len 함수는 0을 리턴한다. 별도로 len함수에 전달할 변수에 대해 요소가 있는지 없는지 확인할 때 nil 체크를 하지 않아도 된다.

var list []string

if len(list) == 0{ // len(nil)은 0, panic이 발생하지 않음
  fmt.Println("list size is empty")
}

/* 아래와 같이 조건(condition)을 사용하지 않아도 됨
if list == nil || len(list) == 0{
	fmt.Println("list size is empty")
}
*/
go

다음 로직 실행까지 지연을 주고 싶으면 time.Sleep을 사용, 특정 Duration마다 반복 실행할 경우 Ticker를 사용

for { // 아래와 같이 하면 정확한 초 단위 실행을 보장할 수 없음
  time.Sleep(time.Second)

  //Something to do
}

// 아래와 같이 하면 매 초마다 로직을 실행할 수 있음
for range tick.C {
	//Something to do		
}
go

고루틴은 시작시간이 Sequential(순차적) 하지 않다

go func(){
  fmt.Println("A")
}()
fmt.Println("B")
go func(){
	fmt.Println("C")
}()

// output이 A -> B -> C 순서로 나오지 않을 수 있다
go

시간 포맷 파싱할 때 Location 정보를 주지 않으면 time Location은 UTC가 됨

// time.Parse를 사용하면 Location 정보를 포함하지 않음
// 그러므로 한국 시간을 기준으로 time을 다룰경우 Location 정보를 포함하는 것이 좋음

fmt.Println(time.ParseInLocation("2006-01-02", "2033-01-01", time.Local))

fmt.Println(time.Parse("2006-01-02", "2033-01-01"))

//output
//
// 2023-01-01 00:00:00 +0900 KST <nil>
// 2023-01-01 00:00:00 +0000 UTC <nil>
go

map의 value type이 nil-able 이 아닌 경우, key가 없는 경우에도 key를 추가하며 값을 연산할 수 있음

m := map[string]int{}

//아래와 같이 key의 유무를 확인하지 않아도 된다
/* 필요하지 않음
if _, ok := m["key"]; !ok{
  m["key"] = 0
}
*/

m["key"]++

//time.Duration의 경우 int64를 재정의한 type이므로 아래와 같이 사용할 수도 있다.
tm := map[string]time.Duration{}

m["key"] += time.Second
go

context.Context의 완료(Done) 전파 방향은 parent → child[ren] 방향이며 child → parent의 완료 전파는 동작하지 않음

parent, parentCancel := context.WithCancel(context.Background())
defer parentCancel()

child, childCancel := context.WithCancel(parent)
defer childCancel()

// childCancel()
/* Output
2023/10/11 11:10:01 Parent Incomplete
2023/10/11 11:10:01 Child Done
*/

// parentCancel()
/* Output
2023/10/11 11:04:05 Parent Done
2023/10/11 11:04:05 Child Done
*/

select {
case <-parent.Done():
	log.Println("Parent Done")
case <-time.After(time.Second):
	log.Println("Parent Incomplete")
}

select {
case <-child.Done():
	log.Println("Child Done")
case <-time.After(time.Second):
	log.Println("Parent Incomplete")
}
go

OS(또는 플랫폼)에 따라 파일 경로를 결합(Combine)할 땐 filepath.Join을 사용하고, URL과 같이 상시 /로 구분자를 사용해야 하는 경우 path.Join을 사용한다

fmt.Println(filepath.Join("path", "to", "file"))
// Output
// Linux: path/to/file
// Windows: path\to\file

fmt.Println(path.Join("path", "to", "file"))
// Output
// path/to/file
go

함수 중복 실행 방지를 위해 channel을 사용하는 간단한 기법

var channel = make(chan struct{}, 1)

func myFunction() {
    select {
    case channel <- struct{}{}:
        defer func() { <-channel }()
        // 함수 로직
    default:
        // 함수가 이미 실행 중
    }
}
go

Subscribe to our newsletter

Get the latest news and updates from our team