Go 入门指南笔记 - 基本结构和基本数据类型 - 1

一、文件名、关键字与标识符

  • _ 被称为空白标识符,任何赋给这个标识符的值都将被抛弃,不能在后续的代码中使用。
  • 没有名称的变量、类型或方法,这些变量被统称为匿名变量。
  • 变量取名的时候注意避开 25 个关键字或保留字和 36 个预定义标识符即可。
  • 包名都应该使用小写字母。
  • 除了符号 _ 包中所有代码对象的标识符必须是唯一的,以避免名称冲突。但是相同的标识符可以在不同的包中使用,因为可以使用包名来区分它们。
# 25 个关键字
break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var
# 37 个保留字
Constants:    true  false  iota  nil

Types:    int  int8  int16  int32  int64
         uint  uint8  uint16  uint32  uint64  uintptr
         float32  float64  complex128  complex64
         bool  byte  rune  string  error

Functions:   make  len  cap  new  append  copy  close  delete
            complex  real  imag
            panic  recover

二、Go 程序的基本结构和要素

# 四种主要声明方式
var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)

2.1 包的概念、导入与可见性

  • 每个 Go 文件都属于且仅属于一个包,一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。
  • 必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main,其表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
  • 编译包名不是为 main 的源文件,如 pack1,编译后产生的对象文件将会是 pack1.a 而不是可执行程序。
  • Go 的标准库包含了大量的包,存放在 $GOROOT/pkg/$GOOS_$GOARCH/ 目录下。
  • 构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。
  • 包既是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。
  • 对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。
  • 导入多个包时,导入的顺序会按照字母排序。
  • 包名不是以 ./ 开头,Go 会在全局文件进行查找;以 ./ 开头,Go 会在相对目录中查找。
  • 导入包即等同于包含了这个包的所有的代码对象。
  • 导入了一个包却没有使用它,则会在构建程序时引发错误。

2.1.1 可见性规则

  • 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,就可以被外部包的代码所使用,这被称为 导出
  • 标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的。
  • 导入一个外部包后,能够且只能够访问该包中导出的对象。
  • 通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置。
package main

import fm "fmt" // alias

func main() {
   fm.Println("hello, world")
}

2.1.2 包的分级声明和初始化

使用 import 导入包之后定义或声明 0 个或多个常量 const、变量 var 和类型 type,这些对象的作用域都是全局的(在本包范围内),所以可以被本包中所有的函数调用,然后声明一个或多个函数 func。

2.2 函数

定义一个函数最简单的格式:func functionName()

  • 括号 () 中写入 0 个或多个函数的参数(使用逗号 , 分隔),每个参数的名称后面必须紧跟着该参数的类型。
  • 函数体使用大括号 {} 括起来。
  • 只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法;否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。
  • 当被调用函数的代码执行到结束符 } 或返回语句时就会返回,然后程序继续执行调用该函数之后的代码。
func functionName(parameter_list) (return_value_list) {
  //...
}
  • parameter_list 的形式为 (param1 type1, param2 type2, …)
  • return_value_list 的形式为 (ret1 type1, ret2 type2, …)

2.2.1 main 函数

  • main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
  • 如果你的 main 包的源代码没有包含 main 函数,则会引发构建错误
  • main 函数即没有参数,也没有返回类型

程序正常退出的代码为 0 即 Program exited with code 0;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序。

2.3 注释

  • 单行注释 //
  • 多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,一般用于包的文档描述或注释成块的代码片段。
  • 一个包可以分散在多个文件中,但是只需要在其中一个进行注释说明即可。
  • 几乎所有全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释。

