欢迎访问我的博客,目前从事Machine Learning,欢迎交流

【读书笔记&个人心得】第4章:语言的核心结构与技术

文件名、关键字与标识符

文件名

不支持大小驼峰,请使用 小写字母+下划线
文件名均由小写字母组成,如 scanner.go 。如果文件名由多个部分组成,则使用下划线 _ 对它们进行分隔,如 scanner_test.go 。文件名不包含空格或其他特殊字符。

源文件大小

一个源文件可以包含任意多行的代码

标识符

标识符的注意实现

Go 语言也是区分大小写的
Go 代码中的几乎所有东西都有一个名称或标识符,有效的标识符必须以字母(可以使用任何 UTF-8 编码的字符或 _)开头,然后紧跟着 0 个或多个字符或 Unicode 数字,如:X56、group1、_x23、i、өԑ12

以下是无效的标识符:

1ab(以数字开头)
case(Go 语言的关键字)
a+b(运算符是不允许的)

_ 标识符

_ 本身就是一个特殊的标识符,被称为空白标识符。
它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。(那 _ 有啥用)

匿名变量

在编码过程中,你可能会遇到没有名称的变量、类型或方法。虽然这不是必须的,但有时候这样做可以极大地增强代码的灵活性,这些变量被统称为匿名变量。

分隔符

括号 ()、中括号 [] 和大括号 {}

使用分号

每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。
如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

程序的基本结构和要素

hello_world.go

package main

import "fmt"

func main() {
	fmt.Println("hello, world")
}

包的概念、导入与可见性

你必须在源文件中非注释的第一行指明这个文件属于哪个包
每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成

main 包

package main 表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包
一个应用程序可以包含不同的包,只有属于 main 包的源文件才会被编译为可执行程序,非 main 包,如 pack1,编译后产生的对象文件将会是 pack1.a 而不是可执行程序

所有的包名都应该使用小写字母

标准库

在 Go 的安装文件里包含了一些可以直接使用的包,即标准库
一般情况下,标准包会存放在

$GOROOT/pkg/$GOOS_$GOARCH/目录下,如 C:\Program Files\Go\pkg\windows_amd64

编译

如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序

属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包(一个目录只能包含 1 个文件夹,但是可以包含多个源文件,或者 1 个文件夹+多个源文件混)

如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译

Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 .o 的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。
如果 A.go 依赖 B.go,而 B.go 又依赖 C.go:
编译 C.go, B.go, 然后是 A.go,为了编译 A.go,编译器读取的是 B.o 而不是 C.o

go module

go module 是 go 官方自带的 go 依赖管理库,在 1.13 版本正式推荐使用
go module 可以将某个项目(文件夹)下的所有依赖整理成一个 go.mod 文件,里面写入了依赖的版本等 使用 go module 之后我们可不用将代码放置在 src 下
使用 go module 管理依赖后会在项目根目录下生成两个文件 go.mod(会记录当前项目的所依赖)和 go.sum(记录每个依赖库的版本和哈希值)

参考:https://blog.csdn.net/longgeaisisi/article/details/121288696

导入包

fmt 包实现了格式化 IO(输入/输出)的函数
其中,Printf()除了字符串可以直接输出外,其他都要格式符

导入多个包

当你导入多个包时,最好按照字母顺序排列包名,这样做更加清晰易读

import "fmt"
import "os"

import (
   "fmt"
   "os"
)

相对/绝对路径

提示:不建议使用相对路径

如果包名不是以 . 或 / 开头,如 "fmt" 或者 "container/list",则 Go 会在全局文件进行查找;如果包名以 ./ 开头,则 Go 会在相对目录中查找;如果包名以 / 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找

以相对路径在 GOPATH 下导入包会产生报错信息,在 GOPATH 外可以以相对路径的形式执行 go build(go install 不可以)

可见性规则

导入包即等同于包含了这个包的所有的代码对象。除了符号 _,包中所有代码对象的标识符必须是唯一的,但是相同的标识符可以在不同的包中使用,因为可以使用包名来区分它们。

Go 中的 public 和 private

当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是它们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。

同名冲突的处理

假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:pack1.Thing(pack1 在这里是不可以省略的)。

标识符冲突,通过包名区分
例如 pack1.Thing 和 pack2.Thing

包名冲突,通过别名区分

package main

import fm "fmt" // alias3

func main() {
   fm.Println("hello, world")
}

如果你导入了一个包却没有使用它,则会在构建程序时引发错误

全局变量的作用域

你可以在使用 import 导入包之后定义或声明 0 个或多个常量 (const)、变量 (var) 和类型 (type)

这些对象的作用域都是全局的(在本包范围内),所以可以被本包中所有的函数调用(如 gotemplate.go 源文件中的 c 和 v),然后声明一个或多个函数 (func)。

(书写顺序是:package 最先写,import 其次写,定义常/变/类 再其次写,函数定义再再其次写)

函数

main

一个可执行程序,如果有 init() 函数则会先执行,否则先执行 main()
main() 函数既没有参数,也没有返回类型

编写规则

左大括号 { 必须与方法的声明放在同一行
右大括号 } 需要被放在紧接着函数体的下一行

func functionName(parameter_list) (return_value_list) {
   …
}

parameter_list 的形式为 (param1 type1, param2 type2, …)
return_value_list 的形式为 (ret1 type1, ret2 type2, …)

param1--parameter
ret1--return

函数体很短时也可以

func Sum(a, b int) int { return a + b }

if 语句括号写法同上

导出函数

当某个函数需要被外部包调用的时候用大驼峰,否则用小驼峰

打印函数

将字符串输出到控制台
fmt.Println("hello, world")相当于 fmt.Print("hello, world\n")

