前文

  • golang快速入门[1]-go语言导论

  • golang快速入门[2.1]-go语言开发环境配置-windows

  • golang快速入门[2.2]-go语言开发环境配置-macOS

  • golang快速入门[2.3]-go语言开发环境配置-linux

  • golang快速入门[3]-go语言helloworld

  • golang快速入门[4]-go语言如何编译为机器码

  • golang快速入门[5.1]-go语言是如何运行的-链接器

  • golang快速入门[5.2]-go语言是如何运行的-内存概述

  • golang快速入门[5.3]-go语言是如何运行的-内存分配

  • golang快速入门[6.1]-集成开发环境-goland详解

  • golang快速入门[6.2]-集成开发环境-emacs详解

  • golang快速入门[7.1]-项目与依赖管理-gopath

  • golang快速入门[7.2]-北冥神功—go module绝技

  • golang快速入门[8.1]-变量类型、声明赋值、作用域声明周期与变量内存分配

前言

  • 在上文中我们学习了变量的各种概念和go语言中的类型系统

  • 我们将在本文中学习到:

  • 什么是自动类型推断

  • 为什么需要自动类型推断

  • go语言中自动类型推断的特点与陷阱

  • go语言在编译时是如何进行自动类型推断的

类型推断(Type inference)

  • 类型推断是编程语言在编译时自动解释表达式数据类型的能力,通常在函数式编程的语言(例如Haskell)中存在,类型推断的优势主要在于可以省略类型,这使编程任务更加容易。

  • 明确的指出变量的类型在编程语言中很常见,编译器在多大程度上可以做到这一点,因语言而异。例如,某些编译器可以推断出值:变量,函数参数和返回值。

  • go语言作为静态类型语言在编译时就需要知道变量的类型

类型推断的优势

  • 使编译器支持诸如类型推断之类的东西有两个主要的优势。首先,如果使用得当,它可以使代码更易读,例如,可以将如下C ++代码:

vector<int> v;
vector<int>::iterator itr = v.iterator();

变为:

vector<int> v;
auto itr = v.iterator();
  • 尽管在这里获得的收益似乎微不足道,但是如果类型更加复杂,则类型推断的价值变得显而易见。在许多情况下,这将使我们减少代码中的冗余信息。

  • 类型推断还用于其他功能,Haskell语言可以编写为:

succ x = x + 1
  • 上面的函数中,不管变量X是什么类型,加1并返回结果。

  • 尽管如此,显式的指出类型仍然有效,因为编译器可以更轻松地了解代码实际应执行的操作,不太可能犯任何错误。

go语言中的类型推断

如上所述,类型推断的能力每个语言是不相同的,在go语言中根据开发人员的说法,他们的目标是减少在静态类型语言中发现的混乱情况。他们认为许多像Java或C++这样的语言中的类型系统过于繁琐。

  • 因此,在设计Go时,他们从这些语言中借鉴了一些想法。这些想法之一是对变量使用简单的类型推断,给人以编写动态类型代码的感觉,同时仍然使用静态类型的好处

  • 如前所述,类型推断可以涵盖参数和返回值之类的内容,但是Go中没有

  • 在实践中,可以通过在声明新变量或常量时简单地忽略类型信息,或使用:=表示法来触发Go中的类型推断

  • 在Go中,以下三个语句是等效的:

var a int = 10
var a = 10
a := 10
  • Go的类型推断在处理包含标识符的推断方面是半完成的。本质上,编译器将不允许对从标识符引用的值进行强制类型转换,举几个例子:

  • 下面这段代码正常运行,并且a的类型为float64

a := 1 + 1.1
  • 下面的代码仍然正确,a会被推断为浮点数,1会变为浮点数与a的值相加

a := 1.1
b := 1 + a
  • 但是,下面代码将会错误,即a的值已被推断为整数,而1.1为浮点数,但是不能将a强制转换为浮点数,相加失败。编译器报错:constant 1.1 truncated to integer

a := 1
b := a + 1.1
  • 下面的类型会犯相同的错误,编译器提示:,invalid operation: a + b (mi *** atched types int and float64)