2.4 类型

  • 类型定义了某个变量的值的集合与可对其进行操作的集合。
  • 可以包含数据的变量(或常量)可以使用不同的数据类型或类型来保存数据。
  • 使用 var 声明的变量的值会自动初始化为该类型的零值。
  • 类型可以是:
    • 基本类型,如:int、float、bool、string
    • 结构化的(复合的),如:struct、array、slice、map、channel
    • 只描述类型的行为的,如:interface
  • 结构化的类型没有真正的值,它使用 nil 作为默认值,Go 语言中不存在类型继承。
  • 函数也可以是一个确定的类型,就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后:func FunctionName (a typea, b typeb) typeFunc
  • 可以在函数体中的某处返回使用类型为 typeFunc 的变量 var:return var

2.4.1 定义和使用类型

使用 type 关键字可以定义你自己的类型,可能想要定义一个结构体,也可以定义一个已经存在的类型的别名,例如 type IZ int

这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。

var a IZ = 5 // int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能

如果你有多个类型需要定义,可以使用因式分解关键字的方式:

type (
   IZ int
   FZ float64
   STR string
)

每个值都必须在经过编译后属于某个类型(编译器必须能够推断出所有值的类型),因为 Go 语言是一种静态类型语言。

2.4.2 函数可以拥有多返回值

  • 返回类型之间需要使用逗号分割,并使用小括号 () 将它们括起来:func FunctionName (a typea, b typeb) (t1 type1, t2 type2)
  • 返回的形式:return var1, var2
  • 一般用于判断某个函数是否执行成功(true/false)或与其它返回值一同返回错误消息

2.5 Go 程序的一般结构

  1. 在完成包的 import 之后,开始对常量、变量和类型的定义或声明。
  2. 如果存在 init 函数的话,则对该函数进行定义(这是一个特殊的函数,每个含有该函数的包都会首先执行这个函数)。
  3. 如果当前包是 main 包,则定义 main 函数。
  4. 然后定义其余的函数,首先是类型的方法,接着是按照 main 函数中先后调用的顺序来定义相关函数,如果有很多函数,则可以按照字母顺序来进行排序。
package main

import (
   "fmt"
)

const c = "C"

var v int = 5

type T struct{}

func init() { // initialization of package
}

func main() {
   var a int
   Func1()
   // ...
   fmt.Println(a)
}

func (t T) Method1() {
   //...
}

func Func1() { // exported function Func1
   //...
}

2.6 类型转换

