『Go』const常量与内联函数

我想定义一个像这样的常量:

1
2
3
4
const Code = map[string]interface{}{
	"a": "123",
	"b": "456",
}

发现报错了:

Const initializer ‘map[string]interface{}{…}’ is not a constant

而当我这样定义时:

1
2
3
4
5
6
7
const Len = len("qwe")
const Len2 = length("qwe")
//length("qwe") (value of type int) is not constant

func length(a string) int {
	return len(a) 
}

发现 Len 不会报错,而 Len2 却会报错。那 Golang 中什么才可以定义为常量呢?

原来 Golang 中的常量是编译期常量,在编译时就会完全确定取值,并且只能是布尔型数字型(整数型、浮点型和复数)字符串型

所以不能将函数的返回值赋给常量(函数调用发生在运行时期),文章开头的 map 也不能定义为常量

相比之下,Java 中声明为 final 类型的基本类型或声明为 String 类型并直接赋值(非运算)的变量和 JavaScript 中的 const 代表的是一次性赋值的变量,本质上还是变量,只是不允许再做修改

{% note tip tip %} 这里可以用锁 {% endnote %}

可是 len() 为什么可以呢,难道这个函数就不是函数了吗?

一番搜索后我看到了一个词:内联函数

内联函数

什么是内联函数

{% note info 内联函数 %}

在计算机科学中,内联函数(有时称作在线函数编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展)。

引自内联函数_百度百科

{% endnote %}

原来 len() 是一个内联函数,既然他是编译时期展开函数,那就没问题了

学过 C 语言的应该对内联函数不陌生,在 C 语言中有一个关键字 inline , 使用 inline 修饰的函数就是内联函数。比如:

1
2
3
4
5
6
#include <stdio.h>
inline int add(int a, int b) { return a + b; }

int main() {
    printf("%d", add(1, 2)); // 3
}

这段代码中 int add(int a, int b) 使用了 inline 进行修饰,那代码在执行的时候就会变成下面这样:

1
printf("%d", 1 + 2);

这样可以避免频繁调用函数时栈内存重复开辟所带来的消耗,一般用于这种能够快速执行的短小的函数,因为在这种情况下函数调用的时间消耗显得更为突出


我们再来运行一下这段代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

const Len = len("qwe")
const hello = "Hello World"

func length(a string) int {
	return len(a)
}

func main() {
	fmt.Println(length("qwe"))
	fmt.Println(hello)
}

执行 go run --gcflags=-m .\main.go ,输出如下:

# command-line-arguments .\main.go:8:6: can inline length .\main.go:13:20: inlining call to length
.\main.go:13:13: inlining call to fmt.Println .\main.go:14:13: inlining call to fmt.Println .\main.go:8:13: a does not escape .\main.go:13:20: length(“qwe”) escapes to heap .\main.go:13:13: []interface {}{…} does not escape .\main.go:14:13: hello escapes to heap .\main.go:14:13: []interface {}{…} does not escape :1: .this does not escape :1: .this does not escape 3 Hello World

可以看到,length 可以作为内联函数,main 不可以,而 fmt.Println 也是一个内联函数。我们再来这样跑一下:go run --gcflags="-m -m" .\main.go ,注意中间加了一个 -m ,得到输出如下,可以看到更详细地信息,也可以看到 fmt.Printlnlength 都被展开为了怎样的形式:

{% fold %}

#command-line-arguments .\main.go:8:6: can inline length with cost 3 as: func(string) int { return len(a) } .\main.go:12:6: cannot inline main: function too complex: cost 157 exceeds budget 80 .\main.go:13:20: inlining call to length func(string) int { return len(a) } .\main.go:13:13: inlining call to fmt.Println func(…interface {}) (int, error) { var fmt..autotmp_3 int; fmt..autotmp_3 = ; var fmt..autotmp_4 error; fmt..autotmp_4 = ; fmt..autotmp_3, fmt..autotmp_4 = fmt.Fprintln(io.Writer(os.Stdout), fmt.a…); return fmt..autotmp_3, fmt..autotmp_4 } .\main.go:14:13: inlining call to fmt.Println func(…interface {}) (int, error) { var fmt..autotmp_3 int; fmt..autotmp_3 = ; var fmt..autotmp_4 error; fmt..autotmp_4 = ; fmt..autotmp_3, fmt..autotmp_4 = fmt.Fprintln(io.Writer(os.Stdout), fmt.a…); return fmt..autotmp_3, fmt..autotmp_4 } .\main.go:8:13: a does not escape .\main.go:14:13: hello escapes to heap: .\main.go:14:13: flow: ~arg0 = &{storage for hello}: .\main.go:14:13: from hello (spill) at .\main.go:14:13 .\main.go:14:13: from ~arg0 := hello (assign-pair) at .\main.go:14:13 .\main.go:14:13: flow: {storage for []interface {}{…}} = ~arg0: .\main.go:14:13: from []interface {}{…} (slice-literal-element) at .\main.go:14:13 .\main.go:14:13: flow: fmt.a = &{storage for []interface {}{…}}: .\main.go:14:13: from []interface {}{…} (spill) at .\main.go:14:13 .\main.go:14:13: from fmt.a = []interface {}{…} (assign) at .\main.go:14:13 .\main.go:14:13: flow: {heap} = *fmt.a: .\main.go:14:13: from fmt.Fprintln(io.Writer(os.Stdout), fmt.a…) (call parameter) at .\main.go:14:13 .\main.go:13:20: length(“qwe”) escapes to heap: .\main.go:13:20: flow: ~arg0 = &{storage for length(“qwe”)}: .\main.go:13:20: from length(“qwe”) (spill) at .\main.go:13:20 .\main.go:13:20: from ~arg0 := length(“qwe”) (assign-pair) at .\main.go:13:13 .\main.go:13:20: flow: {storage for []interface {}{…}} = ~arg0: .\main.go:13:20: from []interface {}{…} (slice-literal-element) at .\main.go:13:13 .\main.go:13:20: flow: fmt.a = &{storage for []interface {}{…}}: .\main.go:13:20: from []interface {}{…} (spill) at .\main.go:13:13 .\main.go:13:20: from fmt.a = []interface {}{…} (assign) at .\main.go:13:13 .\main.go:13:20: flow: {heap} = *fmt.a: .\main.go:13:20: from fmt.Fprintln(io.Writer(os.Stdout), fmt.a…) (call parameter) at .\main.go:13:13 .\main.go:13:20: length(“qwe”) escapes to heap .\main.go:13:13: []interface {}{…} does not escape .\main.go:14:13: hello escapes to heap .\main.go:14:13: []interface {}{…} does not escape :1: .this does not escape :1: .this does not escape 3 Hello World

{% endfold %}

无类型常量

1
2
const hello = "Hello World"
//const hello untyped string = "Hello World"

我们发现,当我们这样定义一个字符串常量时,它的类型是 untyped string

原来 Go 中有六种未明确类型的常量类型,分别是

  • 无类型的布尔型
  • 无类型的整形
  • 无类型的字符型
  • 无类型的浮点型
  • 无类型的复数
  • 无类型的字符串

编译器会为这些没有明确基础类型的常量提供比基础类型更高精度的算术运算,赋值给特定类型时会有影响

Licensed under CC BY-NC-SA 4.0