print、println 是预定义的方法,只可以用于调试阶段,在部署程序的时候务必将它们替换成 fmt 中的相关函数

程序正常/异常退出

程序正常退出的代码为 0 即 Program exited with code 0;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序

注释

主要分为导出注释和包注释
注释不会被编译,但可以通过 godoc 来使用

每一个包应该有相关注释,在 package 语句之前的块注释将被默认认为是这个包的文档说明,其中应该提供对整体功能做简要的介绍

// Package superman implements methods for saving the world.
//
// Experience has shown that a small number of procedures can prove
// helpful when attempting to save the world.
package superman

全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释


// enterOrbit是用来 xxx
func enterOrbit() error {
   ...
}

类型

型定义了某个变量的值的集合与可对其进行操作的集合--简称定义了值和操作

var

使用 var 声明的变量的值会自动初始化为该类型的零值
基本类型,如:int、float、bool、string;
结构化的(复合的),如:struct、array、切片 (slice)、map、通道 (channel);
只描述类型的行为的,如:interface

结构化的类型没有真正的值,它使用 nil(相当于 C 的 null) 作为默认值

var i int // 自动0值
var i2 int = 1

将函数作为返回值

函数也可以是一个确定的类型,就是以函数作为返回类型

func Add2() func(b int) int {
    return func(b int) int {
        return b + 2
    }
}

函数多返回值

func Atoi(s string) (i int, err error)

返回的形式
return var1, var2

自定义类型

使用 type 关键字可以定义你自己的类型,你可能想要定义一个结构体(第 10 章),但是也可以定义一个已经存在的类型的别名

type IZ int
var a IZ = 5

这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。
这里我们可以看到 int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能(第 4.2.6 节)

单次定义多个类型

type (
   IZ int
   FZ float64
   STR string
)

Go 程序的一般结构

下面的程序可以被顺利编译但什么都做不了,不过这很好地展示了一个 Go 程序的首选结构

package main

import (
"fmt"
)

const c = "C"

var v int = 5

type T struct{}

func init() { // initialization of package
}

func main() {
var a int
Func1()
// ...
fmt.Println(a)
}

func (t T) Method1() {
//...
}

func Func1() { // exported function Func1
//...
}

Go 程序的执行(程序启动)顺序如下:

  1. 按顺序导入所有被 main 包引用的其它包,然后在每个包中执行如下流程:
  2. 如果该包又导入了其它的包,则从第一步开始递归执行,但是每个包只会被导入一次。
  3. 然后以相反的顺序在每个包中初始化常量和变量,如果该包含有 init() 函数的话,则调用该函数。
  4. 在完成这一切之后,main 也执行同样的过程,最后调用 main() 函数开始执行程序。
    (为什么要按相反的顺序初始化常量和变量?)

类型转换

Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明

写法

valueOfTypeB = typeB(valueOfTypeA)

a := 5.0
b := int(a)

精度丢失

从一个取值范围较小的类型转换到一个取值范围较大的类型可以转换成功(例如将 int16 转换为 int32)。当从一个取值范围较大的转换到取值范围较小的类型时,也可以转换成功(例如将 int32 转换为 int16 或将 float32 转换为 int),但是,会发生精度丢失(截断)

type 和转换

具有相同底层类型的变量之间可以相互转换

type IZ int

var a IZ = 5
c := int(a)
d := IZ(c)

Go 命名规范

Go 通过 gofmt(go format) 来强制实现统一的代码风格

  1. 名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。
  2. 返回 某个对象 的 函数或方法 的名称一般都是使用名词,不以 Get 之类的字符开头,如果是用于修改某个对象,则使用 SetName()。
  3. 可以使用大小驼峰即使用大小写,而不是使用下划线来分割多个名称。

常量

常量使用关键字 const 定义,用于存储不会改变的数据。
存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

const Pi = 3.14159

在 Go 语言中,你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。

显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"

根据上下文获得类型

未定义类型的常量会在必要时刻根据上下文来获得相关类型。

var n int
f(n + 5) // 无类型的数字型常量 “5” 它的类型在这里变成了 int

常量时编译时能确定

常量的值必须是能够在编译时就能够确定的,比如 getNumber()的内容是运行程序后用户输入的,将其赋给常量在编译时无法确定,那么就会报错。但是,内置函数可以使用,如:len()

正确的做法:const c1 = 2/3
错误的做法:const c2 = getNumber() // 引发构建错误: getNumber() used as value

数字型常量

数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出:

const Ln2 = 0.693147180559945309417232121458\
			176568075500134360255254120680009
const Log2E = 1/Ln2 // this is a precise reciprocal
const Billion = 1e9 // float constant
const hardEight = (1 << 100) >> 97

反斜杠 \ 可以在常量表达式中作为多行的连接符使用。
当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误

常量的并行赋值

const beef, two, c = "eat", 2, "veg"
const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6
const (
	Monday, Tuesday, Wednesday = 1, 2, 3
	Thursday, Friday, Saturday = 4, 5, 6
)

常量作枚举

const (
	Unknown = 0
	Female = 1
	Male = 2
)

iota

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1,并且没有赋值的常量默认会应用上一行的赋值表达式

const (
	a = iota
	b = iota
	c = iota
)
// 赋值一个常量时,之后没赋值的常量都会应用上一行的赋值表达式
const (
	a = iota  // a = 0
	b         // b = 1
	c         // c = 2
	d = 5     // d = 5
	e         // e = 5
)

