初识Go语言


Go 语言结构

初识Go语言

Go 语言结构

package main  //①

import "fmt" //②

func main() { //③
   /* 这是我的第一个简单的程序 */
   fmt.Println("Hello, World!")
}

代码说明:

① 定义包名,必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。

② 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。

③ 程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。

说明:对比Java中的public 与 protected,Go语言中当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。

注释

Go语言支持两种注释:

  • 行注释以//开始,直到出现换行符时结束。

  • 块注释以/* 开始,以*/结束

标识符

Go语言标识符是一个非空的字母或数字串,其中第一个字符必须是字母,该标识符不能是关键字的名字。

标识符是区分大小写的,以大写字母开头的标识符是公开的---以Go语言术语来讲就是可以导出的。其他的任何标识符都是私有的---以Go语言术语来讲就是未导出的。

行分隔符

在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。有两个地方必须使用分号,即当我们在一行中放入一条或多条语句时,或者使用原始的for循环语句时。

如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

Go语言基本数据类型

  • 布尔型: bool,布尔型的值只可以是常量 true 或者 false。 默认值为false

Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。

  • 整型:int8,byte,int16,int,uint,uintptr等

  • 浮点型:float32(精确到小数点后6位)、float64(精确到小数点后15位),默认为 float64

  • 复数类型:complex64(两个float32组成),complex128(两个float64组成,这个为复数默认值)

  • 字符串:string

  • 字符类型:rune 等同于 int32

  • 错误类型:error

  • 派生类型:指针类型(Pointer),数组类型(array),结构化类型(struct),Channel 类型,函数类型,切片类型,接口类型(interface),Map 类型

数据类型的长度根据运行程序所在的操作系统类型所决定的。

比如 int类型 在32位或64位操作系统中,各自的长度就不一样。

类型 符号 长度范围
uint8 无符号 8位整型 (0 到 255)
uint16 无符号 16位整型 (0 到 65535)
uint32 无符号 32位整型 (0 到 4294967295)
uint64 无符号 64位整型 (0 到 18446744073709551615)
int8 有符号 8位整型 (-128 到 127)
int16 有符号 16位整型 (-32768 到 32767)
int32 有符号 32位整型 (-2147483648 到 2147483647)
int64 有符号 64位整型 (-9223372036854775808 到 9223372036854775807)

**浮点类型与其他数字类型的长度 **

类型 长度
float32 math.MaxFloat32大约是3.4e38。最小的非负值大约是1.4e-45。
float64 math.MaxFloat64大约是1.8e308。最小的非负值大约是4.9e-324。
byte 类似 uint8,是它的别名,两个可以直接赋值,无需转换。区别自定义类型
rune 类似 int32,是它的别名,两个可以直接赋值,无需转换。区别自定义类型
int 与 uint 一样大小,默认的整数类型,依据平台,分为 32位和 64 位
uintptr 无符号整型,用于存放一个指针

如果需要在不同的数值类型间进行数值运算或者比较操作,那么就必须进行类型转换,通常是将类型转换为最大的类型以防止精度丢失,类型转换采用 type(value)的方式进行。

若需要进行缩小尺寸的类型转换,我们就需要自定义向下转换函数

比如 int 与 uint8的转换

func IntToUint8(a int)(uint8,error)  {
   if 0<=a && a<= math.MaxUint8{
      return uint8(a),nil
   }
   return 0,fmt.Errorf("%d is out of the uint8 range",a)
}

字符串

在go语言中,字符串是使用UTF-8格式编码的只读的Unicode字节序列。每个字符对应一个rune类型。一旦字符串变量赋值之后,内部的字符就不能修改,英文是一个字节,中文是三个字节。

使用range迭代字符串时,需要注意的是range迭代的是Unicode而不是字节。返回的两个值,第一个是被迭代的字符的UTF-8编码的第一个字节在字符串中的索引,第二个值的为对应的字符且类型为rune(实际就是表示unicode值的整形数据)。

默认值是空字符串,而非NULL

const s = "Go语言"
for i, r := range s {
    fmt.Printf("%#U  : %d\n", r, i)
}

-----output-----
U+0047 'G' : 0
U+006F 'o' : 1
U+8BED '语' : 2
U+8A00 '言' : 5

Go语言变量

在数学概念中,变量表示没有固定值且可改变的数,从计算机角度来讲,变量是一段或者多段用来存储数据的内存。

作为静态语言,go变量有固定的数据类型,变量类型决定了变量内存的长度和存储格式。

我们只能修改变量值,无法改变类型。

在编码阶段,我们用一名字来表示这段内存,但是编译后的机器码从不使用变量名,而是直接通过内存地址来访问目标数据。

  • 指定变量类型:var + 变量名 + 类型,比如
