本文是Go语言系列文章Golang Notes的其中一篇,完整的文章列表请去总章查看。
本篇主要着眼于阐述一些Go语言中的基础知识点。当然,语法本身涉及的不会太多,看官方的tutorial就好,Go语言本身就以语法不复杂著称。
这里要介绍下极客时间上的专题:Go语言核心36讲
随书附带的代码:hyper0x/Golang_Puzzlers
总的来说讲的内容不算很深,一看作者的专业功底就很好,用词和语言都非常专业和规范,和看一些大部头的技术书感觉很类似。优点在于讲解内容不是很深,适合新手使用。缺点在于部分章节的安排不是很好,前后关系以及一些对于新手来说需要介绍的内容过渡不够,此外,范例和文章的契合度不够,作者在很多情况下都是给了个github库的链接,让读者自己去匹配着看,体验不够好。
此外,还有一个以范例来进行Go语言基础编码指导的站点:Go by Example,可以利用。
名称的首字母为大写的程序实体才可以被当前包外的代码引用,否则它就只能被当前包内的其他代码引用。小写,包私有;大写,公开。
路径和包名为internal的是模块私有的代码,无法被外部引用。
具体规则是,internal代码包中声明的公开程序实体仅能被该代码包的直接父包及其子包中的代码引用。当然,引用前需要先导入这个internal包。对于其他代码包,导入该internal包都是非法的,无法通过编译。
Go 语言中的程序实体
包括:
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名rune // int32 的别名
// 表示一个 Unicode 码点float32 float64
complex64 complex128
因为是强类型语言,且Go里面的类型转换都必须要显示进行,因此有的时候处理起来比较麻烦。这方面,比较常见的问题是字符串和数字类型之间的转换:
// string => int
int, err := strconv.Atoi(string)
// string => int64
int64, err := strconv.ParseInt(string, 10, 64)
// int => string
string := strconv.Itoa(int)
// int64 => string
string := strconv.FormatInt(int64,10)
它们本身就是对某种类型指针的封装:slice封装的指针是一个数组,所以传递的时候直接传递其本身就够了,一般来说不需要取址(&)
Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。
Go 语言在声明变量时,自动对变量对应的内存区域进行初始化操作。每个变量会初始化其类型的默认值,例如:
所以看到某个被申明的变量直接被拿来使用的时候千万不要惊奇,并不是一定要进行初始化才可以使用。
How do I know whether a variable is allocated on the heap or the stack?
Go语言中new和make的区别{:target:“_blank”}
make
只用于slice、map以及channel的初始化(非零值)new
用于类型(struct)的内存分配,并且内存置为零make
返回的还是这三个引用类型本身:func make(t Type, size ...IntegerType) Type
new
返回的是指向类型的指针:func new(Type) *Type
你可以认为,表达式类型就是对表达式进行求值后得到结果的类型。
[]string
是一个类型字面量。所谓类型字面量,就是用来表示数据类型本身的若干个字符。
type MyString = string // 这条声明语句表示,MyString是string类型的别名类型。
type MyString2 string // 注意,这里没有等号。这里的MyString2是一个新的类型,不同于其他任何类型。这种方式也可以被叫做对类型的再定义。
数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。(引用类型主要就这几种,不多的)
Go 语言里的切片长度和容量。长度指当前切片中的真实元素有多少个,容量指切片申请的内存容量是多少。
[...]int{1, 2, 3, 4, 5, 6}
用三个点代替了长度申明,这句语句执行的结果仍旧是一个数组,而不是切片。
从数组生成切片时,上界含,下界不含:
var s []int = primes[1:4]
1号位包含,4号位不含
判断map中键是否存在:
elem, ok = m[key]
// OR
_, ok = m[key]
表达式 T(v) 将值 v 转换为类型 T。
所有的类型转换必须是显示的。
f := float32(1)
官方文档:Type assertions。
类型断言表达式的语法形式是x.(T)。其中的x代表要被判断类型的值。这个值当下的类型必须是接口类型的,不过具体是哪个接口类型其实是无所谓的。
value, ok := interface{}(container).([]string)
一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。
_, ok := interface{}(1).(int)
fmt.Println("TypeOK:", ok)
// OR
var i interface{} = "hello"
_, ok := i.(string)
// OR
reflect.TypeOf(x) // return Type
这个ok还是必要的,否则在类型不匹配的时候会发生panic
根据不同类型进行不同行为
func m_type(i interface{}) {
switch i.(type) {
case string:
//...
case int:
//...
}
return
}
fmt.Printf("The type of pet is %T.\n", pet)
fmt.Printf("The type of pet is %s.\n", reflect.TypeOf(pet).String())
if 在判断之前可以添加一句简单的语句进行执行
if v := math.Pow(x, n); v < lim {
return v
}
没有初始化语句和后置语句的 for 就是其他语言中的 while,这种情况下可以删除前后的分号
for x < 1000 {
//...
}
switch 只会执行符合的条件,不会一路 fallthrough 下去
switch 还可以不带上判断条件,这样使用就等于if-else-else…
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
通道,make函数除了必须接收这样的类型字面量作为参数,还可以接收一个int类型的参数。后者是可选的,用于表示该通道的容量。所谓通道的容量,就是指通道最多可以缓存多少个元素值。
一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。
对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。在同一时刻,Go 语言的运行时系统(以下简称运行时系统)只会执行对同一个通道的任意个发送操作中的某一个。直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行。对于通道中的同一个元素值来说,发送操作和接收操作之间也是互斥的。
元素值从外界进入通道时会被复制
。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。
发送操作和接收操作中对元素值的处理都是不可分割的。例如,发送操作要么还没复制元素值,要么已经复制完毕,绝不会出现只复制了一部分的情况。
发送操作在完全完成之前会被阻塞
。接收操作也是如此。
针对缓冲通道
的情况。如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。
对于非缓冲通道
,情况要简单一些。无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递。并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。相比之下,缓冲通道则在用异步的方式传递数据。
select语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。
只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。
传入函数的参数值:数组是值类型,所以每一次复制都会拷贝它,以及它的所有元素值。对于引用类型,比如:切片、字典、通道,像上面那样复制它们的值,只会拷贝它们本身而已,并不会拷贝它们引用的底层数据。也就是说,这时只是浅表复制,而不是深层复制。
Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被“绑定”在了这些变量上。
例如,函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
在 Go 语言中,我们可以通过为一个类型编写名为String的方法,来自定义该类型的字符串表示形式。这个String方法不需要任何参数声明,但需要有一个string类型的结果声明。
Go 语言规范规定,如果一个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。我们可以通过此类型变量的名称后跟“.”,再后跟嵌入字段类型的方式引用到该字段。也就是说,嵌入字段的类型既是类型也是名称。
type Animal struct {
scientificName string // 学名。
AnimalCategory // 动物基本分类。
}
func (a Animal) String() string {
return fmt.Sprintf("%s (category: %s)",
a.scientificName, a.AnimalCategory)
}
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。(有点继承的意思,应该更类似于Mixin)
如果被嵌入的类型中有和嵌入者重名的方法,则被嵌入这的方法会覆盖掉嵌入者的方法。(这个可以理解为重载)
Why is there no type inheritance?
简单来说,面向对象编程中的继承,其实是通过牺牲一定的代码简洁性来换取可扩展性,而且这种可扩展性是通过侵入的方式来实现的。而Go语言类型之间的组合采用的是非声明的方式,我们不需要显式地声明某个类型实现了某个接口,或者一个类型继承了另一个类型。同时,类型组合也是非侵入式的,它不会破坏类型的封装或加重类型之间的耦合。
类型的接收者类型:
func (cat *Cat) SetName(name string)
指针类型func (cat Cat) SetName(name string)
值类型区别:
struct{}类型值的表示法只有一个,即:struct{}{}。并且,它占用的内存空间是0字节。确切地说,这个值在整个 Go 程序中永远都只会存在一份。虽然我们可以无数次地使用这个值字面量,但是用到的却都是同一个值。
接口类型间的嵌入也被称为接口的组合。只要组合的接口之间有同名的方法就会产生冲突,从而无法通过编译。接口的组合根本不可能导致“屏蔽”现象的出现。
接口值可以看做包含值和具体类型的元组:
(value, type)
接口值保存了一个具体底层类型的具体值:
type I interface {
M()
}
type F float64
func (f F) M() {
fmt.Println(f)
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
var i I = F(math.Pi)
describe(i)
// (3.141592653589793, main.F)
*
表示的是取值操作,传过来的是一个指针,通过在前面附带*
,就获得了这个指针所指向的值。&
表示的是寻址操作,传过来的是一个值,通过在前面附带&
,就获得了指向这个值的指针。下列表中的值都是不可寻址的:
共性:
不可变的
值不可寻址。常量、基本类型的值字面量、字符串变量的值、函数以及方法的字面量都是如此。其实这样规定也有安全性方面的考虑。临时结果
的值都是不可寻址的。算术操作的结果值属于临时结果,针对值字面量的表达式结果值也属于临时结果。但有一个例外,对切片字面量的索引结果值虽然也属于临时结果,但却是可寻址的。不安全的
,该值就不可寻址。由于字典的内部机制,对字典的索引结果值的取址操作都是不安全的。另外,获取由字面量或标识符代表的函数或方法的地址显然也是不安全的。a := [3]int{1,2,3}
fmt.Printf("%p\n", &a)
在sync/atomic包中声明了很多用于原子操作的函数。可以用在协程竞争的时候的线程安全。
e.g
atomic.AddUint32(&count, 1)
对于具体错误的判断,Go 语言中都有哪些惯用法?
从 panic 被引发到程序终止运行的大致过程:建立panic,并从运行的代码开始按调用栈逐层返回,最终返回运行时系统,打印信息,程序崩溃。
error返回可以被忽略,因此一般应用在”不致命”的场景。内建函数panic可用于引发 panic,一般用在”致命”错误的场景。
defer是先进后出(FILO)的,相当于一个栈,需要注意:
func main() {
defer fmt.Println("first defer")
for i := 0; i < 3; i++ {
defer fmt.Printf("defer in for [%d]\n", i)
}
defer fmt.Println("last defer")
}
// last defer
// defer in for [2]
// defer in for [1]
// defer in for [0]
// first defer
Go语言官方附带了不少很好用的命令行工具,除了以go命令为开头的go xxx
的命令之外,还有:
Go命令行工具官方文档入口:Command Documentation
此外,这里还引用了不少hyper0x/go_command_tutorial这个文档库的内容,有兴趣的可以直接读一读。不过这库里的解释和范例都是以Go语言1.4-1.5版本为基准的,算是特别老旧了,只能说参考下。主要还是要看英语的官方手册。
下文并不会穷举官方手册中所有的Go命令行工具,仅列举部分常用的。
功能:编译指定的源码文件或代码包以及它们的依赖包
手册:Compile packages and dependencies
中文:go build
功能:编译并安装指定的代码包及它们的依赖包
手册:Compile and install packages and dependencies
中文:go install
功能:从互联网上下载或更新指定的代码包及其依赖包
手册:Download and install packages and dependencies
中文:go get
功能:删除掉执行其它命令时产生的一些文件和目录
手册:Remove object files and cached files
中文:go clean
功能:展示指定代码包的文档
手册:Show documentation for package or symbol
中文:go doc与godoc
功能:运行命令源码文件
手册:Compile and run Go program
中文:go run
功能:对Go语言编写的程序进行测试
手册:Test packages
中文:go test
功能:列出指定的代码包的信息
手册:List packages or modules
中文:go list
功能:按Go语言代码规范格式化指定代码包中的所有Go语言源码文件
手册:Gofmt (reformat) package sources
中文:go fmt与gofmt
功能:把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码
手册:Update packages to use new APIs
中文:go fix与go tool fix
功能:检查Go语言源码中静态错误
手册:Report likely mistakes in packages
中文:go vet与go tool vet
功能:分析go应用程序,给出profile文件
手册:这个工具没有官方文档,讨论相关可以见:There’s *no* pprof documentation?,官方倒是有个pprof包的文档Package pprof
google手册:google/pprof
中文:go tool pprof
如果有订阅Go语言核心36讲
的话:
功能:创建能够调用C语言代码的Go语言源码文件
手册:Calling between Go and C
中文:go tool cgo
功能:打印Go语言的环境信息
手册:Print Go environment information
中文:go env
比较细节的解释可以参考:剖析使Go语言高效的5个特性(2/5): 函数调用不是免费的。此外,可以看下官方的wiki:Function Inlining来了解官方对于内联的要求。
Map的值是不可以取址的,见:spec: can take the address of map[x] #11865。如果有这样的需求,必须将其赋值给某个变量,然后对某个变量进行取址。
// compiling error
fmt.Printf("Address: %p", &m["key"])
// do this, but it's a little bit meaning less
var v = m["key"]
fmt.Printf("Address: %p", &v)
EOF