// 赋值两个常量(使用并行赋值的方式),iota 只会增长一次,而不会因为使用了两次就增长两次
const (
	Apple, Banana = iota + 1, iota + 2 // Apple=1 Banana=2
	Cherimoya, Durian                  // Cherimoya=2 Durian=3
	Elderberry, Fig                    // Elderberry=3, Fig=4

)

// 使用 iota 结合 位运算 表示资源状态的使用案例
const (
	Open = 1 << iota  // 0001
	Close             // 0010
	Pending           // 0100
)

const (
	_           = iota             // 使用 _ 忽略不需要的 iota
	KB = 1 << (10 * iota)          // 1 << (10*1)
	MB                             // 1 << (10*2)
	GB                             // 1 << (10*3)
	TB                             // 1 << (10*4)
	PB                             // 1 << (10*5)
	EB                             // 1 << (10*6)
	ZB                             // 1 << (10*7)
	YB                             // 1 << (10*8)
)

译者注:关于 iota 的使用涉及到非常复杂多样的情况,这里作者解释的并不清晰,因为很难对 iota 的用法进行直观的文字描述。如希望进一步了解,请观看视频教程 《Go 编程基础》 第四课:常量与运算符

itoa 每遇到一次 const 关键字,iota 就重置为 0

变量

var identifier type 注意先写名再写类型,Go 为什么要选择这么做呢?
它是为了避免像 C 语言中那样含糊不清的声明形式,例如:

int* a, b;

在这个例子中,只有 a 是指针而 b 不是,下面的 Go 的声明方法,ab 都是指针

var a, b *int

自动初始化

当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float32(64) 为 0.0,bool 为 false,string 为空字符串,指针为 nil。

全局变量

如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用
全局变量用 var 声明

类型推断

Go 编译器可以根据变量的值来自动推断其类型

//var identifier [type] = value
var a int = 15

//推断
var b = false
var str = "Go says hello to the world!"

//错误,没有推断的依据
var a

我猜 Go 编译器应该是从小到大推,够用就好,所以有些情况还是不能用推断

var n int64 = 2

声明变量的写法

一说是全局用 var,函数用:=,一说是只声明用 var,声明并赋值用:=

:= 是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值(亲测可用)

并行赋值

不允许声明了变量但不使用。并行赋值除了赋值,还用于函数多返回值的接收
下面声明多个变量,赋值,使用

package main

import "fmt"

func main() {
	var a, b, c int
	a, b, c = 5, 7, 9
	str := fmt.Sprintf("%d:%d:%d", a, b, c)
	fmt.Println(str)
}

package main

import "fmt"

func main() {
	a, b, c := 5, 7, 9
	str := fmt.Sprintf("%d:%d:%d", a, b, c)
	fmt.Println(str)
}

值交换

a, b = b, a

抛弃值

空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。

运行时推断

不只是编译时推断,运行时也可

var (
	HOME = os.Getenv("HOME")
	USER = os.Getenv("USER")
	GOROOT = os.Getenv("GOROOT")
)
package main

import (
	"fmt"
   "runtime"
	"os"
)

func main() {
	var goos string = runtime.GOOS
	fmt.Printf("The operating system is: %s\n", goos)
	path := os.Getenv("PATH")
	fmt.Printf("Path is %s\n", path)
}

值类型和引用类型

程序中所用到的内存在计算机中使用一堆箱子来表示(这也是人们在讲解它的时候的画法),这些箱子被称为“字”(全部箱子一起成为字),有的系统字长 32 位(4 字节)有的 64 位(8 字节),所有的字都使用以十六进制数表示

补充:
计算机进行数据处理时,一次存取、加工和传送的数据长度称为字(word)。一个字通常由一个或多个(一般是字节的整数位)字节构成。例如 286 微机的字由 2 个字节组成,它的字长为 16;486 微机的字由 4 个字节组成,它的字长为 32 位机。

值类型

int、float、bool 和 string 这些基本类型都属于值类型,这些类型的变量直接指向内存中的值
数组(第 7 章)和结构(第 10 章)这些复合类型也是值类型
当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝(注意了,是拷贝值不是拷贝引用)

通过& 访问值变量的地址

引用类型

说白了,引用存的是值所在的地址
更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存
引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置

赋值语句 r2 = r1 时,只有引用(地址)被复制(拷贝地址而不是值)

打印

fmt 包

func Printf(format string, list of variables to be printed)

fmt.Printf 是格式化输出函数
%d 代表数值,%s 代表字符串标识符、%v 代表使用类型的默认输出格式的标识符、%T代表变量的类型

拼装字符串

fmt.Sprintf 与 Printf 的作用是完全相同的,不过前者将格式化后的字符串以返回值的形式返回给调用者

package main

import "fmt"

func main() {
	var a = "sss"
	var b = 111
	str := fmt.Sprintf("%s:%d", a, b) // sss:111
	fmt.Println(str)
}

init 函数

(全局)变量除了可以在全局声明中初始化,也可以在 init() 函数中初始化,执行优先级比 main() 函数高
每个源文件可以包含多个 init() 函数,同一个源文件中的 init() 函数会按照从上到下的顺序执行
下面程序输出 1 2 3

package main

import "fmt"

func init() {
	fmt.Println(1)
}

func init() {
	fmt.Println(2)
}

func main() {
	fmt.Println(3)
}

如果一个包有多个源文件包含 init() 函数的话,则官方鼓励但不保证以文件名的顺序调用(也就是不保证按源文件名顺序调用 init,可能顺序是乱的,起码源文件内顺序是可以保证的)。初始化总是以单线程并且按照包的依赖关系顺序执行。

package main

var a = "G"

func main() {
	n()
	m()
	n()
}

func n() { print(a) }

func m() {
	a := "O"
	print(a)
}

输出,GOG,因为在函数作用域内,函数内声明的变量优先级更高