var v1 int = 10 
  • 根据值自行判定变量类型:var + 变量名,比如
var v1 = 10
  • 变量声明再赋值
var v1 int
v1 = 123
  • 省略 var 关键字,注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误,而且这种不能声明全局变量,也就是说只能在函数内使用。比如
 v1:= 10

:= 是用来明确表达同时进行变量声明与初始化的工作,类型自动推导。

:=的使用

它只能被用在函数体内,而不可以用于全局变量的声明与赋值。

声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误。但是全局变量是允许声明且不使用。 同一类型的多个变量可以声明在同一行

在下面代码中使用 := 就会产生影子变量,一般情况下不要出现下面这种情况的代码

func main() {
   var a,b,c = 1,2,3//①
   for a:=4;a<5 ;a++  {  //内部a覆盖了外部a的值
      b:=5 //内部b覆盖了外部b的值
      c =6  //依旧是外部的c
      fmt.Printf("inner:a=%v, b=%v, c=%v\n",a,b,c)
   }
   fmt.Printf("outer:a=%v, b=%v, c=%v\n",a,b,c)
}

----output----
inner:a=4, b=5, c=6
outer:a=1, b=2, c=6

上面代码中其实for是创建了一个新的作用域,属于内部作用域,跟①不是在同一个作用域

:= 模式并不总是重新定义变量,也有可能是赋值操作

func main() {
    x := 100
    println(&x)
    x,y := 200,"abc"  //在这里,x为赋值操作,y才是变量定义
    println(&x,x)
    println(&y,y)
}

---output---
0xc00008cf38
0xc00008cf38 200
0xc00008cf40 abc

:= 变为 赋值操作的前提条件是:至少有一个新变量被定义,且必须是同一个作用域,否则会产生影子变量。

多变量声明

//类型相同多个变量, 非全局变量var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3

//和python很像,不需要显示声明类型,自动推断
var vname1, vname2, vname3 = v1, v2, v3 

//出现在:=左侧的变量不应该全是已经被声明过的,否则会导致编译错误
vname1, vname2, vname3 := v1, v2, v3 

// 这种因式分解关键字的写法一般用于声明全局变量
var (
    vname1 v_type1
    vname2 v_type2
)

多重赋值功能

将 i 与 j 的值互换

i,j = j,i;

多重返回值与空白标识符 _

比如GetName() 函数中会返回 三个值:name1,name2,name3,而我们只需要第三个值 name3,那么我们可以采用以下方式取得。前两个值使用 空白标识符 _ 抛弃不用。

//_ 表示丢弃这个值
_,_,name3 = GetName();

空白标识符 _ 在Go语言中是一个只写变量,只能写入,无法读取。它有两种用途:

  • 比如在导包时,使用 _ 空标志符导入一个包时,就是想执行里面的init函数

    import _ "github.com/go-sql-driver/mysql"
    
    
  • 舍弃不需要的值

但是空白标记符在函数参数上使用时,传参并不能忽略该值

func main() {
    test(1,2,"abc")
}

func test(x,y int,s string ,_ bool)  {
    fmt.Println(x,y,s)
}

会报如下错误

.\mian.go:6:6: not enough arguments in call to test
    have (number, number, string)
    want (int, int, string, bool)

变量零值

type user struct {
    name string
}

var (
    a int
    b bool
    str string
    浮点 float32    //中文可以作为变量标识符
    n  *user
    u   user
    var demo1 [10]int
    var demo2 []int
    var demo3 map[string]string
)

当一个变量被var声明之后,系统自动赋予它该类型的零值:

  • int 零值为 0
  • float 零值为 0.0
  • bool 零值为 false
  • string 零值为空字符串""
  • 指针零值为 nil
  • 结构体的零值为各字段的零值集合
  • 数组的零值为 [0 0 0 0 0 0 0 0 0 0]
  • 切片零值为 nil
  • map零值为 nil
  • 函数零值为nil
  • channel零值为nil
  • interface零值为nil

这些变量在 Go 中都是经过初始化的。大部分类型的零值在内存中占据的字节都是零,为什么不是所有呢?因为这依赖于编译器。

nil值

只有pointer, channel, func, interface, map, slice 这些类型的值才可以是nil。结构体变量和数组变量并不可以为nil,将一个变量声明为 nil时,需要指出该变量的类型,编译器无法猜出该变量的具体类型

package main

func main() {  
    var x = nil // 错误
    _ = x
}

在一个 nil 的slice中添加元素是没问题的,但对一个map做同样的事将会生成一个运行时的panic:

package main

func main() {  
    var a [10]int
    a[0] = 1
    fmt.Println(a) // 正常打印
    var m map[string]int // 或者是 var x5 map[string]int = nil 
    m["one"] = 1 
    fmt.Println(m) //  panic: assignment to entry in nil map
}