valueOfTypeB = typeB(valueOfTypeA)
  • 所有的转换都必须显式说明,就像调用一个函数一样。
  • 只能在定义正确的情况下转换成功,当从一个取值范围较大的转换到取值范围较小的类型时(例如将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失。
  • 编译器捕捉到非法的类型转换时会引发编译时错误,否则将引发运行时错误。
  • 具有相同底层类型的变量之间可以相互转换。
a := 5.0
b := int(a)

var a IZ = 5
c := int(a)
d := IZ(c)

2.7 Go 命名规范

  • 名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符
  • 返回某个对象的函数或方法的名称一般都是使用名词,没有 Get... 之类的字符
  • 用于修改某个对象,则使用 SetName
  • 有必须要的话可以使用大小写混合的方式,如 MixedCaps 或 mixedCaps,而不是使用下划线来分割多个名称

三、常量

  • 常量使用关键字 const 定义,用于存储不会改变的数据。
  • 存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
  • 常量的定义格式:const identifier [type] = value
  • 你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。
const Pi = 3.14159
  • 显式类型定义: const b string = "abc"
  • 隐式类型定义: const b = "abc"

一个没有指定类型的常量被使用时,会根据其使用环境而推断出它所需要具备的类型。换句话说,未定义类型的常量会在必要时刻根据上下文来获得相关类型。

var n int
f(n + 5) // 无类型的数字型常量 “5” 它的类型在这里变成了 int

常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

  • 正确的做法:const c1 = 2/3
  • 错误的做法:const c2 = getNumber() // 引发构建错误: getNumber() used as value

因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()。

  • 数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出
  • 反斜杠 \ 可以在常量表达式中作为多行的连接符使用。
  • 当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误。

常量也允许使用并行赋值的形式:

const beef, two, c = "meat", 2, "veg"
const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6
const (
    Monday, Tuesday, Wednesday = 1, 2, 3
    Thursday, Friday, Saturday = 4, 5, 6
)

常量还可以用作枚举:

const (
    Unknown = 0
    Female = 1
    Male = 2
)

iota

四、变量

  • 声明变量的一般形式是使用 var 关键字:var identifier type
  • 一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil
  • 变量的命名规则遵循骆驼命名法
  • 如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用
  • 函数体内声明的变量称之为局部变量,它们的作用域只在函数体内
  • 变量可以编译期间就被赋值,也可以在运行时对变量进行赋值操作
  • 一般情况下,只有类型相同的变量之间才可以相互赋值
  • 声明与赋值(初始化)语句也可以组合起来 var a int = 15
  • Go 编译器可以根据变量的值来自动推断其类型 var i = 5
  • 自动推断类型并不是任何时候都适用的,当你想要给变量的类型并不是自动推断出的某种类型时,你还是需要显式指定变量的类型 var n int64 = 2
  • 变量的类型也可以在运行时实现自动推断,例如:HOME = os.Getenv("HOME")
  • 在函数体内声明局部变量时,应使用简短声明语法 := 例如 a := 1,这叫做 初始化声明
  • 交换两个变量的值 a, b = b, a
  • _ 是一个只写变量,也用于抛弃值,这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。

var (
    a int
    b bool
    str string
)
package main

import (
    "fmt"
    "os"
)

func main() {
    var goos string = os.Getenv("GOOS")
    fmt.Printf("The operating system is: %s\n", goos)
    path := os.Getenv("PATH")
    fmt.Printf("Path is %s\n", path)
}

下面展示一个小技巧

// 同一类型的多个变量可以声明在同一行
var a, b, c int
// 多变量可以在同一行进行赋值
a, b, c = 5, 7, "abc"
// 更简短的写法
a, b, c := 5, 7, "abc"
// 你并不需要使用从一个函数得到的所有返回值
_, b = 5, 7
// 并行赋值也被用于当一个函数返回多个返回值时
val, err = Func1(var1)

4.1 错误的语法

  • var a 这种语法是不正确的,因为编译器没有任何可以用于自动推断类型的依据
  • 不可以声明了一个局部变量却没有在相同的代码块中使用它
  • 不可以多次初始化一个变量
  • 不可以不声明变量就使用它

4.2 值类型和引用类型

  • int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值
  • 数组 和 结构 这些复合类型也是值类型
  • 使用等号 = 将一个变量的值赋值给另一个变量时,j = i 实际上是在内存中将 i 的值进行了拷贝
  • 通过 &i 来获取变量 i 的内存地址,值类型的变量的值存储在栈中
  • 复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存
  • 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置,这个内存地址为称之为指针,这个指针实际上也被存在另外的某一个字中

4.3 init 函数

  • 不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
  • 每一个源文件都可以包含且只包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
  • 可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。
  • 经常被用在当一个程序开始之前调用后台执行的 goroutine
package trans

import "math"

var Pi float64

func init() {
   Pi = 4 * math.Atan(1) // init() function computes Pi
}
package main

import (
   "fmt"
   "./trans"
)

var twoPi = 2 * trans.Pi

func main() {
   fmt.Printf("2*Pi = %g\n", twoPi) // 2*Pi = 6.283185307179586
}

五、基本类型和运算符

  • 表达式是一种特定的类型的值,它可以由其它的值以及运算符组合而成。
  • 每个类型都定义了可以和自己结合的运算符集合,如果你使用了不在这个集合中的运算符,则会在编译时获得编译错误。
  • 一元运算符只可以用于一个值的操作(作为后缀),而二元运算符则可以和两个值或者操作数结合(作为中缀)。
  • 只有两个类型相同的值才可以和二元运算符结合
  • Go 是强类型语言,因此不会进行隐式转换,任何不同类型之间的转换都必须显式说明
  • Go 不存在像 C 和 Java 那样的运算符重载,表达式的解析顺序是从左至右

5.1 布尔类型

  • 布尔型的值只可以是常量 true 或者 false
  • 两个类型相同的值可以使用相等 == 或者不等 != 运算符来进行比较并获得一个布尔型的值
  • 两个的值的类型相同且值是完全相同,会返回 true
  • 只有两个类型相同的值才可以进行比较,如果值的类型是接口 interface,它们也必须实现了相同的接口
  • 如果其中一个值是常量,那么另外一个值的类型必须和该常量类型相兼容的
  • 如果以上条件都不满足,则其中一个值的类型必须在被转换为和另外一个值的类型相同之后才可以进行比较
  • 逻辑值可以被用于条件结构中的条件语句
  • 如果你有多个条件判断,应当将计算过程较为复杂的表达式放在运算符的右侧以减少不必要的运算
  • 布尔值(以及任何结果为布尔值的表达式)最常用在条件结构的条件语句中,例如:if、for 和 switch 结构
  • 好的命名能够很好地提升代码的可读性,例如以 is 或者 Is 开头的 isSorted、isFinished、isVisible

5.2 数字类型

  • 支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码
  • 也有基于架构的类型,例如:int、uint 和 uintptr,也就是根据运行程序所在的操作系统类型所决定的
    • int 和 uint 在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)
    • uintptr 的长度被设定为足够存放一个指针即可
  • Go 语言中没有 float 类型
  • 与操作系统架构无关的类型都有固定的大小
    • 整数:
      • int8(-128 -> 127)
      • int16(-32768 -> 32767)
      • int32(-2,147,483,648 -> 2,147,483,647)
      • int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)
    • 无符号整数
      • uint8(0 -> 255)
      • uint16(0 -> 65,535)
      • uint32(0 -> 4,294,967,295)
      • uint64(0 -> 18,446,744,073,709,551,615)
    • 浮点型(IEEE-754 标准)
      • float32(+- 1e-45 -> +- 3.4 * 1e38)
      • float64(+- 5 * 1e-324 -> 107 * 1e308)
  • int 型是计算最快的一种类型
  • 整型的零值为 0,浮点型的零值为 0.0
  • float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。由于精确度的缘故,你在使用 == 或者 != 来比较浮点数时应当非常小心
  • 尽可能地使用 float64,因为 math 包中所有有关数学运算的函数都会要求接收这个类型
  • 通过增加前缀 0 来表示 8 进制数(如:077),增加前缀 0x 来表示 16 进制数(如:0xFF),以及使用 e 来表示 10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)
  • 可以使用 a := uint64(0) 来同时完成类型转换和赋值操作,这样 a 的类型就是 uint64
  • 不允许不同类型之间的混合使用,但是对于常量的类型限制非常少,因此允许常量之间的混合使用
