Skip to content

unexported-return: allow unexported type aliases for exported types#1456

Closed
tie wants to merge 1 commit intomgechev:masterfrom
tie:master
Closed

unexported-return: allow unexported type aliases for exported types#1456
tie wants to merge 1 commit intomgechev:masterfrom
tie:master

Conversation

@tie
Copy link
Copy Markdown
Contributor

@tie tie commented Aug 2, 2025

Allow unexported type aliases if there is an “exported” type that can be used instead of an alias.

Closes #1455

@denisvmedia
Copy link
Copy Markdown
Collaborator

Thank you for your report. Personally, I don't think the behavior is wrong currently. See this:

// example.go
package example

import "time"

// Exported type
type ExportedType = time.Time

// Unexported alias for an exported type
type internalAlias = ExportedType

// Exported function returning unexported alias
func GetInternalAlias() internalAlias {
        return time.Now()
}

// Unexported alias for an exported function type
type internalFuncAlias = func()

// Exported function returning unexported alias
func GetFuncAlias() internalFuncAlias {
        return func() {
                println("hello")
        }
}
$ go doc -all .
package example // import "example"


FUNCTIONS

func GetInternalAlias() internalAlias
    Exported function returning unexported alias

func GetFuncAlias() internalFuncAlias
    Exported function returning unexported alias


TYPES

type ExportedType = time.Time
    Exported type

As you may notice, an unexported types are now shown in the docs, and it's not clear what is the original exported type. This is exactly what this rule is meant for.

@tie
Copy link
Copy Markdown
Contributor Author

tie commented Aug 2, 2025

This is exactly what this rule is meant for.

I’d assume that this rule is mostly meant to avoid returning types that cannot be passed around, i.e.

-- go.mod --
module example

go 1.24.0
-- main.go --
package main

import (
	"example/exported"
)

func main() {
	v := exported.Unexported()
	_ = v // can’t pass around the value since the type is not exported
}
-- exported/exported.go --
package exported

type unexported struct{}

func Unexported() unexported {
	return unexported{}
}

E.g. we do allow interface types:

type unexportedInterface interface {
        UnexportedInterface()
}

func UnexportedInterfaceReturner() unexportedInterface { // ← OK!
        return nil
}

Perhaps we can make this behavior configurable via an argument?

@tie
Copy link
Copy Markdown
Contributor Author

tie commented Aug 2, 2025

E.g. we do allow interface types:

That said, revive also allows returning unexported interfaces with unexported methods, which I think shouldn’t be valid per this rule.

-- go.mod --
module example

go 1.24.0
-- main.go --
package main

import (
	"example/exported"
)

func main() {
	// ./main.go:9:42: cannot use exported.UnexportedInterface() (value of interface type exported.unexportedInterface) as interface{unexportedMethod()} value in variable declaration: exported.unexportedInterface does not implement interface{unexportedMethod()} (missing method unexportedMethod)
	var _ interface{ unexportedMethod() } = exported.UnexportedInterface()
}
-- exported/exported.go --
package exported

type unexportedInterface interface {
	unexportedMethod()
}

func UnexportedInterface() unexportedInterface { // should not be OK
	return nil
}

@veeam-denis
Copy link
Copy Markdown

@tie returning interfaces is in general considered to be a bad practice (see). But even if the case is valid, generically speaking it is unfriendly to the user of your package to export a function signature that has unexported symbols in it. Of course, relying on IDE capabilities to discover the types and/or reading the original source can be an option, but it's still something that is being detected by this rule.

Note, that this example is perfectly compilable, and usable:

package main

import (
	"example/exported"
)

func main() {
	v := exported.Unexported()
	_ = v // can’t pass around the value since the type is not exported
}
-- exported/exported.go --
package exported

type unexported struct{}

func Unexported() unexported {
	return unexported{}
}

But still is unfriendly to the user.

Note, in certain cases you can even do more with unexported types:

package main

import (
    "fmt"
    "example/exported"
)

func main() {
    type converted struct {
        SomeField int
    }

    v := exported.Unexported()
    var s converted = converted(v)
    fmt.Println(s)
}
---
package exported

type unexported struct{
    SomeField int
}

func Unexported() unexported {
    return unexported{}
}

It is perfectly compilable and runnable as well, but is again unfriendly to the user.

@tie
Copy link
Copy Markdown
Contributor Author

tie commented Aug 2, 2025

Thank you for the feedback, I agree with all your points. I was confused by the fact that unexported interface types were allowed. I’ve created #1457 and #1458 to address this.

@tie tie closed this Aug 2, 2025
@alexandear alexandear added the question Issue is a like question to clarify anything label Aug 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

question Issue is a like question to clarify anything

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unexported-return: does not allow unexpected type aliases for exported types

4 participants