并不是所有的类型都可以声明为 nil

var x = nil //错误
var x interface{} = nil  //正确
var x string = nil //错误,""是字符串的零值
var x error = nil   //正确
var x map[string]int = nil  //正确
var x user = nil  //错误
var x [10]int = nil //错误

nil值可以比较,比如声明一个切片或者字典

func main() {
    var demo1 []int
    var demo2 map[string]string
    fmt.Println(demo1 == nil) //true
    fmt.Println(demo2 == nil) //true
}

const关键字

常量是一个简单值的标识符,在程序运行时,不会被修改的量。被 const 修饰的常量不能再被重新赋予任何值。可以被 const 修饰的数据类型只能是:布尔型、数字型(整数型、浮点型和复数)和字符串型,字符。在const 定义中,对常量名没有强制要求全部大写,不过我们一般都会全部字母大写,以便阅读。

常见的定义格式

//显式类型定义:
const B string = "abc"
//隐式类型定义,编译器可以根据变量(常量)的值来推断其类型。
const B = "abc"

同时声明多个常量

const(
    NUM1 = 1
    NUM2 = 2
    NUM3 = 3
)

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

没有被使用的常量并不会引发编译错误。

在Go语言中,预定义了 truefalseiota常量。

iota关键字

自增默认类型为 int,初始值为0,可被编译器修改的常量,每次出现 const 就会重置为 0 ,在下一个 const出现之前,每出现一次 iota,值就会加1。

//示例1
func main() {
   const (
      c0 = iota;
      c1 = iota;
      c2 = iota;
   )
   fmt.Println(c0,c1,c2)// 0 1 2
}

上面的const 赋值语句相同,因此可以简写为下面实例,后面常量如果没有赋值,则继承上一个常量值。

//示例2
func main() {
   const (
      c0 = iota
      c1 
      c2 
   )
   fmt.Println(c0,c1,c2)// 0 1 2
}

iota 的起始行 为 const 的第一行,哪怕第一行并没出现 iota,且后续自增按行序递增,而不是按上一取值递增

//示例3
func main() {
    fmt.Println(c0,c1,c2) // 1 1 3
}

const (
    c0 = 1 // 在这行,iota 就已经被初始化为 0,并开始在下面每行 iota 值加1。
    c1 = 1
    c2 = 1 + iota
)

若有新的常量声明后,iota 不再向下赋值

//示例4
func main() {
   fmt.Println(c0,c1,c2) // 0 8 8
}

const (
    c0 = iota
    c1 = 8
    c2
)

自增类型默认为 int,可以显式指定类型,进行类型转换,要注意类型取值范围。

const (
    a         = iota
    b float32 = iota
    c         = iota
)

常量值也可以是某些编译器能在编译期计算出结果的表达式

const (
    a         = len("hello world")
    b float32 = unsafe.Sizeof(uintptr(0))
)

常量与变量的区别

const x  = "java"
var y =  "golang"

func main() {
    println(&x,x)
    println(&y,y)
}

---output---
./main.go:16:10: cannot take the address of x

不同于变量在运行期分配存储内存(非优化状态),常量通常会在预处理阶段直接展开,作为指令数据。

常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获得常量的地址。

常量声明方式不同,对编译器的影响

const x  = 100  //隐式类型定义
const y byte =  x  //正常进行,相当于  const y byte = 100  
 
const a int  = 100 //显式指定常量类型,编译器会做强类型检查
const b byte  = a //会报错:cannot use a (type int) as type byte in const initializer

枚举

由于Go语言并不支持Enum 关键字,但可以使用 const 与 iota 关键字来实现枚举

func main() {
    const (
        Sunday = iota
        Monday
        Tuesday
        Wednesday
        Thursday
        Firday
        Saturday
    )
    fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Firday, Saturday)
}

----output----
0 1 2 3 4 5 6

注:以大写字母开头的常量在包外可见,小写的不可见。

