Golang Improper package structure exports unneeded members

eye-catch Golang

Golang doesn’t have class. Using struct is similar to the class concept but different. When we want to make some members public/private in OOP languages, we add public/private to the members respectively. However, Golang doesn’t have such an attribute. If the member starts with an uppercase, it’s public. If it starts with a lowercase, it’s private.

If you write your code in the same way as other OOP languages, some of the members might be unintentionally exposed. We should know how the package structure should look like to avoid exposing unneeded members.

Sponsored links

Expose unnecessary members for testing purpose

Let’s consider a case where we need to print a greeting message. 3 languages are available and the language is randomly selected. The structure looks as follows.

├── greet.go
├── package1
│   └── greet.go
└── package2
    ├── greet.go
    ├── internal
    │   └── greeting
    │       └── greet.go
    └── selector
        └── selector.go

Let’s check a bad example first.

// package1/greet.go
package package1

import (
    "fmt"
    "math/rand"
    "time"

    "golang.org/x/text/language"
)

var supportedLanguages = []language.Tag{
    language.English,
    language.German,
    language.Japanese,
}

func Greet() {
    rand.Seed(time.Now().UnixNano())
    random := rand.Intn(3)

    selectedLanguage := supportedLanguages[random]
    fmt.Printf("Selected Language: %s\n", selectedLanguage)

    greetStr := greetInSelectedLanguage(selectedLanguage)
    fmt.Println(greetStr)
}

func greetInSelectedLanguage(locale language.Tag) string {
    now := time.Now()
    if locale == language.English {
        return GreetInEnglish(now)
    }
    if locale == language.Japanese {
        return GreetInJapanese(now)
    }
    if locale == language.German {
        return GreetInGerman(now)
    }
    return "Hello"
}

func GreetInEnglish(now time.Time) string {
    hour := now.Hour()
    if hour >= 6 && hour < 11 {
        return "Good morning"
    } else if hour >= 11 && hour < 16 {
        return "Good afternoon"
    } else if hour >= 16 && hour < 22 {
        return "Good evening"
    }
    return "Good night"
}

func GreetInJapanese(now time.Time) string {
    hour := now.Hour()
    if hour >= 6 && hour < 11 {
        return "おはよう"
    } else if hour >= 11 && hour < 16 {
        return "こんにちは"
    } else if hour >= 16 && hour < 22 {
        return "こんばんは"
    }
    return "おやすみ"
}

func GreetInGerman(now time.Time) string {
    hour := now.Hour()
    if hour >= 6 && hour < 11 {
        return "Guten Morgen"
    } else if hour >= 11 && hour < 16 {
        return "Guten Tag"
    } else if hour >= 16 && hour < 22 {
        return "Guten Abend"
    }
    return "Gute Nacht"
}

Having the same logic in 3 functions is not good but please put it aside.

The main function that we want to expose is only Greet(). We have to write the unit tests but if the package name is package1_test for the black-box test, functions that starts with a lowercase can’t be called in the test. Therefore, it makes the tests harder to write.

This ticket in stackoverflow is good to check to know about the white-box/black-box test.

To make the test easier, I made them public. We don’t have to have complex tests in this case.

However, unnecessary functions are exposed to the outside of the package.

Sponsored links

Create packages in a package

Let’s check the structure of package2.

├── greet.go
├── package1
│   └── greet.go
└── package2
    ├── greet.go
    ├── internal
    │   └── greeting
    │       └── greet.go
    └── selector
        └── selector.go

The main function is written in greet.go. Subfunctions are written in separate packages.

The following code is greet.go. There is only one function and only the function is exposed to the outside.

// package2/greet.go
package package2

import (
    "fmt"
    "math/rand"
    "play-with-go-lang/package_structure/package2/selector"
    "time"

    "golang.org/x/text/language"
)

var supportedLanguages = []language.Tag{
    language.English,
    language.German,
    language.Japanese,
}

func Greet() {
    rand.Seed(time.Now().UnixNano())
    random := rand.Intn(3)

    selectedLanguage := supportedLanguages[random]
    fmt.Printf("Selected Language: %s\n", selectedLanguage)

    greetStr := selector.GreetInSelectedLanguage(selectedLanguage)
    fmt.Println(greetStr)
}

GreetInSelectedLanguage function is used in the file. It means that the package selector exposes the function.

// package2/selector/selector.go
package selector

import (
    "golang.org/x/text/language"
    "play-with-go-lang/package_structure/package2/internal/greeting"
    "time"
)

func GreetInSelectedLanguage(locale language.Tag) string {
    now := time.Now()
    if locale == language.English {
        return greeting.GreetInEnglish(now)
    }
    if locale == language.Japanese {
        return greeting.GreetInJapanese(now)
    }
    if locale == language.German {
        return greeting.GreetInGerman(now)
    }
    return "Hello"
}

But this means that the function can be used outside of the package if the package is imported. This might be good if this function is useful to reuse to pass the greeting string. But if it’s not intentional, consider the structure again.

Create ‘internal’ package not to expose any functions

If some functions need to be public but must not be exposed, define them in internal package. If the directory name is internal, its package is not exposed.

GreetInxxxxx is defined in package2/internal/greeting/greet.go. But it can’t be imported. See the following code.

// ./greet.go (top directory)
package packagestructure

import (
    "fmt"
    "play-with-go-lang/package_structure/package1"
    "play-with-go-lang/package_structure/package2"
    "play-with-go-lang/package_structure/package2/selector"

    "golang.org/x/text/language"
    // could not import play-with-go-lang/package_structure/package2/internal/greeting (invalid use of internal package play-with-go-lang/package_structure/package2/internal/greeting)
    // "play-with-go-lang/package_structure/package2/internal/greeting"
)

func PrintGreeting() {
    fmt.Println("--- package1 ---")
    package1.Greet()
    fmt.Println("--- package2 ---")
    package2.Greet()
    fmt.Println("--- selector ---")
    greetingString := selector.GreetInSelectedLanguage(language.Georgian)
    fmt.Println(greetingString)
}

package2/selector can be imported but package2/internal/greeting not. An error is shown due to the illegal import.

When internal directory should be used?

If the functions must not be used from the outside of the package, move them to internal package. Then, unit tests can be written easily even if the project uses only black-box tests.

Comments

Copied title and URL