Golang笔记(一)简洁的语言风格

时间:2022-03-22 16:41:46

Golang笔记(一)简洁的语言风格

概述

Golang继承了很多C语言的风格,寡人使用了十几年C语言,切换到Golang时上手很快,并且随着深入的使用,越来越喜欢这门语言。Golang最直观的感受是简洁(语言细节少)、高效(开发迅速)和高性能(忽略GC时,类比C++的性能)。

package

package是golang最基本的分发单位。每个golang源代码文件开头都要申明其属于哪个package。如果输出的是可执行文件,则必须定义一个'main' package和属于它的一个main()函数。

通过import关键字引用其需要用到的package,这和C语言的include类似。使用其他package的函数,直接用.操作符即可,非常简洁,无需像C语言一样需要通过头文件将需要使用的函数和变量进行声明,也没有头文件包含的顺序问题。下面这个栗子,用os.OpenFile直接调用package "os"里的OpenFile函数。

package main

import (
"log"
"os"
) func main() {
f, err := os.OpenFile("notes.txt", os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
}

Golang语言生态比较完善,有大量的库可以使用,只需要将对应的package进行import即可。

goroutine

Golang独特的goroutine机制实现多线程并行处理。使用简单的go func()语句即可启动一个goroutine。一个goroutine并不等同于一个线程,实际上Golang底层封装了线程。一个线程绑定到CPU某核,其上循环运行多个goroutine。goroutine是按照抢占式进行调度的,一个goroutine执行一会(如10ms)就会被切换到下一个。

Golang独特的channel机制实现goroutine之间消息交互。channel类似一个消息管道,管道里承载着一种类型为type的数据,通过make(chan type, size)函数定义,size为channel里缓存个数。通过chan <- value写入value到channel,通过value <- chan读取channel里的数据。当channel里没有数据时,读取操作会一直阻塞。同样当channel里缓存满了(没有及时读取),写入操作也会一直阻塞。所以channel也可以用于goroutine之间的同步。

多返回值

Golang函数可以定义多个返回值。这是对C语言的一大改进,在C语言里要想返回多个参数时,只能开辟指针以函数入参的形式带入。这个痛苦在Golang则完全没有。Golang的多参数返回值也非常易于error的返回和处理,相比高级语言里的繁琐的exceptions体系更加简洁易用。看下面代码,OpenFile函数返回了File指针类型和error类型的两个参数,在上面那个栗子里可以看到如何获取两个参数返回值。

package os
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
...
}

指针

Golang保留了C语言的指针类型。使用指针时,*&的含义和C语言一样。

Golang同时也简化了对指针的使用。在C语言中你需要用->操作符来使用指针指向的结构体的值,而结构体变量则使用.操作符。Golang无论指针还是结构体变量,都使用.,这点让码农多数时候甚至都不用关心到底在使用指针还是结构体变量。另外Golang不支持指针运算。C语言里的指针运算虽然灵活,但用的不好很容易出界。

变量

Golang的变量在声明时自动做了初始化。数值型变量默认赋值0,string或者指针变量默认赋值nil,slice变量默认是个空的slice等等。再也不用担心C语言里变量声明时忘记初始化导致的BUG。

Golang的变量支持隐藏式声明,通过":="的方式将其声明为右边的类型。这个设计可以大幅减少变量显示声明,简化了代码。同时也方便了码农,有时候用":="函数的返回值,都不用太care到底返回的类型是啥。Golang本身并不支持"泛型"变量,但通过:=和Interface可以实现类似"泛型"的特性。

另外,Golang支持"%v"自动匹配变量类型的打印输出方式,如果变量是一个结构体类型,则将结构体内的所有字段全部输出。这在高级语言里可能很常见,但作为C语言切换来的码农,这个特性可以大幅提高调试日志的编码效率。要输出一个结构体里几十个字段,在C语言里一个字段一个字段的码代码是多么痛苦的回忆。

array和slice

Golang的array和C语言的数组是一样的概念,用法也是完全一样。数组定义了之后长度无法改变,同时数组是值类型,每次传递都会拷贝一个副本(当然也可以显示使用数组的指针来传递)。

Golang提供了一种更灵活的“动态数组”:slice和array类似,只是其长度可变,可随时为新的数据扩大数组长度。

slice本质上并不是动态数组,它是一个引用类型。slice的内部实现总是指向了一个array。slice数据结构可以抽象为:

  1. 一个指向array或者array偏移位置的指针;
  2. slice的长度len;
  3. slice分配的存储空间cap;

理解了slice是什么,再通过代码样例来看看如何使用slice:

var a = [10]int {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} //定义数组并初始化
var s []int //slice的声明和array的区别是[]里为空
s = a[:5] //赋值s为a的0~4位置,此时slice指向a[0]并且len=5,cap=10。对数组或者slice的截取访问的格式可以是[:5],或者[3:5],或者[3:],或者[:],分别对应0~4,3~4, 3~len-1,0~len-1
s[0] = -1 //slice的赋值只是指针指向的方式进行引用,因此对s[0]的修改会导致a[0]也被改变。 s1 := make([]int, 5, 10) //通过make创建一个int类型的slice,并且len=5,cap=10。其中5个元素值初始化为0。
fmt.Printf("len: %v, cap: %v", len(s1), cap(s1)) //通过len()和cap()函数返回slice的len和cap
for i, v := range s1 { //通过'range'关键字遍历slice,同样的方法也用于遍历array和map。
fmt.Println(v)
}
s1 := append(s1, 5,6,7) //通过append()函数扩展s1
s2 := append(s1, 8) //此时s1,s2指向的是同一个底层array,只是s1指向0~7,s2指向的是0~8,修改s2会影响s1里的数据。
s3 := append(s1, s2) //将s2 append到s1时等效于将s2里的数据append到s1,由于s1的cap不够存储append后的数据,底层会分配一个新的array用于返回。
n := copy(s3, s1) //通过copy()函数可以将数据从array/slice拷贝到slice,返回的n为实际拷贝大小即s1,s3两个slice的较小的长度。
n := copy(s2, s1) //s1,s2底层指向同一个array,因此copy()会发生元素重叠

map

Golang也提供map类型,即key-value的集合。map的操作和slice一样,通过key来操作。不同的是slice的索引是int类型,map则可以是int后者string等类型,并完全支持了'=='和'!='操作符。

在C++/Java中,map一般都以库的方式提供,比如在C++中是STL的std::map<>,在Java中是Hashmap<>。而Golang使用map不需要引入任何库,并且用起来也更加方便。通过如下代码来熟悉下map:

var m map[string]int    	//声明一个string类型key,int类型value的map。需要注意map只是声明就使用,会抛出panic,必须通过make或者其他方法进行创建后才能使用。
m = make(map[string] int) //通过make创建map,也可以指定初始化存储空间:make(map[string] int, 10)
m["first"] = 1 //直接赋值的方式插入或者修改map元素
fmt.Println(len(m)) //len()函数同样可以用于显示map的长度
v, ok := m["first"] //查找key对应value,如果不存在了ok==false
if ok {
fmt.Println(v)
}
for k, v := range m { //使用range遍历map所有元素,返回每个key和value
fmt.Println(v)
}
delete(m, "first") //通过delete()函数将对应元素删除,元素不存在也没有关系,不会panic

总结

Golang的这些特点让它的代码显得非常简洁,开发效率高,性能媲美C++,当然其GC还是会成为性能的瓶颈。