//存储单位的常量枚举
type ByteSize float64
const (
    _ = iota // 通过赋值给空白标识符来忽略值
    KB ByteSize = 1<<(10*iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

浮点数的比较

import "math"

//p为自定义精度
func IsEqual(f1,f2,p float64) bool {
   return math.Abs(f1 - f2)< p
}

复数表示

var v3 complex64
var v4 complex128
var v5 complex64

func main() {
   v3 = 3.2 + 12i
   v4 = 3.2 + 12i
   v5 = complex(3.2, 12i)

对于一个复数 z = complex(x,y); 通过Go内置函数 real(z)获得该复数的实部x,通过imag(z)获得虚部y

类型转换

go强制要求使用显式类型转换

如果转换的目标是指针/单向通道/没有返回值的函数类型,那么必须将其用括号扩起来

func main() {
    x := 100
    p := (*int)(&x) //正确
    p := *int(&x)//错误
}

自定义类型

可以使用 type 基于现有基础类型,结构体,函数类型创建用户自定义类型。

//将MyInt定义为int类型
type myInt int

通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

和 var,const 类似,可以将多个 type 合并成组

func main() {
    type ( //组
        user struct { //自定义结构体
            name string
            age uint8
        }
        event func(string) bool //自定义函数类型
    )
    
    u := user{
        name: "zhangsan",
        age:  20,
    }
    
    var f event = func(s string) bool {
        println(s)
        return len(s) == 0
    }
}

即便自定义类型的底层数据结构相同,也不能表示它们之间有什么关系,它们是属于完全不同的两种类型。

自定义类型不会继承底层数据结构的方法,不能看作是底层数据结构的别名,不能做隐式转换,不能直接用于比较表达式。

func main() {
    type data int
    var d data = 10
    var x int = d       //不能做隐式转换
    var x int = int(d)  //显式转换可以
    println( x == d)    //不能直接用于比较表达式
    println( x == int(d)) //可以比较
}

自定义类型和类型别名区别

//定义一个别名
type myInt = int
//定义一个自定义类型
type myInt1 int

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型,拥有相同的底层结构。但是反过来讲拥有相同的底层结构不一定是别名。

之前见过的runebyte就是类型别名,他们的定义如下:

type byte = uint8
type rune = int32

类型别名只会在代码中存在,在编译完成后。类型别名就不存在了

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt
    
    fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
    fmt.Printf("type of b:%T\n", b) //type of b:int
}

自增

自增,自减不再是运算符,只能是独立语句,不能用做表达式,也不能前置

func main() {
    a := 1
    ++a //不能前置
    --a //不能前置
    
    a++
    a--
    
    if (a++) > 1 { //不能用作表达式
        
    }
    p := &a
    *p++ //相当于(*p)++
}

指针

不能将内存地址与指针混为一谈。

内存地址是内存中每个字节单元的唯一编号,而指针是一个实体。

指针自己也需要分配内存空间,指针是一个专门用来保存地址的整形变量。

p:= &x x := 100
memory 0x1200 100
address 0x800 0x1200
  • 取址运算符 ‘&’ 用于获取对像地址
  • 指针运算符 ‘*’ 用于间接引用目标对象
  • 二级指针用于获取 指向指针的指针的存放地址

并不是所有的对象都能取地址,比如获取 map的元素地址

判断两个指针是否相等:要么指向同一快地址,要么都为nil。

指针支持相等运算,不支持加减运算和类型转换。

可以通过 unsafe.POinter 将指针转换为 uintptr后进行加减运算

值传递还是指针传递

不管是指针,引用类型,还是其他类型参数,都是值拷贝,区别无非就是拷贝目标对象或拷贝指针而已。

func main() {
    a := 100
    p := &a
    fmt.Printf("pointer: %p, target: %v\n",&p,p)
    test(p)
}

func test(x *int)  {
    fmt.Printf("pointer: %p, target: %v\n",&x,x)
}

---output---
pointer: 0xc000006028, target: 0xc00000a0c8
pointer: 0xc000006038, target: 0xc00000a0c8

表面上看传递指针比较好,但是复制的指针会延长目标对象生命周期,还有可能会导致它被分配到堆上

要使用函数改变int,string 的值并且函数外的值也要受影响,可以使用二级指针或者返回值

func main() {
    a := 100
    p := &a
    test(&p)
    fmt.Println(*p)
}

func test(x **int)  {
    a := 200
    *x = &a
}

---output---
200
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 第2章 基本语法 2.1 概述 基本句法和变量 语句 JavaScript程序的执行单位为行(line),也就是一...
    悟名先生阅读 4,114评论 0 13
  • title: 初识go语言tag: golang 编程语言categories: notes 简介 Go语言是一门...
    cmustard阅读 259评论 0 0
  • 就是楼上的刘海呀 有人说从头开始 人很复杂 我自己也复杂过 但是 无论在何时 都要做一个好人 要做一个人品好的人 ...
    仈月阅读 216评论 0 1
  • 本以为主动和对方的引导逐渐了解情况下,明明感觉缘份应不止于此,可一句"小妹子啊",像魔咒一样时刻提醒我,既接受又无...
    艾米丽阿杰阅读 124评论 0 0
  • 灰蒙蒙的夜色经过一整晚的洗礼终于开始隐退了,天渐渐亮了起来,太阳从山谷里慢慢往上升着,远处的狗伴着开门的吱呀声,也...
    王权王阅读 372评论 3 9