현재 3권의 신간들인 Go Optimizations 101, Go Details & Tips 101Go Generics 101이 출간되어 있습니다. Leanpub 서점에서 번들을 모두 구입하시는 방법이 비용 대비 효율이 가장 좋습니다.

Go에 대한 많은 정보들과 Go 101 책들의 최신 소식을 얻으시려면 Go 101 트위터 계정인 @Go100and1을 팔로잉 해주세요.

Memory Leaking Scenarios

When programming in a language supporting auto garbage collection, generally we don't need care about memory leaking problems, for the runtime will collect unused memory regularly. However, we do need to be aware of some special scenarios which may cause kind-of or real memory leaking. The remaining of the current article will list several such scenarios.

Kind-of Memory Leaking Caused by Substrings

Go specification doesn't specify whether or not the result string and base string involved in a substring expression should share the same underlying memory block to host the underlying byte sequences of the two strings. The standard Go compiler/runtime does let them share the same underlying memory block. This is a good design, which is both memory and CPU consuming wise. But it may cause kind-of memory leaking sometimes.

For example, after the demo function in the following example is called, there will be about 1M bytes memory leaking (kind of), until the package-level variable s0 is modified again elsewhere.
var s0 string // a package-level variable

// A demo purpose function.
func f(s1 string) {
	s0 = s1[:50]
	// Now, s0 shares the same underlying memory block
	// with s1. Although s1 is not alive now, but s0
	// is still alive, so the memory block they share
	// couldn't be collected, though there are only 50
	// bytes used in the block and all other bytes in
	// the block become unavailable.
}

func demo() {
	s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
	f(s)
}

To avoid this kind-of memory leaking, we can convert the substring to a []byte value then convert the []byte value back to string.
func f(s1 string) {
	s0 = string([]byte(s1[:50]))
}

The drawback of the above way to avoid the kind-of memory leaking is there are two 50-byte duplicates which happen in the conversion process, one of them is unnecessary.

We can make use of one of the optimizations made by the standard Go compiler to avoid the unnecessary duplicate, with a small extra cost of one byte memory wasting.
func f(s1 string) {
	s0 = (" " + s1[:50])[1:]
}

The disadvantage of the above way is the compiler optimization may become invalid later, and the optimization may be not available from other compilers.

The third way to avoid the kind-of memory leaking is to utilize the strings.Builder supported since Go 1.10.
import "strings"

func f(s1 string) {
	var b strings.Builder
	b.Grow(50)
	b.WriteString(s1[:50])
	s0 = b.String()
}

The disadvantage of the third way is it is a little verbose (by comparing to the first two ways). A good news is, since Go 1.12, we can call the Repeat function with the count argument as 1 in the strings standard package to clone a string. Since Go 1.12, the underlying implementation of strings.Repeat will make use of strings.Builder, to avoid one unnecessary duplicate.

Since Go 1.18, a Clone function has been added to the strings standard package. It becomes the best way to do this job.

Kind-of Memory Leaking Caused by Subslices

Similarly to substrings, subslices may also cause kind-of memory leaking. In the following code, after the g function is called, most memory occupied by the memory block hosting the elements of s1 will be lost (if no more values reference the memory block).
var s0 []int

func g(s1 []int) {
	// Assume the length of s1 is much larger than 30.
	s0 = s1[len(s1)-30:]
}

If we want to avoid the kind-of memory leaking, we must duplicate the 30 elements for s0, so that the aliveness of s0 will not prevent the memory block hosting the elements of s1 from being collected.
func g(s1 []int) {
	s0 = make([]int, 30)
	copy(s0, s1[len(s1)-30:])
	// Now, the memory block hosting the elements
	// of s1 can be collected if no other values
	// are referencing the memory block.
}

Kind-of Memory Leaking Caused by Not Resetting Pointers in Lost Slice Elements

In the following code, after the h function is called, the memory block allocated for the first and the last elements of slice s will get lost.
func h() []*int {
	s := []*int{new(int), new(int), new(int), new(int)}
	// do something with s ...

	return s[1:3:3]
}

