Go语言又名Golang,由谷歌公司开发并推出,它的主要目标是"兼具Python等动态语言的开发速度和C/C++等编译型语言的性能和安全性"。从Go语言的语法中,我们可以看到C/C++、Python等优秀语言的影子,它借鉴了其它主流语言的优势(自动垃圾回收、切片、字典),并实现了自己的特点 (协程开发、动态接口)。
Go语言的主要特点有:
Go语言的变量/常量声明很简单:
var a := 10
var为变量的修饰符,const为常量的修饰符。上面的例子就是声明了一个变量a,同时给它赋值为10。你也可以先进行声明,等到后面再赋值:
var a int a = 10
注意:当你声明了一个变量,如果没有使用它,那么编译器会报错。当你想阻止这种行为时,可以这样做:
_ = a
这段代码确实用到了a,只不过没有对a进行任何操作。
Go源码一般在用户工作区(即创建项目的目录),它包含三个子目录:
go install
目录安装后的代码包的归档文件。go install
安装后,保存生成的可执行文件。在src目录下导包时,要记得将工作区路径添加到
GOPATH
中,只有这样在编译时才能找到对应的代码包。
导包的语法为:
import "包名"
// 不想加前缀,而直接使用某个依赖包中的程序实体
import . "包名"
import {
"包名"
}
import {
"别名" "包名"
}
在Go中,没有访问修饰符这种关键字,而是简单地通过标识符的首字母大小来控制程序实体的访问权限。如果标识符首字母大写,那么所对应的程序实体可以被本代码包之外的代码访问,然后不行。这个规则对于结构体、接口和函数都有效。
Go中每个包都有一个内置函数init()
,当包初始化时,首先初始化所有的全局变量,然后会自动执行该函数的内容,相当于java的static静态代码块
。
Go中有很多预定义类型,可以将它们简单地划分为基本类型和高级类型,基本类型有:
类型 | 宽度 | 初始值 |
---|---|---|
bool | 1 | false |
byte | 1 | 0 |
rune | 4 | 0 |
int/uint | - | 0 |
int8/uint8 | 1 | 0 |
int16/uint16 | 2 | 0 |
int32/uint32 | 4 | 0 |
int64/uint64 | 8 | 0 |
float32 | 4 | 0.0 |
float64 | 8 | 0.0 |
complex64 | 8 | 0.0 + 0.01 |
complex128 | 16 | 0.0 + 0.01 |
string | - | “” |
intXX/floatXX/complex表示不同进制位下的整数/浮点数 /复数类型。
int/uint的实际宽度是根据计算机架构而变化的,在x86-64下,宽度为8字节。
byte(可看做uint8)是代表了一个ASCII编码的字符,rune类型(可看做int32)是用于存储Unicode编码的字符,同时rune字面量还支持转义字符。
在Go语言中不支持类型的自动转换,如果需要强转需要显式声明,比如:
var num int16 = 32
var num2 int32 = int32(num)
Go语言提供了专门的包用于字符串与其它数据的相互转换:
var str string = strconv.Itoa(num) // 数字-->字符串 Itoa是Format
num2,_ := strconv.Atoi(str) // 字符串-->数字 Atoi是ParseInt
// 使用FormatXXX()将给定类型格式化为string类型
str = strconv.FormatBool(true)
str = strconv.FormatFloat(3.0, 'E', -1, 64)
str = strconv.FormatInt(-10, 16)
str = strconv.FormatUint(10, 16)
// 使用ParseXXX()将string类型转换为给定类型
pbool := strconv.ParseBool("true")
pint := strconv.ParseBool("123", 10, 32)
puint := strconv.ParseBool("123", 10, 32)
pfloat := strconv.ParseFloat("3.0", 32)
获取类型的方法可以通过反射:
num := float64(3.14) println(reflect.TypeOf(num).Name())
也可以在后面章节里通过
.(type)
配合switch进行获取。
高级类型有:
数组的声明方式为:
// 指定长度
var arr [4]int = [4]int{1, 2, 3, 4}
// 根据初始化值指定数组长度
arr := [...]int{1, 2, 3, 4}
我们可以通过==
和!=
来比较两个元素类型相同的数组是否相同(数组长度、数组元素)。
数组的使用和在其它语言的使用一样,都是通过索引访问和修改元素。对于数组的遍历一般是使用for
控制语句:
// 从原数组复制元素到新数组
arr2 := arr[2:]
// arr[i:] 从 i 切到最尾部
// arr[:j] 从最开头切到 j(不包含 j)
// arr[:]
for idx, val := range arr{
}
for i := 0; i < len(arr2); i++ {
}
数组一旦声明后,长度不可修改,因此如果想动态修改数组长度只能通过创建一个新数组,将旧数组的数据复制到新数据。
切片和数组很相似,都是基于索引访问元素的,但是它可以动态修改自己的长度,因此大多数情况下都是用切片。
切片的定义有:
var slice []type = make([]type, len) # len 为切片长度
slice :=[] int {1,2,3}
通过len(数组)
返回的是数组长度,通过len(切片)
返回的是切片元素的个数,如果想知道切片容量大小,就应该用cap(切片)
。
可以使用slice = append(slice, el)
向原来切片添加元素,在切片的容量小于 1000 个元素时,容量的增长因子会设为 2。一旦元素个数超过 1000,容量的增长因子会设为 1.25。
注意 :截取的切片和原切片共享同一段底层数组,如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到。Go内置的
copy(dst, src)
函数可以将一个切片中的元素拷贝到另一个切片中,表示把切片 src 中的元素拷贝到切片dst 中,返回值为拷贝成功的元素个数。如果src比dst长,就截断;如果 src比dst 短,则只拷贝src那部分。
字典类似于java的Map结构,通过key-value键值对存储元素。
字典的操作方法:
var map1 map[string]string // 声明一个字典
map1 = make(map[string]string) // 使用 make 函数对map初始化
map1["name"] = "zs" // 赋值操作
fmt.Println(map1["name"]) // 读取操作
delete(map1, "zs") // 删除操作
_,ok = map1["name"]// 如果name存在,ok=true,否则ok=false
// 字典遍历,在Go中没有类似keys()和values()这样的方法,如果我们想获取key和value需要自己循环
for key,val := range fruits{
fmt.Println(key, val)
}
字典默认是无序的,如果要为map排序,需要将key/value拷贝至一个切片中,再对切片排序,然后再使用。
函数作为Go的一等公民,能够当做值来传递和使用,它既可以作为其它函数的参数,也可以作为返回结果。
函数的简单使用:
func test1(){int, error}{
var res int
var err errror
// doSomething
return res, err
}
// 如果返回列表中指定了返回值的字面量,那么可以直接return
func test2(){res int, err error}{
// doSomething
res = ...
err = ...
return
}
将函数作为值的使用:
func test1(fun func()) func() {
print("我是test1\n")
fun() // 函数作为参数值
return test2 // 函数作为返回值
}
func test2() {
print("我是test2\n")
}
func test3() {
print("我是test3\n")
}
func main() {
t := test1
t(test3)()
}
函数可以接收值和指针。对于非指针的数据类型,与它关联的方法的结合中只包含它的值方法,对于它的指针类型,其方法集合中既包含值方法也包含指针方法。Go在内部可以对值和指针进行转换,所以对于非指针数据类型的值,也可以调用其指针方法。
Go的接口遵循Duck Type
原则,并且不需要像java那样显式实现,只要Go中的结构体实现了某个接口的所有方法,那么它就自动与该接口绑定。接口是Go多态的一种实现机制。
接口的使用:
type Person interface {
Say(msg string)
}
type Student struct {
name string
age int
}
func (this *Student)Say(msg string) {
fmt.Printf("学生 %s 说:%s\n", this.name, msg)
}
type Teacher struct {
name string
age int
}
func (this *Teacher)Say(msg string) {
fmt.Printf("老师 %s 说:%s\n", this.name, msg)
}
func main() {
var person Person
person = &Student{"zs", 12}
person.Say("你好")
person = &Teacher{"ls", 40}
person.Say("好啊")
}
Go的结构体和java的对象概念差不多,包含属性和方法,但是在其它地方有很大不同。我们可以看看比较:
public class UserBean implements Serializable {
private Integer id;
private String name;
public UserBean() {
}
public UserBean(Integer id, String name) {
this.id = id;
this.name = name;
}
public String getName() {
return username;
}
public void setUName(String username) {
this.username = username;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public String toString() {
return "UserBean{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
type User struct {
id int32
name string
}
func (self *User) GetId() (int32) {
return self.id
}
func (self *User) SetId(id int32) {
self.id = id
}
func (self *User) GetName() (string) {
return self.name
}
func (self *User) SetName(name string) {
self.name = name
}
func (self *User) String() (string) {
return fmt.Sprintf("id = %d, name = %s", self.id, self.name)
}
前面我们说过,Go中的访问权限是由标识符首字母大小写决定的,在上面的go代码中将User结构体的字段定义为私有,将它的方法定义为公开,
func (self *User)
表示这是User
的方法而不是函数。
跟Java中打印对象实际上是调用对象的
toString()
方法类似,Go也提供了对应的机制,当你使用fmt.Printf("%+v\n", obj)
打印对象时,实际上调用了对象的String()
方法:// 在创建结构体时可以指定结构体字面量或者忽略: user1 := User{12, "zs"} // 没有指定字面量时,传值的顺序要遵循结构体字面量的顺序 user2 := User{id:12, name:"ls"} fmt.Printf("%+v\n", user1) print(user2.String(), "\n")
Go语言简化了控制流程语句的规模,并进行了一定的修改,主要特点有:
defer
关键字实现了类似于java的finally
功能。go
语句开启goroutine多线程功能。select
语句与通道配合使用,增强对多线程的支持。下面,我们来一一看看Go的流程控制语句。
这没什么好说的,主要注意在Go中if语句可以添加一个初始化语句,比如:
if val := test(100); val > 100{ // 当val条件为true时执行if代码块的内容
} else if val > 50 {
} else {
}
for语句一般有三种使用方式:
普通for:
for i := 0; i < len(arr); i++ {
}
do/while:
for m < 100 {
m *= 2
}
range遍历:
for idx, val := range arr {
}
注意,不同的类型使用range的迭代产出不同:
类型 产出值1 产出值2 数组 索引 元素值 字符串 字符下标 字符rune值 切片 索引 元素值 字典 键 值 通道 元素值
switch语句提供多分支执行的方法,一般情况下用于两种情况:
普通的分支执行:
switch con := test(); con {
case 1:
fmt.Printf("this is 1\n")
case 2:
fmt.Printf("this is 2\n")
case 3:
fmt.Printf("this is 3\n")
// 默认情况下当匹配到一个case时,后面便不再匹配,但是可以用fallthrough指示继续向下匹配
fallthrough
default:
fmt.Printf("this is unkown\n")
}
匹配数据类型:
var num interface{}
num = "3"
switch num.(type) {
case int:
fmt.Printf("this is 1\n")
case string:
fmt.Printf("this is 2\n")
case float32:
fmt.Printf("this is 3\n")
default:
fmt.Printf("this is unkown\n")
}
注意:
num.(type)
必须要在num声明为接口时才能使用。
panic相当于java的throw,不过在Go中一般称作运行时恐慌,而不是异常。panic一旦引发,就会通过调用方法传播直到程序崩溃,Go中提供了类似于catch的recover来拦截恐慌。recover被调用后,将会一个interface类型的恐慌结构,否则返回nil。
recover一般是结合defer使用的,defer语句将在下一部分阐述:
func panicTest(val int) {
if val < 0 {
panic("错误的值")
}
fmt.Printf("value is %d\n", val)
}
func recoverTest(val int) {
defer func() {
if e := recover(); e != nil {
fmt.Printf("拦截异常成功!\n")
}
}()
panicTest(val)
}
func main() {
recoverTest(10)
recoverTest(-10)
recoverTest(10)
}
在上面的代码中,首先向recoverTest传入一个合法的值,函数正常执行。然后传入一个非法值,在panicTest时抛出错误,但是在recoverTest函数中在
defer
进行了处理,将抛出的错误进行了处理,所以程序没有崩溃,第三次正常执行。
在测试异常机制时,使用了defer
语句,该语句用于延迟调用指定的函数,将一个方法延迟到包裹该方法的方法返回时执行,该函数被称之为延迟函数。它的运行机制如下:
可以通过defer定义多个延迟函数,此时会将所有的延迟函数按照代码顺序进行压栈处理,当defer被触发时,所有的压栈方法都会出栈。
注意,只有在方法返回时,defer才会被调用,如果直接使用类似
os.Exit(0)
退出的话,defer不会被触发。
defer的闭包使用:
先来看下面这个代码:
var arr [5]struct{}
for i := range arr {
defer func() { fmt.Println(i) }()
}
for i := range arr {
defer func(i int) { fmt.Println(i) }(i)
}
运行之后结果为432104444,这是为什么呢?
首先43210是第二个for循环的输出,44444是第一个for循环的输出,如前面讲的,延迟函数是通过栈存储的,遵循FILO原则。在第一个for循环中,变量i在defer被声明时就已经确定值了,即实际上它等价于:defer func() { fmt.Println(4) }()
,i等于4的原因是因为defer是在函数执行完成时开始的,此时for循环已经过了5次,i结果累加也就变成了4。而在第二个for循环中,因为defer语句接收i作为函数参数,所以每次存储的i的值都是不一样的。
在Go中,处理并发的是更轻量级的线程——协程,虽然协程的出现比线程还要早,但是由于当时协程由于是非抢占式的,需要用户手动释放运行权(相当于单线程),所以并没有受到重视。但是Go在此基础上自己实现了协程的调度机制,使用户不需要手动设置。
每一个并发执行的活动被称为goroutine,当一个程序启动的时候,只有一个goroutine来调用main函数,称它为主goroutine,新的goroutine通过go语句进行创建。
goroutine的简单使用如下:
func main() {
go func() {
fmt.Printf("Hello World!\n")
}()
time.Sleep(1 * time.Second) // 让主线程睡眠1s
fmt.Printf("exit\n")
}
相对于java需要继承或实现相关的类,Go对于并发的使用就简单很多 ,只需要使用go语句就能在主goroutine上开启子goroutine。
Go的sync包提供了基本的同步方法,比如互斥锁、信号量等。但是Go并不推荐这个包中大多数的方法,因为Go提倡使用以共享内存的方式来通信。Go用于控制并发的方法主要是下面的channel(通道)。
sync中主要有:
WaitGroup:用来等待一组goroutines的结束。
func helloWorld() {
fmt.Printf("Hello World\n")
}
func main() {
var wg sync.WaitGroup // 声明group
wg.Add(10) // 指定group个数
for i := 0; i < 10; i++ {
go func() {
helloWorld()
wg.Done() // group次数减1
}()
}
wg.Wait() // 等待直到group次数为0
}
Map:就是在并发环境下使用的Map,跟java的CurrentHashMap作用差不多。
func main() {
var scene sync.Map
scene.Store("test1", 97) // 将键值对保存到sync.Map
scene.Store("test2", 100)
scene.Store("test3", 200)
fmt.Println(scene.Load("test1")) // 从sync.Map中根据键取值
scene.Delete("test1") // 根据键删除对应的键值对
// 遍历所有sync.Map中的键值对,遍历需要提供一个匿名函数,将结果返回
scene.Range(func(k, v interface{}) bool {
fmt.Printf("key:%s ----> value:%s\n", k, v)
return true
})
}
Mutex:互斥锁使用:
func helloWorld() {
fmt.Printf("Hello World\n")
}
func main() {
var lock sync.Mutex // 声明互斥锁
for i := 0; i < 10; i++ {
lock.Lock() // 加锁
go func() {
helloWorld()
lock.Unlock() // 解锁
}()
}
}
RWMutex:跟java的读写锁差不多,可以分为读锁和写锁进行操作。
Once:指定某个方法只执行一次:
func helloWorld() {
fmt.Printf("Hello World\n")
}
func main() {
var once sync.Once
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
once.Do(helloWorld) // 调用指定方法
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
Pool:缓存对象池,Go的Pool在很多地方都用到了,比如http连接的创建、数据库连接的创建等。
func helloWorld() interface{} {
fmt.Printf("Hello World\n")
return 0
}
func main() {
p := &sync.Pool{ // Pool的缓存对象数量没有限制
New:helloWorld,
}
task1 := p.Get().(int) // 获取返回值
p.Put(10) // 修改值
runtime.GC() // GC时清除所有的pool里面的所有缓存的对象
task2 := p.Get().(int)
fmt.Printf("a = %d, b = %b\n", task1, task2)
}
Go语言为不同线程间的通信提供了channel
支持,通道是可以让一个goroutine发送特定的值到另外一个goroutine的通信机制。
示例如下所示:
func sender(ch chan string) {
ch <- "test1" // 从channel中写数据
ch <- "test2"
ch <- "test3"
ch <- "test4"
}
func recver(ch chan string) {
for {
fmt.Println(<-ch) // 从channel中读数据
}
}
func main() {
ch := make(chan string) // 创建一个channel
go sender(ch)
go recver(ch)
time.Sleep(1e9)
}
上面代码中创建的channel是无缓冲的,也可以使用
make(chan string, 2)
创建有缓冲的channel。
我们可以在函数的参数列表指定是只读channel、只写channel、可读可写channel:
// 只读channel func test1(out <-chan int) // 只写channel func test2(in chan<- int) // 可读可写channel func test3(ch chan int)
channel本质上传递的是数据的拷贝。
select可用于监听多个channel的数据流入:
select {
case v, ok := <-chann:
if ok {
fmt.Println(v)
} else {
fmt.Println("close")
return
}
default:
fmt.Println("waiting")
}