Golang中不同包名导致结果差异的问题探讨

Golang中不同包名导致结果差异的问题探讨 不同的包名会导致不同的结果,当我使用 app 包时,结果是:

Student study
Student study

当我把包名从 app 改为 stu 时,结果是:

BoyStudent study
GirlStudent study

我想要第二种结果,但不受包名影响

package app

import (
	"fmt"
)

type IStudent interface {
	study()
}

type Student struct {
}

func (me *Student) study() {
	fmt.Println("Student study")
}

type StudentProxy struct {
	student IStudent
}

func (me *StudentProxy) study() {
	me.student.study()
}

type studentManager struct {
	students map[string]*StudentProxy
	finder   func(string) IStudent
}

var sm = &studentManager{
	students: map[string]*StudentProxy{},
}

func StudentManager() *studentManager {
	return sm
}

func (me *studentManager) SetFinder(finder func(string) IStudent) {
	me.finder = finder
}

func (me *studentManager) GetStudent(name string) *StudentProxy {
	proxy, ok := me.students[name]
	if !ok {
		stu := me.finder(name)
		proxy = &StudentProxy{student: stu}
		me.students[name] = proxy
	}
	return proxy
}

func (me *studentManager) MethodA() {
	names := []string{"boy", "girl"}
	for _, name := range names {
		me.GetStudent(name).study()
	}
}

func (me *studentManager) MethodB() {
	names := []string{"boy", "girl"}
	for _, name := range names {
		me.GetStudent(name).study()
	}
}
package auto

import (
	"github.com/boringwork/godemo/interface/app"
	"github.com/boringwork/godemo/interface/impl"
)

func Finder(name string) app.IStudent {
	switch name {
	case "boy":
		return &impl.BoyStudent{}
	case "girl":
		return &impl.GirlStudent{}
	}
	return nil
}
package impl

import (
	"fmt"

	"github.com/boringwork/godemo/interface/app"
)

type BoyStudent struct {
	app.Student
}

func (me *BoyStudent) study() {
	fmt.Println("BoyStudent study")
}

type GirlStudent struct {
	app.Student
}

func (me *GirlStudent) study() {
	fmt.Println("GirlStudent study")
}
package main

import (
	"github.com/boringwork/godemo/interface/app"
	"github.com/boringwork/godemo/interface/auto"
)

func main() {
	app.StudentManager().SetFinder(auto.Finder)
	app.StudentManager().MethodA()
}

你使用的是哪个 Go 版本(go version)?

go version go1.10.1 darwin/amd64


更多关于Golang中不同包名导致结果差异的问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

12 回复

是的,但如果我将 do() 设为公共方法,它总是显示"child"。

更多关于Golang中不同包名导致结果差异的问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我仍然没有足够的信息来了解发生了什么。如果您无法分享包含重现步骤的源代码仓库,您能否提供每个步骤的差异对比?

// 代码示例保留原样

是的,你说得对,我重新检查并尝试了更多包名。结果如下

你的帖子中存在一些相互矛盾的信息,让人难以理解问题所在。在帖子开头你提到使用 package app 会导致:

Student study
Student study

但在你的截图中却显示使用 package app 会得到:

BoyStudent study
GirlStudent study

你能否提供一个可运行的代码库并附上重现步骤?这样能帮助我们更清楚地理解问题所在。

asv1:

原来 Go 是按字母顺序导入包的。

这在规范(包初始化)中有记载:

为确保初始化的可重现行为,构建系统鼓励按词法文件名顺序向编译器呈现属于同一包的多个文件。

我发现了新问题,保持包名不变,将方法名从 study 重命名为 Study,也会产生不同的结果。

我已将演示代码提交到GitHub:

GitHub

GitHub

头像

boringwork/godemo

godemo - go 演示项目

是的,现在使用 Study 可以正常工作,但我不确定在其他任何情况下是否都能正常工作。我会持续关注这个问题。

正如你所说,我试图将结构体嵌入视为继承,既然 Golang 提供了这样的特性,就应该加以利用。即使不能作为继承使用,计算机中也不应存在不确定性——要么所有情况都能访问,要么都不能访问,而不应受包名影响产生不同结果。

请告知这是否是语言层面的问题,以及 Golang 将如何处理它。

非常感谢。

这似乎与您的方法未导出有关。通常无法从其他包调用未导出(小写字母开头)的方法或函数。当我将方法和接口中的 study 改为 Study 后,它就开始按预期工作了。

看起来编译器在决定调用哪个方法时会考虑该方法是否已导出。简而言之,如果您要将接口导出到包外,那么也应该导出实现该接口所需的所有方法。

请告诉我将 study 改为 Study 是否解决了您的问题。

看起来您试图将结构体嵌入当作继承来处理,但"重写"了嵌入类型的某个方法。结构体嵌入并不是继承,可能会导致像这样的奇怪行为。在这种情况下,似乎 app.Student 类型不应该有 Study 方法。