package main

var a = "G"

func main() {
   n()
   m()
   n()
}

func n() {
   print(a)
}

func m() {
   a = "O"
   print(a)
}

输出,GOO

package main

var a string

func main() {
   a = "G"
   print(a)
   f1()
}

func f1() {
   a := "O"
   print(a)
   f2()
}

func f2() {
   print(a)
}

输出,GOG

基本类型和表达式

Go 是强类型语言,因此不会进行隐式转换,必须显示转换
不存在 C 那样的运算符重载(大概就是+碰到字符就是连接,碰到数字就是加和)

对于值之间的比较,必须类型相同,或者实现了相同的接口
如果一个值是常量,那么另一个值必须与该常量的类型相兼容

&& || 同样存在短路特性

布尔值变量命名要用 is 开头

数字类型范围

int、uint 范围根据系统变化,64 位操作系统就是使用 64 位二进制(8 个字节)
uintptr 长度被设定为足够存放一个指针即可
Go 中没有 float、double ,只有 float32 和 float64

与操作系统有关

int、uint

与操作系统无关

uintptr

整数:
int8(-128 -> 127)
int16(-32768 -> 32767)
int32(-2,147,483,648 -> 2,147,483,647)
int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)

无符号整数:
uint8(0 -> 255)
uint16(0 -> 65,535)
uint32(0 -> 4,294,967,295)
uint64(0 -> 18,446,744,073,709,551,615)

浮点型(IEEE-754 标准):
float32(+- 1e-45 -> +- 3.4 * 1e38)
float64(+- 5 * 1e-324 -> 107 * 1e308)

PS:表示范围

十进制小数转二进制小数:乘 2 取整:
0.125*2取0
0.25*2取0
0.5*2取1
得0.001

二进制小数转十进制小数:按位乘相加
0.001

0*1/2(2^-1)
0*1/4(2^-2)
1*1/8(2^-3)

0+0+1/8=0.125

https://zhuanlan.zhihu.com/p/339949186?ivk_sa=1024320u

V = (-1)^S _ M _ R^E
S:符号位,取值 0 或 1,决定一个数字的符号,0 表示正,1 表示负
M:尾数,用小数表示,例如前面所看到的 8.345 * 10^0,8.345 就是尾数
R:基数,表示十进制数 R 就是 10,表示二进制数 R 就是 2
E:指数,用整数表示,例如前面看到的 10^-1,-1 即是指数

总结就是,根据 IEEE-754 标准,只有只用 64 表示浮点数,那么表示范围是很有限的,如果是通过科学技术法表示,浮点数范围就可以很爆炸,因为指数爆炸,我们通过规划 1 个符号位,m 个尾数,n 个指数,且基数是直接系统默认的,由解读者控制,所以可以不存储,这样又省了一些位给其他部分,可以表示更大的精度范围。

浮点数精度(10 进制)

float32 精确到小数点后 7 位,float64 精确到小数点后 15 位(我的试验是点后16位),使用==和!=要注意精度(应该是指 10 进制的位数)

你应该尽可能地使用 float64,因为 math 包中所有有关数学运算的函数都会要求接收这个类型

数字类型 8 、16 、10 连乘

8 进制 0 开头:077
16 进制 0x 开头:0xFF
e 表示 10 连乘(几个 0):1e3 = 1000,或者 6.022e23 = 6.022 x 1e23

a := uint64(0) 来同时完成类型转换和赋值操作

只允许常量之间的混合使用

package main

func main() {
	var a int
	var b int32
	a = 15
	b = a + a	 // 编译错误
	b = b + 5    // 因为 5 是常量,所以可以通过编译
}

除非显式转换:

m = int32(n)

格式化说明符(很迷惑的东西)

%d 用于格式化整数(%x 和 %X 用于格式化 16 进制表示的数字)
%g 用于格式化浮点型(%f 输出浮点数,%e 输出科学计数表示法)
%0nd 用于规定输出长度为 n 的整数,其中开头的数字 0 是必须的。

%0nd

原数字短的会补够长度,原数字过长会照原数字输出

package main

import "fmt"

func main() {
	fmt.Printf("%03d", 13)
}

%n.mf

关于 %n.mf,n 是宽度,算上小数点,只加不扣,预设宽度不足时显示实际宽度,宽度要算上整数部分和小数部分,原数精度不足时补 0,原数精度太多直接截

package main

import  "fmt" // alias3

func main() {
   var a float32 = 1234.5678
   fmt.Printf("|%9.3g|%8.4f|\n",a, 31.45)// | 1.23e+03| 31.4500|
}

%n.me

关于 %n.me,n 是宽度,包括小数点、数组、e、+/-,m 是指数,m 是控制尾数的精度

package main

import (
	"fmt"
)

func main() {
	fmt.Printf("|%9.2e|", 3.14) // | 3.14e+00|
}

建议精度格式化用 strconv 包的函数
https://www.cnblogs.com/wukai66/p/12016862.html

FormatFloat()
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
函数将浮点数表示为字符串并返回。

bitSize 表示 f 的来源类型(32:float32、64:float64),会据此进行舍入。

fmt 表示格式:’f’(-ddd.dddd)、’b’(-ddddp±ddd,指数为二进制)、’e’(-d.dddde±dd,十进制指数)、’E’(-d.ddddE±dd,十进制指数)、’g’(指数很大时用’e’格式,否则’f’格式)、’G’(指数很大时用’E’格式,否则’f’格式)。

prec 控制精度(排除指数部分):对’f’、’e’、’E’,它表示小数点后的数字个数;对’g’、’G’,它控制总的数字个数。如果 prec 为-1,则代表使用最少数量的、但又必需的数字来表示 f。

