摘要

在配置好环境之后,要研究的就是这个语言的语法了。在这篇文章中,作者希望可以简单的介绍一下Golang的各种语法,并与C和Java作一些简单的对比以加深记忆。因为这篇文章只是入门Golang的第二篇文章,所以本文并不会对一些指令进行深挖,仅仅只是停留在“怎么用”的程度,至于“为什么是这样”,则涉及到了具体的应用场景和汇编指令,作者将会在以后的文章中进行介绍。

1 导包

总所周知,“Hello World”是程序员的一种仪式感。

而这一行“Hello World”,一定会涉及到输入输出相关的方法。所以,如何导入包,是我们需要研究的第一步。

includeimportimportGOROOTGOPATH全局GOPATH
注意,在Golang中和Java有一点很大的区别,就是在Golang中,import导入的是目录,而不是包名。而且,Golang没有强制要求包名和目录名需要一致。


下面举一些例子来说明在Golang中包名和目录的关系,先来看看目录结构:


src
package pktest

func Func1()  {
    println("这是第一个函数")
}


test2.go如下:

package pktest

func Func2()  {
    println("这是第二个函数")
}


然后我们再来看看testmain.go下面的内容:

package main

import "package1/package2"

func main() {
    pktest.Func1()
}
Func1pktestpackage1/package2package2
package2.Func1test1.Func1
package2pktest
packageName.FunctionName

结论如下:

import
以上部分内容摘自于这篇文章

2 声明

看完了导包方面的内容,我们再来看看如何声明一个变量。在声明变量这一部分,和C以及Java也有较大的区别

2.1 变量的定义

我们先定义一些变量看看:

var a int
var b float32
var c, d float64
e, f := 9, 10
var g = "Ricardo"
var
:=var
g
注意,Golang是强类型的一种语言,所有的变量必须拥有类型,并且变量仅仅可以存储特定类型的数据。

2.2 匿名变量

标识符为(下划线)的变量,是系统保留的匿名变量,在赋值后,会被立即释放,称之为匿名变量。其作用是变量占位符,对其变量赋值结构。通常会在批量赋值时使用。 例如,函数返回多个值,我们仅仅需要其中部分,则不需要的使用来占位
func main() {
  // 调用函数,仅仅需要第二个返回值,第一,三使用匿名变量占位
  _, v, _ := getData()
  fmt.Println(v)
}
// 返回两个值的函数
func getData() (int, int, int) {
  // 返回3个值
  return 2, 4, 8
}

如上述代码所示,如果我仅仅需要一个变量的值,就不需要去额外定义一些没有意义的变量名了,仅仅只是需要使用占位符这种“用后即焚”的匿名变量。

2.3 常量

const:=

3 判断

我们在使用Java或者C的时候,写判断语句是这样的:

if(condition){
    ...
}

在Golang中,唯一的不同是不需要小括号,但是大括号还是必须的。如下:

func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
        return v
    }
    return lim
}

4 循环

在Golang中,只有一种循环,for循环。

和判断语句一样,在Golang中也是没有小括号的。

func main() {
    sum := 0
    for i := 0; i < 10; i++ {
        sum += i
    }
    fmt.Println(sum)
}
for循环while循环
func main() {
    sum := 1
    for sum < 1000 {
        sum += sum
    }
    fmt.Println(sum)
}
while(true)
func main() {
    for {
        ...
    }
}

5 函数

5.1 函数的定义

func
注意,在Golang的函数中,如果首字母是小写,则只能在包内使用;如果首字母是大写,则可以在包外被引入使用。可以理解为,使用小写的函数,是`private`的,使用大写的函数,是`public`的。


在Golang的函数定义中,一样可以不接受参数,或者接受多个参数。而在参数的定义过程中,也是按照定义变量的格式,先定义变量名,再声明变量类型。对于函数的返回类型,也是按照这样的格式,先写函数名,再写返回类型:

func add(x int, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(42, 13))
}

并且,对于相同类型的两个参数,参数类型可以只写一个,用法如下:

func add(x, y int) int {
    return x + y
}

在Golang中,对于函数的返回值,和C以及Java是不一样的。

Golang中的函数可以返回任意多个返回值。

例如下面的小李子,

func swap(x, y string) (string, string) {
    return y, x
}

func main() {
    a, b := swap("hello", "world")
    fmt.Println(a, b)
}

其次,函数的返回值是可以被命名的:

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}
return

5.2defer