这看起来是一个bug。我重新创建了一个最小复现示例。如果你将文件夹"child"重命名为"achild",那么会打印Child,如果文件夹名是"child"则只会打印Base。事实证明Go会按字母顺序导入包。

scratch/base/base.go

import "fmt"

type Interface interface {
	do()
}

type Base struct{}

func (Base) do() { fmt.Println("base") }

func Call(f func() Interface) {
	f().do()
}

scratch/child/child.go

import "scratch/base"
import "fmt"

type Child struct{ base.Base }

func (Child) do() { fmt.Println("child") }

scratch/main.go

import "scratch/child"     // prints "base"
//import "scratch/achild"  // prints "child"
import "scratch/base"

func main() {
	base.Call(func() base.Interface {
		return child.Child{}
	})
}

根据我所见,当你通过大写字母导出函数时,它应该会更可靠一些。如果你不导出它,而是使用导出的接口,行为似乎会变得不稳定。

关于一致性的观点,实际情况并非总是如此。语言经常存在未定义或意外行为。我认为在导出接口中定义未导出方法是一种被允许但可能导致未定义行为的情况。这种模式也表明代码编写得不够深思熟虑。

是否应该禁止这种行为并导致编译器错误?也许吧,这很难说,因为有些人可能依赖它并且从未遇到这类问题。

我不确定是否同意因为语言支持结构体嵌入就应该使用它。Go语言允许在当前作用域中重新定义nil的含义,但这并不意味着这是个好主意:

我在自己的程序中从不使用结构体嵌入,很可能是因为我也不像面向对象编程语言那样编写Go代码。虽然我有Ruby背景,曾经渴望类似继承的特性,但我已经意识到很少需要它。

问题出在Go语言的方法集规则和嵌入类型的方法提升机制上。当包名从app改为stu时,编译器对嵌入类型的方法提升处理发生了变化。

根本原因是:当BoyStudentGirlStudent嵌入app.Student时,如果包名相同,编译器会将嵌入类型的study()方法提升到外层类型,导致实际调用的是嵌入类型的study()方法而不是外层类型的study()方法。

以下是修复后的代码:

// app包中的代码保持不变
package app

import (
	"fmt"
)

type IStudent interface {
	study()
}

type Student struct {
}

func (me *Student) study() {
	fmt.Println("Student study")
}

type StudentProxy struct {
	student IStudent
}

func (me *StudentProxy) study() {
	me.student.study()
}

type studentManager struct {
	students map[string]*StudentProxy
	finder   func(string) IStudent
}

var sm = &studentManager{
	students: map[string]*StudentProxy{},
}

func StudentManager() *studentManager {
	return sm
}

func (me *studentManager) SetFinder(finder func(string) IStudent) {
	me.finder = finder
}

func (me *studentManager) GetStudent(name string) *StudentProxy {
	proxy, ok := me.students[name]
	if !ok {
		stu := me.finder(name)
		proxy = &StudentProxy{student: stu}
		me.students[name] = proxy
	}
	return proxy
}

func (me *studentManager) MethodA() {
	names := []string{"boy", "girl"}
	for _, name := range names {
		me.GetStudent(name).study()
	}
}

func (me *studentManager) MethodB() {
	names := []string{"boy", "girl"}
	for _, name := range names {
		me.GetStudent(name).study()
	}
}
// impl包中移除嵌入,直接实现接口
package impl

import (
	"fmt"

	"github.com/boringwork/godemo/interface/app"
)

type BoyStudent struct {
	// 移除嵌入:app.Student
}

func (me *BoyStudent) study() {
	fmt.Println("BoyStudent study")
}

type GirlStudent struct {
	// 移除嵌入:app.Student
}

func (me *GirlStudent) study() {
	fmt.Println("GirlStudent study")
}
// auto包保持不变
package auto

import (
	"github.com/boringwork/godemo/interface/app"
	"github.com/boringwork/godemo/interface/impl"
)

func Finder(name string) app.IStudent {
	switch name {
	case "boy":
		return &impl.BoyStudent{}
	case "girl":
		return &impl.GirlStudent{}
	}
	return nil
}
// main包保持不变
package main

import (
	"github.com/boringwork/godemo/interface/app"
	"github.com/boringwork/godemo/interface/auto"
)

func main() {
	app.StudentManager().SetFinder(auto.Finder)
	app.StudentManager().MethodA()
}

关键修改是在impl包中移除了对app.Student的嵌入。这样BoyStudentGirlStudent直接实现IStudent接口,不再继承嵌入类型的方法,确保了无论包名如何变化,都会调用正确的study()方法。

现在无论包名是app还是stu,输出都会是:

BoyStudent study
GirlStudent study
回到顶部