数字值转换

有损失精度

丢失小数:
a32bitInt = int32(a32Float)
丢失精度:
从取值范围较大的类型转换为取值范围较小的类型

无损失精度

int --> uint8

func Uint8FromInt(n int) (uint8, error) {
	if 0 <= n && n <= math.MaxUint8 { // conversion is safe
		return uint8(n), nil
	}
	return 0, fmt.Errorf("%d is out of the uint8 range", n)
}

float64--->int32

func IntFromFloat64(x float64) int {
	if math.MinInt32 <= x && x <= math.MaxInt32 { // x lies in the integer range
		whole, fraction := math.Modf(x)
		if fraction >= 0.5 {// 四舍五入
			whole++
		}
		return int(whole)
	}
	panic(fmt.Sprintf("%g is out of the int32 range", x))
}

PS:molf 是一个返回整数部分和小数部分的函数
C 库函数 double modf(double x, double *integer) 返回值为小数部分(小数点后的部分),并设置 integer 为整数部分。由于 Go 支持多返回值,因此返回两个部分就方便多了

Go 复数

位运算

【注意】位运算只能用于整数类型的变量,且需当它们拥有等长位模式时

按位与 &
按位或 |
按位异或 ^:只加不进
位清除 &^:将指定位置上的值设置为 0

&^ 的指定位置使用权值来指定,权值为 4 的位置是右数第 3

 package main
 import "fmt"
 func main() {
 	var x uint8 = 15
 	var y uint8 = 4
 	fmt.Printf("%08b\n", x &^ y);  // 00001011
 }

一元运算符

按位补码^x

^x 其实就是按位取反,计算方法为把一个数转为二进制,然后对该二进制按位取反,接着把得到的数视为结果的补码(转成原码即可知晓结果)

补充:
其中,正数 原码反码补码;负数的反码为原码除符号位外取反,然后,再加 1

其实,规律很简单,从 10 进制来看,就是原数取相反数(加负号),再减一

package main

import "fmt"

func main() {
	fmt.Println(^9) // -10
	fmt.Println(^91) // -92
	fmt.Println(^-8) // 7
	fmt.Println(^-81) // 80
}

<< 位左移

整体向左移,空白用 0 补充

  1 << 10 // 等于 1 KB
  1 << 20 // 等于 1 MB
  1 << 30 // 等于 1 GB

位右移 >> 同左移,只是方向不同

下面使用左移实现存储单位比较妙,天然地支持 1024,而不用考虑 KB 是 1024 还是 1000。1 KB = 1 MB * 1024,而1024=2^10,即扩大1024倍,换成2进制就是 乘 2^10

type ByteSize float64 //用十进制来存二进制
const (
	_ = iota // 通过赋值给空白标识符来忽略值,iota由0开始
	KB ByteSize = 1<<(10*iota)
	MB
	GB
	TB
	PB
	EB
	ZB
	YB
)

在通讯中使用位左移表示标识的用例

type BitFlag int
const (
	Active BitFlag = 1 << iota // 1 << 0 == 1
	Send // 1 << 1 == 2
	Receive // 1 << 2 == 4
)

flag := Active | Send // == 3

算术运算符

关于 "+"

相对于一般规则而言,Go 在进行字符串拼接时允许使用对运算符 + 的重载,但 Go 本身不允许开发者进行自定义的运算符重载

浮点数除以 0.0 会返回一个无穷尽的结果,使用 +Inf 表示

运算符都可以简写

+= a、 -=、*=、/=、%=、a <<= 2 或者 b ^= a & 0xffffffff

++

对于++、--只能使用后缀,且只能作为语句(单成一行),不允许作为表达式,如 n = i++、f(i++) 或者 a[i]=b[i++]

数据溢出

在运算时 溢出 不会产生错误,Go 会简单地将超出位数抛弃。如果你需要范围无限大的整数或者有理数(意味着只被限制于计算机内存),你可以使用标准库中的 big 包,该包提供了类似 big.Int 和 big.Rat 这样的类型(第 9.4 节)

随机数

rand 包实现了伪随机数的生成

函数 rand.Float32 和 rand.Float64 返回介于 $[0.0, 1.0)$ 之间的伪随机数,其中包括 0.0 但不包括 1.0。函数 rand.Intn 返回介于 $[0, n)$ 之间的伪随机数。

你可以使用 rand.Seed(value) 函数来提供伪随机数的生成种子,一般情况下都会使用当前时间的纳秒级数字

以下程序要查看最新的 rand 包和 time 包,感觉可能有变

// You can edit this code!
// Click here and start typing.
package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	for i := 0; i < 10; i++ {
		a := rand.Int()
		fmt.Printf("%d / ", a)
	}
	for i := 0; i < 5; i++ {
		r := rand.Intn(8)
		fmt.Printf("%d / ", r)
	}
	fmt.Println()
	timens := int64(time.Now().Nanosecond())
	rand.Seed(timens)
	for i := 0; i < 10; i++ {
		fmt.Printf("%2.2f / ", 100*rand.Float32())
	}
}

运算符与优先级

二元运算符的运算方向均是从左至右
上至下代表优先级由高到低:

优先级 	运算符
 7 		^ !
 6 		* / % << >> & &^
 5 		+ - | ^
 4 		== != < <= >= >
 3 		<-
 2 		&&
 1 		||

使用括号来临时提升某个表达式的整体运算优先级

类型别名(感觉可读性不好)

package main
import "fmt"

type TZ int

func main() {
	var a, b TZ = 3, 4
	c := a + b
	fmt.Printf("c has the value: %d", c) // 输出:c has the value: 7
}