defer
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
}
defer
注意,defer后面必须是函数调用语句,不能是其他语句,否则编译器会报错。


可以考虑到的场景是,文件的关闭,或数据库连接的释放等,这样打开和关闭的代码写在一起,既可以使得代码更加的整洁,也可以防止出现开发者在写了长长的业务代码后,忘记关闭的情况。

至于defer的底层实现,本文不进行详细的解释,简单来讲就是将defer语句后面的函数调用的地址压进一个栈中,在当前的函数执行完毕,CPU即将执行函数外的下一行代码之前,先把栈中的指令地址弹出给CPU执行,直到栈为空,才结束这个函数,继续执行后面的代码。

从上文刚刚的表述中也可以推断出,如果有多条refer语句,将会从下往上依次执行。

因为本文只是对各种指令简单的进行对比,所以对于refer的详细解释,将在以后的文章中详细说明。

6 指针

对于指针,如果是C或者C++开发者,一定很熟悉;而对于Java开发者,指针是对开发者透明的一个东西,一个对象会在堆中占据一定的内存空间,而在当前的栈桢中,有一个局部变量,他的值就是那个对象的首地址,这也是一个指针。

可以说,指针就是开发者访问内存的一种途径,只不过是由控制权交给了开发者还是虚拟机。

&*
但是,与 C 不同,Golang没有指针运算。

7 数组

在Golang中,数组的定义是这样的:

var a [10]int

这样做会将变量 a 声明为拥有 10 个整数的数组。

注意,在Golang中,数组的大小也同样和 C 语言一样不能改变。

7.1切片

数组的切片,顾名思义,就是将一个数组按需切出自己所需的部分。

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:

a[low : high]

它会选择一个半开区间,包括第一个元素,但排除最后一个元素。

以下表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素:

a[1:4]

举个例子:

func main() {
    str := [4]string{
        "aaa",
        "bbb",
        "ccc",
        "ddd",
    }
    fmt.Println(str)

    a := str[0:2]
    b := str[1:3]
    fmt.Println(a, b)

    b[0] = "XXX"
    fmt.Println(a, b)
    fmt.Println(str)
}
abab
bstr

这是因为,Golang中的切片,不是拷贝,而是定义了新的指针,指向了原来数组所在的内存空间。所以,修改了切片数组的值,也就相应的修改了原数组的值了。

此外,切片可以用append增加元素。但是,如果此时底层数组容量不够,此时切片将会指向一个重新分配空间后进行拷贝的数组。

因此可以得出结论:

  • 切片并不存储任何数据,它只是描述了底层数组中的一段。
  • 更改切片的元素会修改其底层数组中对应的元素。
  • 与它共享底层数组的切片都会观测到这些修改。

7.2 make

切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。
在此之前需要解释两个定义,len(长度)和cap(容量)。
len是数组的长度,指的是这个数组在定义的时候,所约定的长度。  
cap是数组的容量,指的是底层数组的长度,也可以说是原数组在内存中的长度。
在前文中所提到的切片,如果我定义了一个str[0,0]的切片,此时的长度为0,但是容量依旧还是5。

make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:

a := make([]int, 5)  // len(a)=5

要指定它的容量,需向 make 传入第三个参数:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

也就是说,make函数可以自定义切片的大小。用Java的话来说,他可以被重载。

有两种形式,如果只有两个参数,第一个参数是数组内元素的类型,第二个参数是数组的长度(此时长度和容量都为5)。

而如果有第三个参数,那么第三个参数可以指定数组的容量,即可以指定这个数组在内存中分配多大的空间。

写在最后

首先,谢谢你能看到这里。

如果这篇文章对你能起到哪怕一点点的帮助,作者都会很开心!

其次要说明的是,作者也是刚开始接触Golang,写这篇文章的目的是起到一个笔记的效果,能够去比较一些C,Java,Golang中的语法区别,也一定会有不少的认知错误。如果在这篇文章中你看到了任何与你的认识有差距的地方,请一定指出作者的错误。如果本文有哪些地方是作者讲的不够明白的,或者是你不理解的,也同样欢迎留言,一起交流学习进步。

而且在本文中,很多地方没有进行深入挖掘,这些作者都有记录,并且打算在之后的文章中,也会从源码的角度出发,分析这些原因。在这篇文章中,就只是单纯的学会怎么用,就达到目的了。

那么在最后,再次感谢~

PS:如果有其他的问题,也可以在公众号找到作者。并且,所有文章第一时间会在公众号更新,欢迎来找作者玩~