package main

func main() {
    var a int
    var b int32
    a = 15
    b = a + a    // 编译错误
    b = b + 5    // 因为 5 是常量,所以可以通过编译
}
package main

import "fmt"

func main() {
    var n int16 = 34
    var m int32
    // compiler error: cannot use n (type int16) as type int32 in assignment
    //m = n
    m = int32(n)

    fmt.Printf("32 bit int is: %d\n", m)
    fmt.Printf("16 bit int is: %d\n", n)
}

当进行类似 a32bitInt = int32(a32Float) 的转换时,小数点后的数字将被丢弃。你可以写一个专门用于处理类型转换的函数来确保没有发生精度的丢失。

// 安全地从 int 型转换为 int8
func Uint8FromInt(n int) (uint8, error) {
    if 0 <= n && n <= math.MaxUint8 { // conversion is safe
        return uint8(n), nil
    }
    return 0, fmt.Errorf("%d is out of the uint8 range", n)
}
// 安全地从 float64 转换为 int
func IntFromFloat64(x float64) int {
    if math.MinInt32 <= x && x <= math.MaxInt32 { // x lies in the integer range
        whole, fraction := math.Modf(x)
        if fraction >= 0.5 {
            whole++
        }
        return int(whole)
    }
    panic(fmt.Sprintf("%g is out of the int32 range", x))
}
  • Go 拥有以下复数类型
    • complex64 (32 位实数和虚数)
    • complex128 (64 位实数和虚数)
  • 复数使用 re+imI 来表示,其中 re 代表实数部分,im 代表虚数部分,I 代表根号负 1。
  • 函数 real(c)imag(c) 可以分别获得相应的实数和虚数部分。