实际上,类型别名得到的新类型并非和原类型完全相同,新类型不会拥有原类型所附带的方法(第 10 章)

字符类型(不是一个类型)byte 和 ASCII

这并不是 Go 语言的一个类型,字符只是整数的特殊用例,byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题

//转义字符,\x 总是紧跟着长度为 2 的 16 进制数

package main

import "fmt"

func main() {
	var ch byte = '\173'
	var ch2 byte = '\174'
	var ch3 byte = '\x41'
	var ch4 byte = 65
	fmt.Printf("%c%c%c%c", ch, ch2, ch3, ch4)// {|AA
}


Unicode(UTF-8)

Go 同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int(而不是 uint8) 来表示,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。

rune == int32

其实 rune 也是 Go 当中的一个类型,并且是 int32 的别名。(utf8最长4个字节,即32位)

\u+xxxx

因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。

如果需要使用到 4 字节,则会加上 \U 前缀;
前缀 \u 则总是紧跟着长度为 4 的 16 进制数,前缀 \U 紧跟着长度为 8 的 16 进制数

var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3) // UTF-8 code point

输出

65 - 946 - 1053236
A - β - r
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234

%x 和 %X 用于格式化 16 进制表示的数字
%c 用于表示字符;当和字符配合使用时
%v 或 %d 会输出用于表示该字符(单个字符)的整数(10 进制),编码的整数,如对'a'输出97;
%U 输出格式为 U+hhhh 的字符串(另一个示例见第 5.4.4 节)

unicode 包

判断是否为字母:unicode.IsLetter(ch)
判断是否为数字:unicode.IsDigit(ch)
判断是否为空白符号:unicode.IsSpace(ch)

这些函数返回单个布尔值。包 utf8 拥有更多与 rune 类型相关的函数

字符串

字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)

Go 中的字符串里面的字符也可能根据需要占用 1 至 4 个字节(示例见第 4.6 节)

不可变

字符串是一种值类型,且值不可变,即创建某个文本后你无法再次修改这个文本的内容;更深入地讲,字符串是(关于)字节的定长数组(这说的应该是字面量的意思)

支持转义解释的字符串--解释字符串

使用双引号括起来

\n:换行符
\r:回车符
\t:tab 键
\u 或 \U:Unicode 字符
\\:反斜杠自身

非解释字符串

