Go 语言入门指南:基础语法和常用特性解析 | 青训营

54 阅读9分钟

go的基础语法以及常用特性

输出

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

随机数

这里通过运行代码会发现生成的随机数总是一样的

func main() {
    fmt.Println("My favorite number is", rand.Intn(10))
}

rand包实现的是伪随机数生成器,需要先对随机数种子进行初始化,为了保证每次运行的随机性,通常在对于种子的sourse初始化用时间戳

rand.Seed(time.Now().UnixNano()) //随机数种子

rand.Intn()代表的是rand的取值范围

下面是一个随机数的猜数字游戏

 func main() {
    maxNum := 100
    rand.Seed(time.Now().UnixNano()) //随机数种子
    se := rand.Intn(maxNum)
    fmt.Println(se)

    fmt.Println("输入数字")
    reader := bufio.NewReader(os.Stdin)
    for {
        input, err := reader.ReadString('\n') // 读取一行输入
        if err != nil {
            fmt.Println(err)
            return
            continue
        }
        //input = strings.TrimSuffix(input, "\n") // 去掉换行符
        input = strings.TrimSpace(input) // 这个是去除字符串前后的空白字符 上面那个只能去除换行符 但是输入的时候会敲回车 导致/r 的出现 会报错

        guess, err := strconv.Atoi(input) //转换字符串成数字

        if err != nil {
            fmt.Println(err)
            return
            continue
        }

        fmt.Println("ur guess is", guess)

        if guess > se {
            fmt.Println("large")
        } else if guess < se {
            fmt.Println("small")
        } else {
            fmt.Println("corret !")
            break
        }
    }

函数

函数可以没有参数或者接受多个参数,同时在go语言里面函数可以同时返回多个值

接受参数

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

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

注意返回类型以及变量类型后置的写法

有多个类型同时赋值的时候,可以这样写

x int, y int -> x, y int

返回参数

函数可以返回任意数量的值,可以这样定义

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

函数命名

  • 函数名称命名

函数名称小写代表函数私有,大写代表函数公有,公有的时候,可以被别的函数跨包调用

  • 函数返回值命名
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

没有参数的return会直接返回,仅用于短的函数,长的函数会影响代码的可读性

变量

变量的初始化

在这个代码中展示了两种变量赋值的方法,就是创建和赋值,以及创建并赋值

注意go里面变量创建就必须要赋值并使用,否则会报错

var x, y int
x = 1
y = 2

z := 3

类型转换

go里面类型转换需要显式转换

var f float64 = math.Sqrt(float64(x*x + y*y))

常量

用const关键字

const Pi = 3.14

循环

for循环

  • 一般for结构 go里面只有for循环结构,下面是一个例子
for i := 0; i < 10; i++ {
        sum += i
    }

go语言中没有条件括号,但是必须有花括号,其他的用法与cpp相同

  • for的缺省结构

可以用这种写法替代while

sum := 1
    for sum < 1000 {
        sum += sum
    }

判断

if

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

if x < 0 {
        return sqrt(-x) + "i"
    }
  • if 的简短语句

通常可以在if的条件判断语句执行之前,执行一段简短的语句,该语句声明的变量作用域仅在 if 之内。

if value, err = judge(key); err!=nil {
  fmt.Error("%v", err)
}
  • if-else语句

语法与c++, java类似

swich

go中的switch语句不限制参数的类型,因此也可以替代if,以及if else语句,

同时switch中不需要加break因为go中执行了相应的case之后会跳出switch

func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.\n", os)
    }
}

注意 switch中代替if的写法,不带switch

func main() {
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    case t.Hour() < 17:
        fmt.Println("Good afternoon.")
    default:
        fmt.Println("Good evening.")
    }
}

defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

这是一个栈式调用,通常用于关闭流

func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
}

指针

Go 拥有指针。指针保存了值的内存地址。

go的指针定义与cpp非常相似

类型 *T 是指向 T 类型值的指针。其零值为 nil。

var p *int

& 操作符会生成一个指向其操作数的指针。

i := 42
p = &i
  • 操作符表示指针指向的底层值。
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21         // 通过指针 p 设置 i

这也就是通常所说的“间接引用”或“重定向”。

与 C 不同,Go 没有指针运算。

结构体

定义

结构体与java类比 有点类似于实体类与对象

下面是一个简单例子

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2} //定义实例
    v.X = 4 //访问字段
    fmt.Println(v.X)
}

结构体指针

在这一段代码中,我之前的疑惑时p时结构体v的指针,

如果要访问v的内存,按照c++的写法时需要*p来访问,但是这么写会报错

查询资料后,发现,go语言中对于结构体指针的使用,允许隐式调用,也就是p.x等价于*p.x

原因是go中没有指针运算,一直写*p.x过于啰嗦

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    p := &v
    p.X = 1e9
    fmt.Println(v)
}

结构体指针的实际应用

在函数中对于大的结构体进行调用的时候,直接用应用类型,会导致执行过程中会对结构体进行复制

非常消耗内存,此时可以用结构体指针进行传参,这样可以节省性能消耗

结构体实例化

type Vertex struct {
    X, Y int
}

var (
    v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
    v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
    v3 = Vertex{}      // X:0 Y:0
    p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

func main() {
    fmt.Println(v1, p, v2, v3)
}

运行结果:

{1 2} &{1 2} {1 0} {0 0}

结构体方法

由于go中没有类,但是可以写结构体方法,未结构体加上方法

方法就是一类带特殊的 接收者 参数的函数。

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
} // 修改副本 不改变原址 不常用