var c1 complex64 = 5 + 10i
fmt.Printf("The value is: %v", c1)
// 输出: 5 + 10i

// 如果 re 和 im 的类型均为 float32,那么类型为 complex64 的复数 c 可以通过以下方式来获得:
c = complex(re, im)

5.3 运算符与优先级

二元运算符的运算方向均是从左至右。可以通过使用括号来临时提升某个表达式的整体运算优先级。

  • http://www.runoob.com/go/go-operators.html

5.4 类型别名

  • 你在使用某个类型时,你可以给它起另一个名字,然后你就可以在你的代码中使用新的名字(用于简化名称或解决名称冲突)
  • type TZ int 中,TZ 就是 int 类型的新名称(用于表示程序中的时区),然后就可以使用 TZ 来操作 int 类型的数据
package main
import "fmt"

type TZ int

func main() {
    var a, b TZ = 3, 4
    c := a + b
    fmt.Printf("c has the value: %d", c) // 输出:c has the value: 7
}

5.5 字符类型

  • 字符只是整数的特殊用例
  • byte 类型是 uint8 的别名,
  • \x 总是紧跟着长度为 2 的 16 进制数
  • \ 后面紧跟着长度为 3 的十进制数,例如:\377
  • Go 同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。
    • 在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数
    • rune 也是 Go 当中的一个类型,并且是 int32的别名
    • 书写 Unicode 字符时,需要在 16 进制数之前加上前缀 \u 或者 \U
    • Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。
    • 如果需要使用到 4 字节,则会加上 \U 前缀
    • 前缀 \u 则总是紧跟着长度为 4 的 16 进制数,前缀 \U 紧跟着长度为 8 的 16 进制数。
// 下面写法是等效的
var ch byte = 'A'
var ch byte = 65
var ch byte = '\x41'

六、字符串

  • 字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)
  • 字符串分两种:
    • 解释字符串:使用双引号括起来,其中的相关的转义字符将被替换 \n, \r, \t, \u, \U, \\
    • 非解释字符串:该类字符串使用反引号括起来,支持换行
  • Go 中的字符串是根据长度限定,而非特殊字符 \0
  • string 类型的零值为长度为零的字符串,即空字符串 ""
  • 一般的比较运算符 ==、!=、<、<=、>=、> 通过在内存中按字节比较来实现字符串的对比
  • 通过函数 len() 来获取字符串所占的字节长度,例如:len(str)
  • 字符串的内容(纯字节)可以通过标准索引法来获取,在中括号 [] 内写入索引,索引从 0 开始计数
    • 这种转换方案只对纯 ASCII 码的字符串有效
    • 获取字符串中某个字节的地址的行为是非法的 &str[i]
  • 字符串拼接符使用 ++=,更好的办法是使用函数 strings.Join() 或者使用字节缓冲 bytes.Buffer
// 非解释字符串 `\n\` 会被原样输出
`This is a raw string \n`

// 字符串拼接符
str := "Beginning of the string " +
    "second part of the string"

s := "hel" + "lo,"
s += "world!"
fmt.Println(s) //输出 “hello, world!”

相关

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @Victor Mar 1, 2019

奉献爱心