平时工作语言是c/c++,做后端开发很强大,但是毕竟写起来还是麻烦了一些,缺乏现代语言特性。所以一直在找一门辅助语言,它有便捷的语法,可以随手写一些小工具甚至小服务啥的。之前考虑过node.js,写c bundle感觉还可以,但是v8的用法感觉写起来有点麻烦,而且最重要的是node里操作二进制数据太麻烦,日常工作中一半的工作都是和二进制打交道,所以node并不太合适。并且js的语法也相对比较随意,写起来不那么规范。正好最近感觉自己好久没有学习新语言了,于是打算研究研究golang,作为面向concurrency的语言之一,Scala/Erlang是跑在虚拟机上的语言,在现在的生产环境中用起来有点维护麻烦,同时这种fp的语言对我来说学习成本也比较高,所以看起来还是go更合适一些。
hello world
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
运行$ go run hello.go
,编译$ go build -o hello hello.go
数据结构
基本类型
bool
布尔类型,true
和false
,没啥好说的
整型
uint8 //the set of all unsigned 8-bit integers (0 to 255)
uint16 //the set of all unsigned 16-bit integers (0 to 65535)
uint32 //the set of all unsigned 32-bit integers (0 to 4294967295)
uint64 //the set of all unsigned 64-bit integers (0 to 18446744073709551615)
int8 //the set of all signed 8-bit integers (-128 to 127)
int16 //the set of all signed 16-bit integers (-32768 to 32767)
int32 //the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64 //the set of all signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
浮点数
float32 //the set of all IEEE-754 32-bit floating-point numbers
float64 //the set of all IEEE-754 64-bit floating-point numbers
复数
complex64 //the set of all complex numbers with float32 real and imaginary parts
complex128 //the set of all complex numbers with float64 real and imaginary parts
特殊类型
byte //alias for uint8
rune //alias for int32
uint //either 32 or 64 bits
int //same size as uint
uintptr //an unsigned integer large enough to store the uninterpreted bits of a pointer value
记不清之前在哪个文档上看的,说int
无论在32位还是64位机器上,都是32位的,但看Language Specification上还是说int的长度和机器的架构有关系,可能之前版本的go设计是统一32位,但由于某些原因,最新版本还是将int的设计为架构相关,这点和c不太一样。不过明确声明长度,用uint64
之类的还是一个很好的习惯,循环之类的用用int
还行。
字符串
go的字符串和c有点像,但go的字符串是不可改变的,实际上c的静态字符串也是这样的,可以通过下标来取得每个字节的内容,和c是一模一样的,len
函数用来求长度,类似strlen
。不可以对s[i]
取址,即&s[i]
是错误的。
数组
go的数组有点像python,提供各种切片操作。这点要比c灵活得多。go的数组是值,所以赋值时会复制每个元素。go的数组长度是它的类型的一部分,也就是说[5]int
和[10]int
是两种类型。如果go的数组要实现传址,必须要显示的求址才行,不过go的语法不推荐这么使用数组,go的语法习惯是使用切片(slice),感觉切片叫着别扭,以后就统一叫slice好了。
//类似int arr[5];
var arr [5]int
//初始化了int arr[] = {1, 2, 3, 4, 5};
b := [5]{1, 2, 3, 4, 5}
slice
slice可以看做是一个特殊的数组,其实和c里面的数组非常像。slice作为参数时是传址的,也就是可以在函数内部修改参数中的slice并且外部看得到变化。slice可以做一些类似python中的下标范围操作。slice有两个重要的内建函数,len
是返回当前数据的长度,而cap
是返回最大容量。特殊的len(nil)=0
, cap(nil)=0
。
//新建一个len=5, cap=5的slice
sli := make([]byte, 5)
//新建一个len=0, cap=10的slice
sli2 := make([]int, 0, 10)
sli2 = []int{1, 2, 3, 4, 5} //len(sli2)=5, cap(sli2)=10
fmt.Println(sli2[0:3])//{1, 2, 3}
fmt.Println(sli2[:3])//{1, 2, 3}
fmt.Println(sli2[2:])//{3, 4, 5}
map
伟大领袖xxx曾经说过,有哈希的语言都是好语言,嗯,没错,这是一个很常用的数据结构,go这种现代语言当然是内置的。hash和数组/slice很像,就像一个key(下标)不一定是整数的数组。如果尝试取一个hash中没有的值,会返回值类型的零值。任何定义了相等操作的类型都可以作为key的类型。
mp := map[string]int {
"hello": 1,
"world": 2,//这里的,是允许的
}
//使用map的值很简单
fmt.Println(mp["hello"])
if v, exists := mp["no"]; exists {
fmt.Printf("exists %d\n", v);
} else {
fmt.Printf("not exists %d\n", v);//not exists 0
}
//删除一个k-v
delete(mp, "hello")
结构体
go没有java/c++里类的概念,但是它有结构体,和c里的结构体类似。结构体中首字母大写的字段可以在外部被访问,小写开头的变量不能被外部访问。
//定义一个结构体
type Intint struct {
A uint64
B uint64
}
//初始化赋值
ii := Intint{1, 2}
//指定某个成员
ii2 := Intint{A:1}//B=0
//全自动初始化为零值
ii3 := Intint{}
fmt.Println(unsafe.Sizeof(ii))//16
//声明并初始化赋值一个结构体指针
ip := &Intint{2, 3}
//使用new创建指针
ip2 := new(Intint)//type = *Intint
“枚举类型”
go不存在真正意义上的枚举类型,但可以通过自定义类型常量来实现,例如以下例子:
//自定义一个LogLevel类型作为enum的类型
type LogLevel int
const (
//iota表示枚举初始值,这里是整型,所以是0, 这里声明成LogLevel型,可以实现类型约束
LOG_L_DEBUG LogLevel = iota
LOG_L_NOTICE
LOG_L_WARNING
LOG_L_FATAL
)
语法结构
包
go使用package的概念来管理模块,可以有目录层次结构,导入使用import
关键字,如果涉及到目录层次结构,一般程序里可以直接用最后一个元素来指代。在源文件的开头以package
关键字声明这个程序属于哪个包。
//属于test包
package test
//引入俩包,第二个用rand.Xxxx来使用
import (
"fmt"
"math/rand"
)
包内全局变量,初始化函数
当需要定义一些复杂的全局变量时,可以直接在函数体外定义,赋值可以是一些运行时确定的表达式,包内可以有多个名为init的函数,这个函数在初始化全局变量后会执行。每个包的init函数只执行一次,并且全局变量也指初始化赋值1次。例如:
//./util/a.go
package util
import "./logger"
var (
A = 1
)
func What() int {
return logger.What()
}
//./util/logger/b.go
package logger
import "fmt"
var (
a = 0
)
func init() {
fmt.Println("init has been invoked.")
a++
}
func What() int {
return a
}
func SetWhat(v int) {
a = v
}
//./pkg.go
package main
import (
"./util"
"./util/logger"
"fmt"
)
func main() {
fmt.Println(logger.What())
logger.SetWhat(1000)
fmt.Println(logger.What())
fmt.Println(util.What())
}
//go run pkg.go
init has been invoked.
1
1000
1000
声明
golang是一门静态类型语言,支持类型推导,声明变量赋值可以采用平行赋值,比较方便。常量因为是编译时必须确定值,所以只能是基本数值类型或者字符串。比如:
var test int = 1
var a, b, c int = 1, 2, 3
// 类型推到之一
var needType = true
// 类型推到之二,等价于var msg string = "hello world!"
msg := "hello world!"
//常量声明
const pi float = 3.1415926
go对声明未赋值的变量统一初始化为其零值,如整数为0,字符串为空串””等等。
控制结构
if
go的if和c很像,就是没有那个小括号,并且强制要求{
必须在上一行的末尾,而不是独占一行。嗯,对于这个用法我很习惯啊,也终于结束了c/c++中{
到底在哪的争论。不过不写()还是有些不习惯的。因为go的switch非常强大,所以如果if的分支比较多时,推荐使用switch代替。
if a > b {
doSomething()
}
//也可以先赋值在判断条件
if f, err := os.Open(name); err != nil {
fmt.Println(err)
}
//多分支
if a < 0 {
doSth1()
} else if a == 0 {
doSth2()
} else {
doSth3()
}
switch
go的switch要比c强大一些,除了枚举数值之外,还可以加入一些条件判断,取代if-else-if-else的结构。与c不同的是没有fallthrough,多值的情况用,
分隔即可,也可以通过fallthrough
关键字强制向下执行。
switch {
case a<10 && a>0:
do1()
case a>=10 && a<100:
do2()
case a>=100 && a<1000:
do3()
default:
do4()
}
type switch
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
循环
go只有for一个循环关键字,while的功能也被for取代,用法也比c强大一些,还比较简洁。for-range操作时,一个基本单位是rune
,如果是一个中文字符串,那么每次迭代都是其unicode编码。
//最常见的
for i:=0; i<100; i++ {
do(i)
}
//我是while
i := 0
for i<100 {
do(i)
i++
}
//我是死循环
for {
do()
}
//foreach
arr := [3]int{1, 2, 3}
for i, v := range arr {
fmt.Printf("%d=>%d\n", i, v)
}
//1=>1\n2=>2\n3=>3\n
函数
和各种函数式编程语言一样,go也是first-class function,函数可以当做一个值来传递,也就有了各种匿名函数的用法。同时,自然也是支持闭包的。与c不同的是,go的函数可以返回多个值,这个用法在go里应用非常广泛,标准库中很多函数都是以错误作为第二个返回值返回的。另外go也支持命名返回值,也就是给出返回值的变量名,然后函数里可以直接赋值,最后直接return即可。
//一个2个参数2个返回值的函数
func some(a, b int) (int, int) {
return a+b, a*b
}
//不定参数
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
//匿名函数
f := func(a, b int) int {
return a+b
}
fmt.Println(f(5, 6))//11
闭包
但凡FP类的语言,都支持函数闭包。因为fp用的少,所以也不是特别清楚闭包的使用场景,大概意思就是函数实例化之后可以保存其局部变量,有点“小”对象的意思。来个斐波那契数列的例子吧。
//fibonacci是一个返回值类型为func() int的函数
func fibonacci() func() int {
//通过闭包实现保存局部变量a,b的值
a := 0
b := 1
return func() int {
sum := a + b
a = b
b = sum
return a
}
}
func main() {
//函数的“实例化”
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
方法
go是没有“类”的,但是你可以给任何本包内的结构体添加方法。和函数不同,方法有一个_method receiver_,也就是这个方法是属于哪个结构的。事实上除了结构体,go可以给本包内除了基本类型以外的任何类型添加方法。但需要注意,最好以指针类型作为method receiver,否则按值传递可能会起不到效果,并且造成效率降低。
//一个方法的例子
type TwoInt struct {
A int
B int
}
func (this *TwoInt)Sum() int {
return this.A + this.B
}
分配内存
go有两种分配内存的方式,new
和make
。
new
new(T)
返回一个*T
类型的指针,同时new
还将所有的值赋为零值。可以理解为分配了一块全为0的内存空间。这个特性比较有用,省去了c/c++自己赋零值的麻烦。当有需要初始化为非零值,也就是相当于构造函数的功能,go习惯上命名为NewT的函数。例如:
//一个“构造函数”的例子
func NewTwoInt(a, b int) *TwoInt {
return &TwoInt{a, b}
}
make
go中make用于分配slice
, map
, channel
,返回一个初始化(不一定是零值)的T类型的值,而new
返回的是指针。