a := 1
b := 1.1
c := a + b

详细的实现说明

  • 在之前的这篇文章中(go语言如何编译为机器码),我们介绍了编译器执行的过程:词法分析 => 语法分析 => 类型检查 => 中间代码 => 代码优化 => 生成机器码

  • 编译阶段的代码位于go/src/cmd/compile文件中

词法分析阶段

  • 具体来说,在词法分析阶段,会将赋值右边的常量解析为一个未定义的类型,类型有如下几种:顾名思义,其中ImagLit代表复数,IntLit代表整数…

//go/src/cmd/compile/internal/syntax
const (
 IntLit LitKind = iota
 FloatLit
 ImagLit
 RuneLit
 StringLit
)
  • go语言源代码采用UTF-8的编码方式,在进行词法分析时当遇到需要赋值的常量操作时,会逐个的读取后面常量的UTF-8字符。字符串的首字符为",数字的首字母为'0'-'9'。实现函数位于:

// go/src/cmd/compile/internal/syntax

func (s *scanner) next() {
...
switch c {
    case '0''1''2''3''4''5''6''7''8''9':
        s.number(c)
    case '"':
        s.stdString()
    case '`':
        s.rawString()
    ...
  • 因此对于整数、小数等常量的识别就显得非常简单。具体来说,一个整数就是全是"0"-"9"的数字。一个浮点数就是字符中有"."号的数字,字符串就是首字符为"

  • 下面列出的函数为小数和整数语法分析的具体实现:

// go/src/cmd/compile/internal/syntax
func (s *scanner) number(c rune) {
    s.startLit()

    base := 10        // number base
    prefix := rune(0// one of 0 (decimal), '0' (0-octal), 'x', 'o', or 'b'
    digsep := 0       // bit 0: digit present, bit 1: '_' present
    invalid := -1     // index of invalid digit in literal, or < 0

    // integer part
    var ds int
    if c != '.' {
        s.kind = IntLit
        if c == '0' {
            c = s.getr()
            switch lower(c) {
            case 'x':
                c = s.getr()
                base, prefix = 16'x'
            case 'o':
                c = s.getr()
                base, prefix = 8'o'
            case 'b':
                c = s.getr()
                base, prefix = 2'b'
            default:
                base, prefix = 8'0'
                digsep = 1 // leading 0
            }
        }
        c, ds = s.digits(c, base, &invalid)
        digsep |= ds
    }

    // fractional part
    if c == '.' {
        s.kind = FloatLit
        if prefix == 'o' || prefix == 'b' {
            s.error("invalid radix point in " + litname(prefix))
        }
        c, ds = s.digits(s.getr(), base, &invalid)
        digsep |= ds
    }
...
  • 我们以赋值操作a := 333为例, 当完成词法分析时, 此赋值语句用AssignStmt表示。

    AssignStmt struct {
        Op       Operator // 0 means no operation
        Lhs, Rhs Expr     // Rhs == ImplicitOne means Lhs++ (Op == Add) or Lhs-- (Op == Sub)
        simpleStmt
    }
  • 其中Op代表操作符,在这里是赋值操作,Lhs与Rhs分别代表左右两个表达式,左边代表了变量a,右边代表了整数333,此时右边整数的类型为intLit

抽象语法树阶段

  • 接着生成在抽象语法树AST时, 会将词法分析的AssignStmt解析变为一个ode,Node结构体是对于抽象语法树中节点的抽象。

type Node struct {
    // Tree structure.
    // Generic recursive walks should follow these fields.
    Left  *Node
    Right *Node
    Ninit Nodes
    Nbody Nodes
    List  Nodes
    Rlist Nodes
    E   interface{} // Opt or Val, see methods below
    ...
  • 仍然是Left左节点代表了左边的变量a,Right右节点代表了整数333

  • 此时在E接口中,Right右节点会存储值333,类型为mpint。mpint用于存储整数常量

  • 具体的代码如下,如果为IntLit类型,转换为Mpint类型,其他类型类似。

  • 但是注意,此时左边的节点还是没有任何类型的。

// go/src/cmd/compile/internal/gc
func (p *noder) basicLit(lit *syntax.BasicLit) Val {
    // TODO: Don't try to convert if we had syntax errors (conversions may fail).
    //       Use dummy values so we can continue to compile. Eventually, use a
    //       form of "unknown" literals that are ignored during type-checking so
    //       we can continue type-checking w/o spurious follow-up errors.
    switch s := lit.Value; lit.Kind {
    case syntax.IntLit:
        checkLangCompat(lit)
        x := new(Mpint)
        x.SetString(s)
        return Val{U: x}

    case syntax.FloatLit:
        checkLangCompat(lit)
        x := newMpflt()
        x.SetString(s)
        return Val{U: x}
  • 如下Mpint类型的结构,我们可以看到AST阶段整数存储通过math/big.int进行高精度存储。

// Mpint represents an integer constant.
type Mpint struct {
    Val  big.Int
    Ovf  bool // set if Val overflowed compiler limit (sticky)
    Rune bool // set if syntax indicates default type rune
}
  • 最后在抽象语法树进行类型检查的阶段,会完成最终的赋值操作。将右边常量的类型赋值给左边变量的类型。

  • 最终具体的函数位于typecheckas,将右边的类型赋值给左边

func typecheckas(n *Node) {
...
if n.Left.Name != nil && n.Left.Name.Defn == n && n.Left.Name.Param.Ntype == nil {
        n.Right = defaultlit(n.Right, nil)
        n.Left.Type = n.Right.Type
    }
}
...
  • mpint类型对应的为CTINT标识。如下所示,前一阶段不同类型对应不同的标识。最终左边的变量存储的类型会变为types.Types[TINT]

func (v Val) Ctype() Ctype {
 switch x := v.U.(type) {
 default:
  Fatalf("unexpected Ctype for %T", v.U)
  panic("unreachable")
 case nil:
  return 0
 case *NilVal:
  return CTNIL
 case bool:
  return CTBOOL
 case *Mpint:
  if x.Rune {
   return CTRUNE
  }
  return CTINT
 case *Mpflt:
  return CTFLT
 case *Mpcplx:
  return CTCPLX
 case string:
  return CTSTR
 }
}
  • types.Types是一个数组,存储了不同标识对应的go语言中的实际类型。

var Types [NTYPE]*Type
  • Type是go语言中类型的存储结构,types.Types[TINT]最终代表的类型为int类型。其结构如下:

// A Type represents a Go type.
type Type struct {
    Extra interface{}

    // Width is the width of this Type in bytes.
    Width int64 // valid if Align > 0

    methods    Fields
    allMethods Fields

    Nod  *Node // canonical OTYPE node
    Orig *Type // original type (type literal or predefined type)

    // Cache of composite types, with this type being the element type.
    Cache struct {
        ptr   *Type // *T, or nil
        slice *Type // []T, or nil
    }

    Sym    *Sym  // symbol containing name, for named types
    Vargen int32 // unique name for OTYPE/ONAME

    Etype EType // kind of type
    Align uint8 // the required alignment of this type, in bytes (0 means Width and Align have not yet been computed)

    flags bitset8
}
  • 最后,我们可以用下面的代码来验证类型,输出结果为:int

a :=  333
fmt.Printf("%T",a)

总结

  • 在本文中,我们介绍了自动类型推断的内涵以及其意义。同时,我们用例子指出了go语言中自动类型推断的特点。

  • 最后,我们用a:=333为例,介绍了go语言在编译时是如何进行自动类型推断的。

  • 具体来说,go语言在编译时涉及到词法分析和抽象语法树阶段。对于数字的处理首先采用了math包中进行了高精度的处理,接着会转换为go语言中的标准类型,int或float64.在本文中没有对字符串等做详细介绍,留给以后的文章。

  • see you~

参考资料

  • 项目链接

  • 作者知乎

  • blog

  • Type inference

  • Rob Pike:Less is exponentially more

  • Type inference for go