第一个Go程序

  1. package main
  2. import "fmt"
  3. func main(){
  4. fmt.Println("Hello world!")
  5. }

运行

Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令。Go语言提供的工具都通过一个单独的命令go调用,go命令有一系列子命令。

  • gorun:直接运行.go的文件
  • gobuild:编译成可执行文件 ```shell go run ./src/helloworld.go

    Hello world!

go build ./src/helloworld.go ./helloworld

Hello world!

<a name="GTnP1"></a>
### 代码结构
<a name="JBkVm"></a>
### package
`Go`语言的代码通过包`(package)`组织,包类似于其它语言里的库`(libraries)`或者模块`(modules)`。**一个包由位于单个目录下的一个或多个**`**.go**`**源代码文件组成,目录定义包的作用。每个源文件都以一条**`**package**`**声明语句开始,这个例子里就是**`**packagemain**`**,表示该文件属于哪个包,紧跟着一系列导入**`**(import)**`**的包,之后是存储在这个文件里的程序语句**
<a name="pW3K1"></a>
### import
必须告诉编译器源文件需要哪些包,这就是import声明以及随后的`package`声明扮演的角色。`helloworld`例子只用到了一个包,大多数程序需要导入多个包
<a name="Jli3O"></a>
### main
`main`包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在`main`里的`main`函数也很特殊,它是整个程序执行时的入口7。`main`函数所做的事情就是程序做的。当然了,`main`函数一般调用其它包里的函数完成很多工作,比如`fmt.Println`。
<a name="RcY67"></a>
### func
一个函数的声明由`**func**`**关键字、函数名、参数列表、返回值列表**(这个例子里的main函数参数列表和返回值都是空的)以及包含在大括号里的**函数体**组成。
<a name="FSKRB"></a>
## 命令行参数
大多数的程序都是**处理输入,产生输出**;但是,程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。<br />`os`包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从`os`包的`Args`变量获取;`os`包外部使用`os.Args`访问该变量。

- `os.Args`变量是一个字符串`(string)`的切片`(slice)`
- 用`s[i]`访问单个元素,用`s[m:n]`获取子序列,和`python`基本一样
- 左闭右开
- `os.Args`的第一个元素,`os.Args[0]`,是命令本身的名字;其它的元素则是程序启动时传给它的参数

如下面的代码会输出每个传入命令的信息

- `var`声明定义了两个`string`类型的变量`s`和`sep`。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的零值`(zerovalue)`,数值类型是`0`,字符串类型是空字符串`""`
```go
package main

import(
    "fmt"
    "os"
)

func main(){
    var s, sep string 
    for i:= 1; i < len(os.Args); i++{
        s += sep + os.Args[i] // + 运算符连接字符串
        sep = " "
    }
    fmt.Println(s)
}
## run
go run ./src/args.go arg1 arg2 arg3
## out
arg1 arg2 arg3

for

**Go**语言只有**for**循环这一种循环语句。for循环有多种形式,其中一种如下所示

for initialization; condition; post {
// zero or more statements
}
  • **initialization**:在循环开始前执行(可选),必须是一条简单语句(simplestatement),即,短变量声明、自增语句、赋值语句或函数调用
  • **condition**:布尔表达式booleanexpression其值在每次循环迭代开始时计算。如果为true则执行循环体语句
  • **post**:post语句在循环体执行结束后执行,之后再次对conditon求值。condition值为false

for循环的三个部分均可以省略,因此还有以下几种方式:

  1. 保留condition,实现while的功能

    for condition {
    // zero or more statements
    }
    
  2. 全部不保留:无限循环whileTrue,可以使用return``break打断

    for  {
    // zero or more statements
    }
    
  3. 某种数据类型的区间(range)上遍历,如字符串或切片,例如下面(**range**会同时返回元素的值和**index**因此使用**_**占位) ```go package main

import ( “fmt” “os” )

func main(){ seq := “ “ s := “” for _, arg := range os.Args[1:] { s += seq + arg } fmt.Println(s) }

连接字符还有一种更高效的办法,`strings`中的`Join`
```go
...
import (
    ...
    "strings"
)

func main() {
    fmt.Println(strings.Join(os.Args[1:], " "))
}

var

使用一条短变量声明来声明并初始化上述代码的sseps,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价,实践中一般使用前两种形式中的某个初始值重要的话就显式地指定变量的类型,否则使用隐式初始化

s := "" // 只可以用于函数内部,包内变量不可以
var s string // 利用初始化机制,s = ""
var s = "" // 用的少,除非一次声明多个变量
var s string = "" // 显式地标明变量的类型, 当变量类型与初值类型相同时, 类型冗余, 但如果两者类型不同, 变量类型就必须了

文件输入(查找重复行)

对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。

查找重复行

第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入if语句,map数据类型以及bufio

package main

import (
    "fmt"
    "os"
    "bufio"
)

func main(){
    counts := make(map[string]int)
    input := bufio.NewScanner(os.Stdin)
    for input.Scan() {
        counts[input.Text()]++
    }
    for line, n := range counts {
        if n > 1 {
            fmt.Println("%d\t\n", n, line)
        }
    }
}

if

for循环一样,if语句条件两边也不加括号,但是主体部分需要加。if语句的else部分是可选的,在if的条件为false时执行

map

map存储了键/值(key/value)的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型(最常见的是字符串),只要其值能用==运算符比较,和python中的dict类似

//每次 dup 读取一行输入, 该行被当做 map , 其对应的值递增。
counts[input.Text()]++

map自带默认值(类似于pythondefaultdict),首次读到新行时,等号右边的表达式counts[line]的值被计算为其类型的零值,对于int0

range

为了打印结果,我们使用了基于range的循环,并在counts这个map上迭代。跟之前类似,每次迭代得到两个结果,键和其在map中对应的值。map的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。

bufio

Scanner类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法,用短变量声明创建**bufio.Scanner**类型的变量**input**。类似于创建了**bufio.Scanner**的一个实例?

input := bufio.NewScanner(os.Stdin)

读取过程

for input.Scan() {
        counts[input.Text()]++
    }
  1. 每次调用input.Scanner,即读入下一行,并移除行末的换行符
  2. 读取的内容可以调用input.Text()得到
  3. Scan函数在读到一行时返回true,在无输入时返回false

    printf

    格式化输出
    fmt.Println("%d\t\n", n, line)
    
    image.png

    从文件中读取内容

    按行读取处理

    使用os.Open打开某个文件 ```go package main

import ( “fmt” “bufio” “os” )

func main() { counts := make(map[string]int) files := os.Args[1:] if len(files) == 0 { countLines(os.Stdin, counts) } else { for _, arg := range files { f, err := os.Open(arg) if err != nil { fmt.Fprintf(os.Stderr, “dup2: %v\n”, err) continue } countLines(f, counts) f.Close() } } for line, n := range counts { if n > 1 { fmt.Printf(“%d\t%s\n”, n, line) } } }

func countLines(f *os.File, counts map[string]int) { input := bufio.NewScanner(f) for input.Scan() { counts[input.Text()]++ } }

读取过程<br />`os.Open`函数返回**两个值**。

- 被打开的文件`(*os.File)`,其后被`Scanner`读取
- 内置`error`类型的值。如果`err`等于内置值`nil`(`NULL`),那么文件被成功打开。读取文件,直到文件结束,然后调用`Close`关闭该文件,并释放占用的所有资源。相反的话,如果`err`的值不是`nil`,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。

使用`Fprintf`与表示任意类型默认格式值的动词`%v`,向标准错误流打印一条信息,然后程序继续处理下一个文件;`continue`语句直接跳到`for`循环的下个迭代开始执行
```go
...
for _, arg := range files {
            f, err := os.Open(arg)
            if err != nil {
                fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
                continue
                ...

map是一个由make函数创建的数据结构的引用。map作为为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy,或译为副本),被调用函数对map底层数据结构的任何修改,调用者函数都可以通过持有的**map**引用看到。在我们的例子中,countLines函数向counts插入的值,也会被main函数看到

读取全部内容后进行处理

ReadFile函数(来自于io/ioutil包),其读取指定文件的全部内容,strings.Split函数把字符串分割成子串的切片。
由于ReadFile函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回main函数

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "strings"
)

func main() {
    counts := make(map[string]int)
    for _, filename := range os.Args[1:] {
        data, err := ioutil.ReadFile(filename)
        if err != nil {
            fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
            continue
        }
        // ReadFile 函数返回一个字节切片(byte slice), 
        // 必须把它转换为string,才能用strings.Split分割
        for _, line := range strings.Split(string(data), "\n"){
            counts[line]++
        }
    }
    for line, n := range counts {
        if n > 1 {
            fmt.Printf("%d\t%s\n", n, line)
        }
    }
}

switch

一个例子, coinflip函数返回几种不同的结果, 每一个case都会对应一个返回结果, 这里需要注意, Go语言并不需要显式地在每一个case后写break, 语言默认执行完case后的逻辑语句会自动退出。

switch coinflip() {
case "heads":
    heads++
case "tails":
    tails++
default:
    fmt.Println("landed on edge!")
}

switch也可不跟操作对象(如 函数),此时默认用true代替,case中的每个表达式与true比较, 称为tag switch(tagless switch); 和switch true是等价的,switch也可以紧跟一个简短的变量声明, 一个自增表达式、 赋值语句, 或者一个函数调用

func Signum(x int) int {
    switch {
    case x > 0:
        return +1
    default:
        return 0
    case x < 0:
        return -1
    }
}

break continue

breakcontinue语句会改变控制流。 和其它语言中的breakcontinue一样,

  • break会中断当前的循环, 并开始执行循环之后的内容
  • continue会中跳过当前循环, 并开始执行下一次循环

    指针

    Go语言提供了指针。 指针是一种直接存储了变量的内存地址的数据类型。 在其它语言中, 比如C语言, 指针操作是完全不受约束的。 在另外一些语言中, 指针一般被处理为“引用”, 除了到处传递这些指针之外, 并不能对这些指针做太多事情。 **Go**语言在这两种范围中取了一种平衡。 指针是可见的内存地址, &操作符可以返回一个变量的内存地址, 并且*****操作符可以获取指针指向的变量内容, 但是在**Go**语言里没有指针运算, 也就是不能像**C**语言里可以对指针进行加或减操作。

    命名类型

    类型声明使得我们可以很方便地给一个特殊类型一个名字。 因为struct类型声明通常非常地长, 所以我们总要给这种struct取一个名字。

    type Point struct {
      X, Y int
    } 
    var p Point
    

    方法和接口

  • 方法是和命名类型关联的一类函数Go语言里比较特殊的是方法可以被关联到任意一种命名类型。有点像RS3 method?

  • 接口是一种抽象类型, 这种类型可以让我们以同样的方式来处理不同的固有类型, 不用关心它们的具体实现, 而只需要关注它们提供的方法。