该类字符串使用反引号括起来。反引号 引起了的字符串 表示 字符串字面量,定义时写的啥样,它就啥样,你有换行,它就换行。如果我们传入一个字符串,内容存在"",为了语法合法,必须使用 反引号,比如 "abc"

  `This is a raw string \n` 中的 `\n\` 会被原样输出。

与 C、C++的区别

Go 中的字符串是根据长度限定,而非特殊字符 \0(C 碰到\0 才算字符串结束)

string 类型的零值为长度为零的字符串,即空字符串 ""

byte 和 rune

Go 语言的字符有以下两种:
一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

字符串比较

一般的比较运算符(==、!=、<、<=、>=、>)通过在内存中按字节比较来实现字符串的对比(那么,Unicode 的一个字符占多个字节,会被拆开一半一半来比较?见下面的关于字符串的迭代的内容)

字符串长度

len(str)

取纯 ASCII 码的字符串

字符串 str 的第 1 个字节:str[0]
第 i 个字节:str[i - 1]
最后 1 个字节:str[len(str)-1]

【注意事项】 获取字符串中某个字节的地址的行为是非法的,例如:&str[i]

字符串拼接符

s := s1 + s2

str := "Beginning of the string " +
	"second part of the string"

在循环中使用加号 + 拼接字符串并不是最高效的做法,更好的办法是使用函数 strings.Join()(第 4.7.10 节),有没有更好的办法了?有!使用字节缓冲(bytes.Buffer)拼接更加给力(第 7.2.6 节)

join

join 就是把数组元素之间插入分割符后合并称为字符串,如果分割符为空串,那就可以用来连接两个字符串

import (
	"fmt"
	"strings"
)

func main() {
	s1 := "hello"
	s2 := "word"
	var str []string = []string{s1, s2}
	s3 := strings.Join(str, "")
	fmt.Print(s3) // helloworld
}

通过 buffer 串联字符串

package main

import (
	"bytes"
	"fmt"
)

func main() {
	var buffer bytes.Buffer
	// 下面用三个重复语句模拟循环
	buffer.WriteString("hello")
	buffer.WriteString("world")
	buffer.WriteString("!")
	fmt.Print(buffer.String(), "\n")
}

字符和字符串拼接

package main

import (
	"bytes"
	"fmt"
)

func main() {
	var b *bytes.Buffer = new(bytes.Buffer)
	b.WriteByte('a')
	b.WriteString("bcd")
	b.WriteByte('e')
	b.WriteByte('#')

	fmt.Println(b.String()) // abcde#
}

遍历字符串内容

按索引方式遍历,实际上是返回字符串中的纯字节,只适合纯 ASCII 编码的字符串,如果是一个由多个字节表示的字符(unicode),那么就会出现乱码,解决方法是 for-range

以下函数从 str2 开始,遍历出来的内容其实是无效的

package main

import "fmt"

func main() {
	str2 := "12你好34"
	for ix := 0; ix < len(str2); ix++ {
		fmt.Printf("Character on position %d is: %c \n", ix, str2[ix])
	}
	for i, c := range str2 {
		fmt.Printf("%d:%c ", i, c)
	}
}

Character on position 0 is: 1
Character on position 1 is: 2
Character on position 2 is: ä
Character on position 3 is: ½
Character on position 4 is:
Character on position 5 is: å
Character on position 6 is: ¥
Character on position 7 is: ½
Character on position 8 is: 3
Character on position 9 is: 4
0:1 1:2 2:你 5:好 8:3 9:4

Unicode 字符会占用 2 个字节,有些甚至需要 3 个或者 4 个字节来进行表示。如果发现错误的 UTF8 字符,则该字符会被设置为 U+FFFD 并且索引向前移动一个字节

strings 和 strconv 包

开头结尾

strings 是 Go 的对于字符串的预定义处理函数

判断前缀和结尾

package main

import (
	"fmt"
	"strings"
)

func main() {
	var str string = "This is an example of a string"

	fmt.Printf("T/F? Does the string \"%s\" have prefix %s? --%t", str, "Th", strings.HasPrefix(str, "Th"))
	fmt.Printf("\n")
	fmt.Printf("%t\n", strings.HasPrefix(str, "Th"))
	fmt.Printf("%t\n", strings.HasSuffix(str, "ring"))
}

包含

Contains 判断字符串是否包含某个子串

package main

import (
	"fmt"
	"strings"
)

func main() {
	var str string = "the white car"

	fmt.Printf("%t\n", strings.Contains(str, "ca"))

}

位置

Index 判断子串出现的位置,若不包含字串,返回-1,包含返回 第一次匹配时, 子串在该串的第一个匹配字符的索引,若想返回最后一次匹配 子串在该串的第一个匹配字符的索引,则用 LastIndex
IndexRune 查找非 ASCII 编码 字符在串中的位置

package main

import (
	"fmt"
	"strings"
)

func main() {
	var s string = "the white car, the black car, 我都喜欢"

	fmt.Printf("%d\n", strings.Index(s, "car"))
	fmt.Printf("%d\n", strings.LastIndex(s, "car"))
	fmt.Printf("%d\n", strings.IndexRune(s, '我'))
	fmt.Printf("%d\n", strings.IndexRune(s, rune('我')))
}

替换

Replace() 用于将字符串 str 中的 前个 字符串 old 替换为字符串 new,并返回一个新的字符串,如果 n = -1 则替换所有字符串 old 为字符串 new(n 从 1 开始,表示前 1 个替换为 new)

package main

import (
	"fmt"
	"strings"
)

func main() {
	str := strings.Replace("abc123abc456", "abc", "def", 1)
	str2 := strings.Replace("abc123abc456", "abc", "def", 2)
	fmt.Printf(str + "|" + str2) // def123abc456|def123def456
}

统计次数

package main

import (
	"fmt"
	"strings"
)

func main() {
	var manyG = "g你giig~~g\\g"
	fmt.Printf("%d\n", strings.Count(manyG, "g"))
}

重复某个字符

package main

import (
	"fmt"
	"strings"
)

func main() {
	var origS string = "Hi there! "
	var newS string

	newS = strings.Repeat(origS, 3)
	fmt.Printf("The new repeated string is: %s\n", newS)
}

大小写

package main

import (
	"fmt"
	"strings"
)

func main() {
	var orig string = "Hey, how are you George?"
	fmt.Println(strings.ToLower(orig))
	fmt.Println(strings.ToUpper(orig))
}

修剪字符串的头尾

头尾去掉的字符的个数是不确定的,能去的都去

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.TrimSpace("  abc  "))
	fmt.Println(strings.Trim("cabcc", "c"))
	fmt.Println(strings.TrimLeft("cacb", "c"))
	fmt.Println(strings.TrimRight("cacc", "c"))
}

分割字符串(字符串转数组)

Fields 基于空白符(1 个或多个)来分割字符串
空白字符有:\t, \n, \v, \f, \r, ' ', U+0085 (NEL), U+00A0 (NBSP)

Split 基于自定义字符分割字符串

以上函数都返回 slice,一般使用 for-range 循环来继续处理

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Fields("hello,     this is my girl friend!"))
	fmt.Println(strings.Fields("hello, this\u0085is my\u00A0girl friend!"))
	fmt.Println(strings.Split("hello,|this|is|my|girl|friend!", "|"))
}

输出:
[hello, this is my girl friend!]
[hello, this is my girl friend!]
[hello, this is my girl friend!] len==6

拼接 slice 为字符串(数组转字符串)

Join 与 Split 相反

package main

import (
	"fmt"
	"strings"
)

func main() {
	sl := strings.Fields("hello, this is my girl friend!")
	fmt.Println(strings.Join(sl, "\n"))
}

输出:
hello,
this
is
my
girl
friend!

从字符串中读取内容

函数 strings.NewReader(str) 用于生成一个 Reader 并读取字符串中的内容,然后返回指向该 Reader 的指针,从其它类型读取内容的函数还有:

Read() 从 []byte 中读取内容。
ReadByte() 和 ReadRune() 从字符串中读取下一个 byte 或者 rune。

字符串转其他类型

与字符串相关的类型转换都是通过 strconv 包实现的

strconv.IntSize

strconv.IntSize 用于获取程序运行的操作系统平台下 int 类型所占的位数

任何类型都可以转成字符串

任何类型 T 转换为字符串总是成功的

数字类型转换到字符串
strconv.Itoa(i int) string 返回数字 i 所表示的字符串类型的十进制数(返回十进制数,类型为字符串)

strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int) string 将 64 位浮点型的数字转换为字符串,其中 fmt 表示格式(其值可以是 'b'、'e'、'f' 或 'g'),prec 表示精度,bitSize 则使用 32 表示 float32,用 64 表示 float64

package main

import (
	"fmt"
	"strconv"
)

func main() {
	num := 123
	num2 := 123.456
	fmt.Println(strconv.Itoa(num))
	fmt.Println(strconv.FormatFloat(num2, 'f', 2, 32))
}

字符串转其他类型

将字符串转换为其它类型 tp 并不总是可能的,可能会在运行时抛出错误 parsing "…": invalid argument

因为可能出现错误,所以会返回多一个 err
strconv.Atoi(s string) (i int, err error) 将字符串转换为 int 型。
strconv.ParseFloat(s string, bitSize int) (f float64, err error) 将字符串转换为 float64 型。

package main

import (
	"fmt"
	"strconv"
)

func main() {
	var orig string = "666"
	var an int
	var newS string

	fmt.Printf("The size of ints is: %d\n", strconv.IntSize)

	an, _ = strconv.Atoi(orig)
	fmt.Printf("The integer is: %d\n", an)
	an = an + 5
	newS = strconv.Itoa(an)
	fmt.Printf("The new string is: %s\n", newS)
}

The size of ints is: 64
The integer is: 666
The new string is: 671

时间和日期

https://blog.csdn.net/wschq/article/details/80114036
数据类型:time.Time var t time.Time
获取当前时间 time.Now()

时间处理是个很庞大的库,基本包括时间带点、时间段、时间运算、时区等,用到再仔细研究

Go 的时间格式化是相当智能

package main
import (
	"fmt"
	"time"
)

var week time.Duration
func main() {
	t := time.Now()
	fmt.Println(t) // e.g. Wed Dec 21 09:52:14 +0100 RST 2011
	fmt.Printf("%02d.%02d.%4d\n", t.Day(), t.Month(), t.Year())
	// 21.12.2011
	t = time.Now().UTC()
	fmt.Println(t) // Wed Dec 21 08:52:14 +0000 UTC 2011
	fmt.Println(time.Now()) // Wed Dec 21 09:52:14 +0100 RST 2011
	// calculating times:
	week = 60 * 60 * 24 * 7 * 1e9 // must be in nanosec
	week_from_now := t.Add(time.Duration(week))
	fmt.Println(week_from_now) // Wed Dec 28 08:52:14 +0000 UTC 2011
	// formatting times:
	fmt.Println(t.Format(time.RFC822)) // 21 Dec 11 0852 UTC
	fmt.Println(t.Format(time.ANSIC)) // Wed Dec 21 08:56:34 2011
	// The time must be 2006-01-02 15:04:05
	fmt.Println(t.Format("02 Jan 2006 15:04")) // 21 Dec 2011 08:52
	s := t.Format("20060102")
	fmt.Println(t, "=>", s)
	// Wed Dec 21 08:52:14 +0000 UTC 2011 => 20111221
}

与高级编程有关的

如果你需要在应用程序在经过一定时间或周期执行某项任务(事件处理的特例),则可以使用 time.After() 或者 time.Ticker:我们将会在 第 14.5 节 讨论这些有趣的事情。 另外,time.Sleep(d Duration) 可以实现对某个进程(实质上是 goroutine)时长为 d 的暂停

指针

不允许

不能进行指针运算,如:想通过 pointer+2、p++来移动指针指向字符串的字节数或数组的某个位置

允许

允许你控制特定集合的数据结构、分配的数量以及内存访问模式

获取内存所在地址

var i1 = 5
fmt.Printf("An integer: %d, it's location in memory: %p\n", i1, &i1)

指针数据类型

package main

import (
	"fmt"
	"time"
)

var week time.Duration

func main() {
	var i1 = 5
	fmt.Printf("An integer: %d, it's location in memory: %p\n", i1, &i1)
	var intP *int
	intP = &i1
	fmt.Printf("%p", intP)
}

指针变量在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节

获取指针指向的值

符号 * 可以放在一个指针前,如 *intP,那么它将得到这个指针指向地址上所存储的值;这被称为反引用(或者内容或者间接引用)操作符;

你可以在指针类型前面加上 * 号(前缀)来获取指针所指向的内容,这里的 * 号是一个类型更改器。使用一个指针引用一个值被称为间接引用

当一个指针被定义后没有分配到任何变量时,它的值为 nil。

一个指针变量通常缩写为 ptr

对于任何一个变量 var, 如下表达式都是正确的:var == *(&var)

package main

import "fmt"

func main() {
	s := "good bye"
	var p *string = &s
	*p = "ciao"
	fmt.Printf("Here is the pointer p: %p\n", p)  // prints address
	fmt.Printf("Here is the string *p: %s\n", *p) // prints ciao
	fmt.Printf("Here is the string s: %s\n", s)   // prints ciao
}

你不能获取字面量或常量的地址

const i = 5
ptr := &i //error: cannot take the address of i
ptr2 := &10 //error: cannot take the address of 10

指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期

Go 逃逸分析

https://studygolang.com/articles/17584

逃逸分析是一种确定指针动态范围的方法,可以分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。 当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或者去调用子程序。如果使用尾递归优化(通常在函数编程语言中是需要的),对象也可能逃逸到被调用的子程序中。 如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中的任何一个地方被访问到——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,它们也可能发生逃逸,这种情况是当前程序中的指针逃逸。 逃逸分析需要确定指针所有可以存储的地方,保证指针的生命周期只在当前进程或线程中。

大概就是说,go 消除了堆和栈的区别 go 在一定程度消除了堆和栈的区别,因为 go 在编译的时候进行逃逸分析,来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。所以我们可以把一个函数内的变量取引用返回,这样 Go 就会分析出来,把他保留下来,以便将来继续使用时不会报错,在 C 中,返回后函数的变量会无法访问,即使你返回它的引用,而 Go 却可以依然保证访问。

posted @ 2023-03-01 11:54  有蚊子  阅读(74)  评论(0编辑  收藏  举报