Go语言基础数据类型 | 青训营笔记

115 阅读11分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第1天

emmm,学习内容很多,课程时间有限,介绍的知识也是有限的。这里结合《Go语言圣经》进行学习并详细的记录下来了。最重要的新大陆发现Go语言支持中文变量(使用UTF-8编码)!! 回头就写两个中文变量然后被人打死(bushi)

一、程序结构

声明

Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。

包声明语句之后是import语句导入依赖的其它包,然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问,而不是仅仅在其声明语句所在的源文件中访问。

变量声明

  1. var声明语法:var 变量名字 类型 = 表达式
  • 省略类型信息将根据初始化表达式来推导变量的类型信息。

  • 初始化表达式被省略,那么将用零值初始化该变量。(go语言不存在没有初始化的变量)

    • 数值类型变量对应的零值是0
    • 布尔类型变量对应的零值是false
    • 字符串类型对应的零值是空字符串
    • 接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。
    • 数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
    var i, j, k int                 // int, int, int
    var b, f, s = true, 2.3, "four" // bool, float64, string
    
  1. 简短变量声明:名字:=表达式

    用于声明和初始化局部变量

    freq:=rand.Float64()
    i, j := 0, 1
    

    简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

    简短变量声明左边的变量可能并不是全部都是刚刚声明的。 如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。

    in, err := os.Open(infile)
    // ...
    out, err := os.Create(outfile)
    

    简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:

    f, err := os.Open(infile)
    // ...
    f, err := os.Create(outfile) // compile error: no new variables
    

    如果变量作用域不一样,简短变量声明会生成一个新的变量。

类型声明

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。(无法一起比较运算)

type 类型名字 底层类型

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

变量声明周期

变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。

变量的逃逸:

var global *int
func f() {
    var x int
    x = 1
    global = &x
}
func g() {
    y := new(int)
    *y = 1
}

f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到。用Go语言的术语说,这个x局部变量从函数f中逃逸了。

如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

包与文件

每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decodeutf16.Decode形式访问

包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(因为汉字不区分大小写,因此汉字开头的名字是没有导出的)

二、基础数据类型

整数

Go语言同时提供了有符号和无符号类型的整数运算。

int8int16int32int64uint8uint16uint32uint64

Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。

浮点数

float32float64

一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大

var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1)    // "true"!

很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分:

const Avogadro = 6.02214129e23  // 阿伏伽德罗常数
const Planck   = 6.62606957e-34 // 普朗克常数

函数math.IsNaN用于测试一个数是否是非数NaN,math.NaN则返回非数对应的值。

但是在浮点数中,NaN、正无穷大和负无穷大都不是唯一的,每个都有非常多种的bit模式表示,所以哪怕NaN和自身比较也不相等

nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"

复数

Go语言提供了两种精度的复数类型:complex64complex128,分别对应float32float64两种浮点数精度。

var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y)                 // "(-5+10i)"
fmt.Println(real(x*y))           // "-5"
fmt.Println(imag(x*y))           // "10"

在常量算术规则下,一个复数常量可以加到另一个普通数值常量(整数或浮点数、实部或虚部)

复数也可以用==!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的

布尔类型

同C一样,falsetrue

字符串

文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列

内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目)

索引操作s[i]返回第i个字节的字节,所以如果是中文字符串(中文字符一般是两个字节),取第i个字节只能得到半个字。

字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的

s[0] = 'L' // compile error: cannot assign to s[0]

UTF-8标准:UTF8是一个将Unicode码点编码为字节序列的变长编码。

UTF8编码使用1到4个字节来表示每个Unicode码点,每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。

UTF-8和传统的ASCII编码兼容,如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。

0xxxxxxx                             runes 0-127    (ASCII)
110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)

PS:Go语言的源文件采用UTF8编码,也就是说go语言的变量里使用中文、日文、阿拉伯文等等都是合法的

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。

bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。

strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。

unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。

常量

使用const关键字,可以批量声明

const (
    e  = 2.7182
    pi = 3.1416
)

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。

const (
    a = 1
    b
    c = 2
    d
)
​
fmt.Println(a, b, c, d) // "1 1 2 2"

常量生成器:常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

type Weekday intconst (
    Sunday Weekday = iota //0
    Monday //1
    Tuesday //2
    Wednesday //3
    Thursday //4
    Friday //5
    Saturday //6
)

三、复合数据类型

数组和切片

数组是固定长度的特定类型元素组成的序列。同C语言的数组一样,是定长的,无法改变长度。因此大多情况下用的都是可变长的Slice。

数组初始化:

var a [3]int
var q [3]int = [3]int{1, 2, 3}
q := [...]int{1, 2, 3}\省略号表示根据初始化自动计算长度

也可以指定一个索引和对应值列表的方式初始化

type Currency intconst (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)
​
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

在这中情况下初始化顺序是无关紧要的,没用到的索引可以省略,自动初始化为零值。

在函数参数为数组进行传递这方面,go语言同C、C++一样,如果不进行显式指定为指针,传递的将会是数组的副本,这同python是不同的。

Slice(切片)是go语言的一个术语,在python中切片指的是对数据的一种操作,但是在go中指的是一种数据类型。为了区分下面使用slice:

