Golang学习笔记——基础


平时工作语言是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

布尔类型,truefalse,没啥好说的

整型

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有两种分配内存的方式,newmake

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返回的是指针。

Reference

  1. Go 语言简介(上)— 语法
  2. Go 语言简介(下)— 特性
  3. A Tour of Go(FuckGFW)
  4. Go by Example
  5. Package Documentation(FuckGFW)
  6. Effective Go(FuckGFW)
  7. 学习Go语言
  8. The Go Programming Language Specification(FuckGFW)

文章作者: Odin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Odin !
  目录