As long as the returned slice is still alive, it will prevent any elements of s from being collected, which in consequence prevents the two memory blocks allocated for the two int values referenced by the first and the last elements of s from being collected.

If we want to avoid such kind-of memory leaking, we must reset the pointers stored in the lost elements.
func h() []*int {
	s := []*int{new(int), new(int), new(int), new(int)}
	// do something with s ...

	// Reset pointer values.
	s[0], s[len(s)-1] = nil, nil
	return s[1:3:3]
}

We often need to reset the pointers for some old slice elements in slice element deletion operations.

Real Memory Leaking Caused by Hanging Goroutines

Sometimes, some goroutines in a Go program may stay in blocking state for ever. Such goroutines are called hanging goroutines. Go runtime will not kill hanging goroutines, so the resources allocated for (and the memory blocks referenced by) the hanging goroutines will never get garbage collected.

There are two reasons why Go runtime will not kill hanging goroutines. One is that sometimes it is hard for Go runtime to judge whether or not a blocking goroutine will be blocked for ever. The other is sometimes we deliberately make a goroutine hanging. For example, sometimes we may let the main goroutine of a Go program hang to avoid the program exiting.

We should avoid hanging goroutines which are caused by some logic mistakes in code design.

Real Memory Leaking Caused by Not Stopping time.Ticker Values Which Are Not Used Any More

When a time.Timer value is not used any more, it will be garbage collected after some time. But this is not true for a time.Ticker value. We should stop a time.Ticker value when it is not used any more.

Real Memory Leaking Caused by Using Finalizers Improperly

Setting a finalizer for a value which is a member of a cyclic reference group may prevent all memory blocks allocated for the cyclic reference group from being collected. This is real memory leaking, not kind of.

For example, after the following function is called and exits, the memory blocks allocated for x and y are not guaranteed to be garbage collected in future garbage collecting.
func memoryLeaking() {
	type T struct {
		v [1<<20]int
		t *T
	}

	var finalizer = func(t *T) {
		 fmt.Println("finalizer called")
	}

	var x, y T

	// The SetFinalizer call makes x escape to heap.
	runtime.SetFinalizer(&x, finalizer)

	// The following line forms a cyclic reference
	// group with two members, x and y.
	// This causes x and y are not collectable.
	x.t, y.t = &y, &x // y also escapes to heap.
}

So, please avoid setting finalizers for values in a cyclic reference group.

By the way, we shouldn't use finalizers as object destructors.

Kind-of Resource Leaking by Deferring Function Calls

Please read this article for details.


Index↡

The Go 101 프로젝트는 Github 에서 호스팅됩니다. 오타, 문법 오류, 부정확한 표현, 설명 결함, 코드 버그, 끊어진 링크와 같은 모든 종류의 실수에 대한 수정 사항을 제출하여 Go 101을 개선을 돕는 것은 언제나 환영합니다.

주기적으로 Go에 대한 깊이 있는 정보를 얻고 싶다면 Go 101의 공식 트위터 계정인 @go100and1을 팔로우하거나 Go 101 슬랙 채널에j가입해주세요.

이 책의 디지털 버전은 아래와 같은 곳을 통해서 구매할 수 있습니다.
Go 101의 저자인 Tapir는 2016년 7월부터 Go 101 시리즈 책들을 집필하고 go101.org 웹사이트를 유지 관리하고 있습니다. 새로운 콘텐츠는 책과 웹사이트에 수시로 추가될 예정입니다. Tapir는 인디 게임 개발자이기도 합니다. Tapir의 게임을 플레이하여 Go 101을 지원할 수도 있습니다. (안드로이드와 아이폰/아이패드용):
  • Color Infection (★★★★★), 140개 이상의 단계로 이루어진 물리 기반의 캐주얼 퍼즐 게임
  • Rectangle Pushers (★★★★★), 2가지 모드와 104개 이상의 단계로 이루어진 캐주얼 퍼즐 게임
  • Let's Play With Particles, 세가지 미니 게임이 있는 캐주얼 액션 게임
페이팔을 통한 개인 기부도 환영합니다.

색인: