跳至主要內容

基本语法

寒江蓑笠翁2022年8月12日大约 18 分钟

基本语法

Go 的基本语法十分简单易懂,让我们从一个最简单的例子开始。

package main

import "fmt"

func main() {
   fmt.Println("Hello 世界!")
}

以上就是一个简单的语法介绍,下面就来略微细致地去了解里面的概念。

在 Go 中,程序是通过将包链接在一起来构建的。Go 中进行导入的最基本单位是一个包,而不是.go文件。包其实就是一个文件夹,英文名 package,包内共享所有变量,常量,以及所有定义的类型。包的命名风格建议都是小写字母,并且要尽量简短。

可见性

前面提到过包内共享所有变量,常量,以及所有定义的类型,但对于包外而言并不是这样,有时候你并不想让别人访问某一个类型,所以就需要控制可见性。你可能在其它 OOP 语言中见过PublicPravite等关键字,不过在 Go 语言中没有这些,它控制可见性的方式非常简单,规则如下

比如下面的一个例子,常量MyName就是公开的,而常量mySalary就是私有的。

package example

// 公有
const MyName = "jack"

// 私有
const mySalary = 20_000

这个可见性的规则适用于整个 Go 语言的任何地方。

导入

导入一个包就是导入这个包的所有公有的类型/变量/常量,导入的语法就是import加上包名

package main

import "example"

当导入多个包时,你可以这么写

package main

import "example"
import "example1"

也可以用括号括起来,第二种方法在实践中更加常用。

package main

import (
  "example"
  "example1"
)

如果有包名重复了,或者包名比较复杂,你也可以给它们起别名

package main

import (
  e "example"
  e1 "example1"
)

还有另一种特殊的使用方法就是匿名导入包,匿名导入的包无法被使用,这么做通常是为了加载包下的init函数,但又不需要用到包中的类型,例如一个常见的场景就是注册数据库驱动

package main

import (
  e "example"
  _ "mysql-driver"
)

当你导入后,想要访问包中的类型时,通过名称.标识符去访问即可,比如下面这个例子

package main

import (
  "example"
    "fmt"
)

func main() {
    fmt.Println(example.MyName)
}

若你尝试去访问一个私有的类型,编译器就会告诉你无法访问。

注意

Go 中无法进行循环导入,不管是直接的还是间接的。例如包 A 导入了包 B,包 B 也导入了包 A,这是直接循环导入,包 A 导入了包 C,包 C 导入了包 B,包 B 又导入了包 A,这就是间接的循环导入,存在循环导入的话将会无法通过编译。

内部包

go 中约定,一个包内名为internal 包为内部包,外部包将无法访问内部包中的任何内容,否则的话编译不通过,下面看一个例子。

/home/user/go/
    src/
        crash/
            bang/              (go code in package bang)
                b.go
        foo/                   (go code in package foo)
            f.go
            bar/               (go code in package bar)
                x.go
            internal/
                baz/           (go code in package baz)
                    z.go
            quux/              (go code in package main)
                y.go

由文件结构中可知,crash包无法访问baz包中的类型。

注释

Go 支持单行注释和多行注释,注释与内容之间建议隔一个空格,例如

// 这是main包
package main

// 导入了fmt包
import "fmt"

/*
*
这是启动函数main函数
*/
func main() {
  // 这是一个语句
  fmt.Println("Hello 世界!")
}

标识符

标识符就是一个名称,用于包命名,函数命名,变量命名等等,命名规则如下:

下方列出所有的内置关键字,也可以前往参考手册-标识符查看更多细节

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

运算符

下面是 Go 语言中支持的运算符号的优先级排列,也可以前往参考手册-运算符查看更多细节。

Precedence    Operator
    5             *  /  %  <<  >>  &  &^
    4             +  -  |  ^
    3             ==  !=  <  <=  >  >=
    2             &&
    1             ||

有一点需要稍微注意下,go 语言中没有选择将~作为取反运算符,而是复用了^符号,当两个数字使用^时,例如a^b,它就是异或运算符,只对一个数字使用时,例如^a,那么它就是取反运算符。go 也支持增强赋值运算符,如下。

a += 1
a /= 2
a &^= 2

提示

Go 语言中没有自增与自减运算符,它们被降级为了语句statement,并且规定了只能位于操作数的后方,所以不用再去纠结i++++i这样的问题。

a++ // 正确
++a // 错误
a-- // 正确

还有一点就是,它们不再具有返回值,因此a = b++这类语句的写法是错误的。

字面量

字面量,按照计算机科学的术语来讲是用于表达源代码中一个固定值的符号,也叫字面值。两个叫法都是一个意思,写了什么东西,值就是什么,值就是“字面意义上“的值。

整型字面量

为了便于阅读,允许使用下划线_来进行数字划分,但是仅允许在前缀符号之后数字之间使用。

24 // 24
024 // 24
2_4 // 24
0_2_4 // 24
10_000 // 10k
100_000 // 100k
0O24 // 20
0b00 // 0
0x00 // 0
0x0_0 // 0

浮点数字面量

通过不同的前缀可以表达不同进制的浮点数

0.
72.40
072.40       // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5
1_5.         // == 15.0
0.15e+0_2    // == 15.0

0x1p-2       // == 0.25
0x2.p10      // == 2048.0
0x1.Fp+0     // == 1.9375
0X.8p-0      // == 0.5
0X_1FFFP-16  // == 0.1249847412109375
0x15e-2      // == 0x15e - 2 (integer subtraction)

复数字面量

0i
0123i         // == 123i
0o123i        // == 0o123 * 1i == 83i
0xabci        // == 0xabc * 1i == 2748i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i
0x1p-2i       // == 0x1p-2 * 1i == 0.25i

字符字面量

字符字面量必须使用单引号括起来'',Go 中的字符完全兼容utf8

'a'
'ä'
''
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'

转义字符

Go 中可用的转义字符

\a   U+0007 响铃符号
\b   U+0008 回退符号
\f   U+000C 换页符号
\n   U+000A 换行符号
\r   U+000D 回车符号
\t   U+0009 横向制表符号
\v   U+000B 纵向制表符号
\\   U+005C 反斜杠转义
\'   U+0027 单引号转义 (该转义仅在字符内有效)
\"   U+0022 双引号转义 (该转义仅在字符串内有效)

字符串字面量

字符串字面量必须使用双引号""括起来或者反引号(反引号字符串不允许转义)

`abc`                // "abc"
`\n
\n`                  // "\\n\n\\n"
"\n"
"\""                 // `"`
"Hello, world!\n"
"今天天气不错"
"日本語"
"\u65e5\U00008a9e"
"\xff\u00FF"

函数

Go 中的函数声明方式通过func关键字来进行,跟大多数语言类似

func main() {
  println(1)
}

不过 Go 中的函数有两个不同的点,第一个是参数类型后置,像下面这样

func Hello(name string) {
  fmt.Println(name)
}

第二个不同的点就是多返回值,而且可以带名字

func Pos() () (x, y float64) {
    ...
}

风格

关于编码风格这一块 Go 是强制所有人统一同一种风格,Go 官方提供了一个格式化工具gofmt,通过命令行就可以使用,该格式化工具没有任何的格式化参数可以传递,仅有的两个参数也只是输出格式化过程,所以完全不支持自定义,也就是说所有通过此工具的格式化后的代码都是同一种代码风格,这会极大的降低维护人员的心智负担,所以在这一块追求个性显然是一个不太明智的选择。

下面会简单列举一些规则,平时在编写代码的时候也可以稍微注意一下。

函数花括号换行

关于函数后的花括号到底该不该换行,几乎每个程序员都能说出属于自己的理由,在 Go 中所有的花括号都不应该换行

// 正确示例
func main() {
  fmt.Println("Hello 世界!")
}

如果你真的这么做了,像下面这样

// 错误示例
func main()
{
  fmt.Println("Hello 世界!")
}

这样的代码连编译都过不了,所以 Go 强制所有程序员花函数后的括号不换行。

代码缩进

Go 默认使用Tab也就是制表符进行缩进,仅在一些特殊情况会使用空格。

代码间隔

Go 中大部分间隔都是有意义的,从某种程度上来说,这也代表了编译器是如何看待你的代码的,例如下方的数学运算

2*9 + 1/3*2

众所周知,乘法的优先级比加法要高,在格式化后,*符号之间的间隔会显得更紧凑,意味着优先进行运算,而+符号附近的间隔则较大,代表着较后进行运算。

花括号省略

在其它语言中的 if 和 for 语句通常可以简写,像下面这样

for (int i=0; i < 10; i++) printf("%d", i)

但在 Go 中不行,你可以只写一行,但必须加上花括号

for i := 0; i < 10; i++ {fmt.Println(i)}

三元表达式

Go 中没有三元表达式,所以像下面的代码是无法通过编译的

var c = a > b ? a : b

通过这篇文章你可以对 Go 的语法有一个初步的认知,后续的内容中会进行更细致的展开。