func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
} // 指针接收者 可以直接修改实例的值 更常用

而以指针为接收者的方法被调用时,接收者既能为值又能为指针:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK
  • 选择值或指针作为接收者

使用指针接收者的原因有二:

首先,方法能够修改其接收者指向的值。

其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。

在本例中,Scale 和 Abs 接收者的类型为 *Vertex,即便 Abs 并不需要修改其接收者。

通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。

所以对于结构体方法的调用 我们通常先取地址 然后才调用函数

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

数组

类型后置,其余一样

切片

定义

切片与数组相比,长度更加灵活,不受限制

切片并不存储任何数据,它只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。

与它共享底层数组的切片都会观测到这些修改,共享内存

a := [4]int{1, 2, 3 ,4}
a[low, high] 

左闭右开

切片写法

下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:

[]bool{true, true, false}

切片默认行为

在进行切片时,你可以利用它的默认行为来忽略上下界。

切片下界的默认值为 0,上界则是该切片的长度。

对于数组

var a [10]int

来说,以下切片是等价的:

a[0:10]
a[:10]
a[0:]
a[:]

切片与make实现动态数组

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

append() 向切片追加元素

使用append追加元素的时候,如果追加元素之后,超过了元素的长度,会将切片重新分配地址

由于我们常用空切片 ,因此在追加元素之后都会把切片赋值回去

// 添加一个空切片
    s = append(s, 0)
    printSlice(s)

    // 这个切片会按需增长
    s = append(s, 1)
    printSlice(s)

    // 可以一次性添加多个元素
    s = append(s, 2, 3, 4)
    printSlice(s)

map

定义

map[key]value

var m = map[string]int{
  "a":1,
  "b":2
}

创建

type Vertex struct {
    Lat, Long float64
}

var m = map[string]Vertex{
    "Bell Labs": Vertex{
        40.68433, -74.39967,
    },
    "Google": Vertex{
        37.42202, -122.08408,
    },
}

这里的Vertex也可以省略

type Vertex struct {
    Lat, Long float64
}

var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

修改map

在映射 m 中插入或修改元素:

m[key] = elem

获取元素:

elem = m[key]

删除元素:

delete(m, key)

通过双赋值检测某个键是否存在:

elem, ok = m[key]

若 key 在 m 中,ok 为 true ;否则,ok 为 false。

若 key 不在映射中,那么 elem 是该映射元素类型的零值。

同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。

注 :若 elem 或 ok 还未声明,你可以使用短变量声明:

elem, ok := m[key]

range遍历

  • 遍历数组

返回两个参数,第一个参数是索引,第二个参数是value,如果不想要索引,可以用_替代

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d = %d\n", i, v)
    }
}
  • 遍历map
for key, value := range mapName {
  xxxx
}

接口

接口类型 是由一组方法签名定义的集合。

type Abser interface {
    Abs() float64
}

接口的实现

go中分为显式实现与隐式实现,而隐式实现更为常见

首先定义接口

type sleeper interface {
    sleep()
}

然后定义两个结构体

type dog struct {
    name string
}

type cat struct {
    name string
}

定义结构体实现的接口的方法

其实就是把名字省去,然后名字一样的方法

func (d *dog) sleep() {
    fmt.Printf("dog %s is sleeping", d.name)
}

func (c *cat) sleep() {
    fmt.Printf("cat %s is sleeping", c.name)
}

接口同样是一种数据类型 类似于 java 的声明

通过对接口的赋值(实现了接口的结构体)

通过调用接口的方法 实现 Java 里面的上转型

func main() {
    var s sleeper
    dog := dog{"dd"}
    cat := cat{"cc"}
    s = &dog
    s.sleep() // 实现多态 go会根据类型选择调用哪一个结构体的方法
    s = &cat
    s.sleep() // 实现多态 go会根据类型选择调用哪一个结构体的方法
}
  • 还可以调用接口切片,然后循环赋值,调用方法
func main() {
    food := "apple"
    for _, i := range []lazy{&dog{"dd"}, &cat{"cc"}} {
        i.eat(food)
        i.sleep()
    }
}

接口嵌套

首先定义接口

type sleeper interface {
    sleep()
}

type eater interface {
    eat(eat string)
}

type lazy interface {
    sleeper
    eater
}

然后实现方法

注意 结构体需要实现所有的方法才能够实现该接口 才能进行赋值

type dog struct {
    name string
}

type cat struct {
    name string
}

func (d *dog) sleep() {
    fmt.Printf("dog %s is sleeping", d.name)
}
func (d *dog) eat(food string) {
    fmt.Println(food + d.name)
}

func (c *cat) sleep() {
    fmt.Printf("cat %s is sleeping", c.name)
}
func (c *cat) eat(food string) {
    fmt.Println(food + c.name)
}

最后用切片 遍历赋值接口 实现多态

func main() {
    food := "apple"
    for _, i := range []lazy{&dog{"dd"}, &cat{"cc"}} {
        i.eat(food)
        i.sleep()
    }
}

接口断言-检查接口被赋值的是哪个结构体

获取接口值得真正类型

dog, ok = s.(dog)
if ok {
  fmt.println("is dog")
}

泛型

泛型是通过空接口实现的,所有的结构体都实现了空接口,因此可以通过对空接口进行赋值

空接口可以被任何的结构体赋值 所以这里就实现了泛型

var T interface{} // 空接口

fmt包就使用了泛型