golang路上的小学生系列--使用reflect查找package路径

本文也发布在gitbook

今日看到了一个有趣的golang项目--kolpa(https://github.com/malisit/kolpa)。 这个项目可以用来生成伪造的姓名,地址,时间,User-Agent等等信息,在需要大量随机数据的测试环境中非常合适。

点击fork之后,放在本地环境中build,run结果失败。运行项目中提供的demo也失败,按道理来说官方提供的demo应该都会成功,而且自己也没有修改任何一行代码,失败是不科学的。

所以只能剖解代码,查找失败原因。

一查不知道,原来此项目需要依赖各个语言环境下的模板文件,而模板文件都放在项目的data目录中。 在代码中,通过硬编码来确定模板文件位置:

// Reads the file "fName" and returns its content as a slice of strings.
func (g *Generator) fileToSlice(fName string) ([]string, error) {  
var res []string  
path := os.Getenv("GOPATH") + "/src/github.com/malisit/kolpa/data/" + g.Locale + "/" + fName  
file, err := os.Open(path)

if err != nil {  
return nil, err  
}
defer file.Close()

scanner := bufio.NewScanner(file)  
for scanner.Scan() {  
res = append(res, scanner.Text())  
}

if err := scanner.Err(); err != nil {  
//log.Println("Inteded generation is not valid for selected language. Switching to en_US.")
return g.fileToSlice(fName)  
}

return res, nil  
}

因为我是通过fork,然后clone到本地的方式来运行demo,本地此时packge路径已经不是上面的路径了,所以导致运行失败。 很难说这是一个bug,但的确影响到了程序运行。 所以说不良的代码风格更为恰当吧。

既然找到了问题,那下一步就是如何解决问题。 应该如何在Runtime时实时获取package位置呢?

说到Runtime,那么一定就少不了Reflect package。

Package reflect implements run-time reflection, allowing a program to manipulate objects with arbitrary types. The typical use is to take a value with static type interface{} and extract its dynamic type information by calling TypeOf, which returns a Type.  

从Reflect的介绍上看,Reflect package推荐的使用方式是通过TypeOf返回一个带有interface{}所有动态类型信息的Type类型,然后通过Type类型来获取各种程序需要的信息。

type Type interface {

// Align returns the alignment in bytes of a value of
// this type when allocated in memory.
Align() int

// FieldAlign returns the alignment in bytes of a value of
// this type when used as a field in a struct.
FieldAlign() int

// Method returns the i'th method in the type's method set.
// It panics if i is not in the range [0, NumMethod()).
//
// For a non-interface type T or *T, the returned Method's Type and Func
// fields describe a function whose first argument is the receiver.
//
// For an interface type, the returned Method's Type field gives the
// method signature, without a receiver, and the Func field is nil.
Method(int) Method

// MethodByName returns the method with that name in the type's
// method set and a boolean indicating if the method was found.
//
// For a non-interface type T or *T, the returned Method's Type and Func
// fields describe a function whose first argument is the receiver.
//
// For an interface type, the returned Method's Type field gives the
// method signature, without a receiver, and the Func field is nil.
MethodByName(string) (Method, bool)

// NumMethod returns the number of exported methods in the type's method set.
NumMethod() int

// Name returns the type's name within its package.
// It returns an empty string for unnamed types.
Name() string

// PkgPath returns a named type's package path, that is, the import path
// that uniquely identifies the package, such as "encoding/base64".
// If the type was predeclared (string, error) or unnamed (*T, struct{}, []int),
// the package path will be the empty string.
PkgPath() string

// Size returns the number of bytes needed to store
// a value of the given type; it is analogous to unsafe.Sizeof.
Size() uintptr

// String returns a string representation of the type.
// The string representation may use shortened package names
// (e.g., base64 instead of "encoding/base64") and is not
// guaranteed to be unique among types. To test for type identity,
// compare the Types directly.
String() string

// Kind returns the specific kind of this type.
Kind() Kind

// Implements reports whether the type implements the interface type u.
Implements(u Type) bool

// AssignableTo reports whether a value of the type is assignable to type u.
AssignableTo(u Type) bool

// ConvertibleTo reports whether a value of the type is convertible to type u.
ConvertibleTo(u Type) bool

// Comparable reports whether values of this type are comparable.
Comparable() bool

// Bits returns the size of the type in bits.
// It panics if the type's Kind is not one of the
// sized or unsized Int, Uint, Float, or Complex kinds.
Bits() int

// ChanDir returns a channel type's direction.
// It panics if the type's Kind is not Chan.
ChanDir() ChanDir

// IsVariadic reports whether a function type's final input parameter
// is a "..." parameter. If so, t.In(t.NumIn() - 1) returns the parameter's
// implicit actual type []T.
//
// For concreteness, if t represents func(x int, y ... float64), then
//
//    t.NumIn() == 2
//    t.In(0) is the reflect.Type for "int"
//    t.In(1) is the reflect.Type for "[]float64"
//    t.IsVariadic() == true
//
// IsVariadic panics if the type's Kind is not Func.
IsVariadic() bool

// Elem returns a type's element type.
// It panics if the type's Kind is not Array, Chan, Map, Ptr, or Slice.
Elem() Type

// Field returns a struct type's i'th field.
// It panics if the type's Kind is not Struct.
// It panics if i is not in the range [0, NumField()).
Field(i int) StructField

// FieldByIndex returns the nested field corresponding
// to the index sequence. It is equivalent to calling Field
// successively for each index i.
// It panics if the type's Kind is not Struct.
FieldByIndex(index []int) StructField

// FieldByName returns the struct field with the given name
// and a boolean indicating if the field was found.
FieldByName(name string) (StructField, bool)

// FieldByNameFunc returns the struct field with a name
// that satisfies the match function and a boolean indicating if
// the field was found.
//
// FieldByNameFunc considers the fields in the struct itself
// and then the fields in any anonymous structs, in breadth first order,
// stopping at the shallowest nesting depth containing one or more
// fields satisfying the match function. If multiple fields at that depth
// satisfy the match function, they cancel each other
// and FieldByNameFunc returns no match.
// This behavior mirrors Go's handling of name lookup in
// structs containing anonymous fields.
FieldByNameFunc(match func(string) bool) (StructField, bool)

// In returns the type of a function type's i'th input parameter.
// It panics if the type's Kind is not Func.
// It panics if i is not in the range [0, NumIn()).
In(i int) Type

// Key returns a map type's key type.
// It panics if the type's Kind is not Map.
Key() Type

// Len returns an array type's length.
// It panics if the type's Kind is not Array.
Len() int

// NumField returns a struct type's field count.
// It panics if the type's Kind is not Struct.
NumField() int

// NumIn returns a function type's input parameter count.
// It panics if the type's Kind is not Func.
NumIn() int

// NumOut returns a function type's output parameter count.
// It panics if the type's Kind is not Func.
NumOut() int

// Out returns the type of a function type's i'th output parameter.
// It panics if the type's Kind is not Func.
// It panics if i is not in the range [0, NumOut()).
Out(i int) Type  
// contains filtered or unexported methods
}

在Type接口定义中,和Package Path有关的有两个函数: * PkgPath() * String()

PkgPath 返回指定类型的import package path,也就是说,如果代码中有import encoding/base64这样的语句, 那么通过PkgPath()就会返回encoding/base64,而不是base64package所在的实际路径。 反言之,PkgPath()返回的是import package path。

而如果继续使用上例中的encoding/base64来说,String()返回的是base64,而不是encoding/base64。 String()返回的是实际使用的package name。

所以总结如下: * PkgPath() 返回import package path * String() 返回import package name

显然,PkgPath()更适合我们的需求。 简单改造代码如下:

1.增加 Pkg path

// Generator struct to access various generator functions
type Generator struct {  
Locale string  
Pkg string  
}

2.获取Pkg

// C is the creator function, initiates kolpa with or without locale
// setting. The default locale setting is "en_US".
// Returns a generator type that will be used to call generator methods.
func C(localeVar ...string) Generator {  
newGenerator := Generator{}  
if len(localeVar) > 0 {  
newGenerator.Locale = localeVar[0]  
} else {
newGenerator.Locale = "en_US"  
}
// newGenerator.populateFunctions()
newGenerator.Pkg = reflect.TypeOf(newGenerator).PkgPath()  
return newGenerator  
}

3.替换硬编码

// Reads the file "fName" and returns its content as a slice of strings.
func (g *Generator) fileToSlice(fName string) ([]string, error) {  
var res []string  
path := os.Getenv("GOPATH") + "/src/" + g.Pkg + "/data/" + g.Locale + "/" + fName  
file, err := os.Open(path)

if err != nil {  
return nil, err  
}
defer file.Close()

scanner := bufio.NewScanner(file)  
for scanner.Scan() {  
res = append(res, scanner.Text())  
}

if err := scanner.Err(); err != nil {  
//log.Println("Inteded generation is not valid for selected language. Switching to en_US.")
return g.fileToSlice(fName)  
}

return res, nil  
}

4.运行demo

⋊> ~/S/g/g/s/t/kopla ./kopla
石洁玉
幸和平

完美生成两个随机姓名,话说"和平"也是一代人经常起的名字。

最后提一句,有的地方曾经说到尽量少使用Reflect package。 因此反射使用多了,影响效率。 我想这种想法应该是从Java VM那里流传出来的吧。 因为Java VM负责将字节码翻译成机器码,因此频繁调用反射会加重VM切换上下文的代价,也就是把装载期做的事情搬到了运行期,势必降低运行效率。 golang的反射减少了翻译环节,同时借助于编译器进行了代码优化,虽然同样在反射时需要进行额外的安全检查和类型检查,但不会降低太多效率。 而且上面,我们也看到只有在调用时也只发生了一次反射调用,影响几乎可忽略不计。