Slice初始化:一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

Slice的切片操作同python一样,但是不支持负数索引,同时Slice切片生成的新Slice与Slice底层都是共享之前的底层数组,这同python生成新的list不同

Slice追加元素:使用append函数,但是由于不知道底层内存扩展的策略(可能生成新的Slice,也可能再原来的Slice上扩展),所以将函数返回值赋予原来的变量。

m = append(m,4)
m = append(m,5,6)

Map

定义/初始化:

//
ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}
//
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34

删除元素

delete(ages, "alice")

map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作

_ = &ages["bob"] // compile error: cannot take address of map element

要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,但是遍历顺序是随机的,基于底层hash的实现

for name, age := range ages {
    fmt.Printf("%s\t%d\n", name, age)
}

如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。

import "sort"
​
names := make([]string, 0, len(ages))
for name := range ages {
    names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
    fmt.Printf("%s\t%d\n", name, ages[name])
}

结构体

定义

type Employee struct {
    ID        int
    Name      string
    Address   string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

成员变量访问

  • dilbert结构体变量的成员可以通过点操作符访问,比如dilbert.Namedilbert.DoB

  • 对成员取地址,然后通过指针访问

    position := &dilbert.Position
    *position = "Senior " + *position // promoted, for outsourcing to Elbonia
    
  • 同时不使用C语言中的->,统一使用点操作符,哪怕是指针变量

    var employeeOfTheMonth *Employee = &dilbert
    employeeOfTheMonth.Position += " (proactive team player)"
    

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。

初始化

  • 顺序赋值,直接指定变量的值

  • 根据变量名指定值

    type Point struct{ X, Y int }
    ​
    p := Point{1, 2}
    p := Point{X:2}
    

不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员,即外部包不可见未导出的成员变量,所以赋值失败。

package p
type T struct{ a, b int } // a and b are not exportedpackage q
import "p"
var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
var _ = p.T{1, 2}       // compile error: can't reference a, b

结构体比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的。会依次比较变量。

结构体嵌入和匿名成员

Go语言提供的不同寻常的结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员,这样就可以通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。

不使用匿名成员,访问需要完整的路径

type Point struct {
    X, Y int
}
type Circle struct {
    Center Point
    Radius int
}
type Wheel struct {
    Circle Circle
    Spokes int
}
var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20

只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员

type Circle struct {
    Point
    Radius int
}
​
type Wheel struct {
    Circle
    Spokes int
}
var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20
//上述注释的访问依旧有效

结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过

w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

需要如下定义:

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

JSON

JSON可以定义如下结构体

type Movie struct {
    Title  string
    Year   int  `json:"released"`
    Color  bool `json:"color,omitempty"`
    Actors []string
}

在结构体声明中,Year和Color成员后面的字符串面值是结构体成员Tag

json开头键名对应的值用于控制encoding/json包的编码和解码的行为。

成员Tag中json对应值的第一部分用于指定JSON对象的名字。Color成员的Tag还带了一个额外的omitempty选项,表示当Go语言结构体成员为空或零值时不生成该JSON对象(这里false为零值)。

var movies = []Movie{
    {Title: "Casablanca", Year: 1942, Color: false,
        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
    {Title: "Cool Hand Luke", Year: 1967, Color: true,
        Actors: []string{"Paul Newman"}},
    {Title: "Bullitt", Year: 1968, Color: true,
        Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
    // ...
}

这样的数据结构特别适合JSON格式,并且在两者之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成:

data, err := json.Marshal(movies)
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
/**
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingrid Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Actors":["Paul Newman"]}{"Title":"Bullitt","released":1968,"color":true,"Actors":["Steve McQueen","Jacqueline Bisset"]}]
*//
​

可以发现输出紧凑,难以阅读。

为了生成便于阅读的格式,另一个json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:

data, err := json.MarshalIndent(movies, "", "    ")
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
/**
[
    {
        "Title": "Casablanca",
        "released": 1942,
        "Actors": [
            "Humphrey Bogart",
            "Ingrid Bergman"
        ]
    },
    {
        "Title": "Cool Hand Luke",
        "released": 1967,
        "color": true,
        "Actors": [
            "Paul Newman"
        ]
    },
    {
        "Title": "Bullitt",
        "released": 1968,
        "color": true,
        "Actors": [
            "Steve McQueen",
            "Jacqueline Bisset"
        ]
    }
]
**/

在编码时,默认使用Go语言结构体的成员名字作为JSON的对象。只有导出的结构体成员才会被编码,所以选择用大写字母开头的成员名称。

四、实践例子

实践例子中主要通过三个例子来让我们体会go语言。

  1. 使用猜数字程序来体会go语言输入输出的规范与go语言if语句与循环语句的使用。
  2. 通过在线词典程序,将json数据应用到具体实践中,更好的体会json数据和学习如何在网络上抓取json数据包。
  3. 通过编写socks5协议的代理服务器程序来了解如何通过go语言编写程序以及开启多个协程。

五、总结体会

初次体验青训营学习,感觉学习氛围良好,通过与其他语言对比,也学习到了go语言的不同之处与自身的优势。

六、参考资料