(Go基础)变量与常量?字面量与变量的较量!

1. 字面量

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

1.1 整型字面量

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

go 复制代码
24 // 24
2_4 // 24
0_2_4 // 24
10_000 // 10k
100_000 // 100k
0O24 // 20
0b00 // 0
0x00 // 0
0x0_0 // 0

1.2 浮点数字面量

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

go 复制代码
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)

1.3 复数字面量

go 复制代码
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

1.4 字符字面量

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

go 复制代码
'a'
'ä'
'你'
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'

1.5 转义字符

Go中可用的转义字符

text 复制代码
\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 双引号转义 (该转义仅在字符串内有效)

1.6 字符串字面量

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

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

1.7 Code

go 复制代码
package main

import "fmt"

/*
字面量
*/
func main() {
	// 整数
	var a = 100_000
	var b = 0b00
	var c = 0x00
	fmt.Println(a, b, c)

	// 浮点
	var d = 0.
	fmt.Println(d)

	// 复数
	var e = 0i
	var f = .25i
	fmt.Println(e, f)
}

2. 风格

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

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

  1. 花括号,关于花括号{}到底该不该换行,几乎每个程序员都能说出属于自己的理由,在Go中所有的花括号都不应该换行。
go 复制代码
// 正确示例
func main() {
	fmt.Println("Hello 世界!")
}

// 错误示例
func main() 
{
	fmt.Println("Hello 世界!")
}
  1. 缩进,Go默认使用tabs也就是制表符进行缩进,仅在一些特殊情况会使用空格。

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

    go 复制代码
    2*9 + 1/3*2

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

  3. 还是花括号,花括号在任何时候都不能够省略,就算是只有一行代码,例如

    go 复制代码
    // 正确示例
    if a > b {
    	a++
    }
    // 错误示例
    if a > b a++

3. 常量

常量的值无法在运行时改变,一旦赋值过后就无法修改,其值只能来源于:

  • 字面量
  • 其他常量标识符
  • 常量表达式
  • 结果是常量的类型转换
  • iota

常量只能是基本数据类型,不能是

  • 除基本类型以外的其它类型,如结构体,接口,切片,数组等
  • 函数的返回值

常量的值无法被修改,否则无法通过编译

3.1 初始化

常量的声明需要使用 const 关键字,常量需要在声明时就初始化一个值,并且常量的类型可以省略

go 复制代码
const a = "张三"
const num byte = 1
const bExpression = (1+2+3)/2%100 + num
fmt.Println(a, num, bExpression) // 张三 1 4

如果仅仅只是声明而不确定值,将会无法通过编译

编译器报错

bash 复制代码
missing init expr for b

在Go中,可以批量声明变量并赋值,定义常量或者变量时都没有问题

go 复制代码
const (
    name = "张三"
    age  = 13
)
const c,d,e = 1,"老吴",false

在同一个常量分组中,在已经赋值的常量后面的常量可以不用赋值,其值默认就是前一个的值,比如

go 复制代码
const (
	A = 1
	B // 1
	C // 1
	D // 1
	E // 1
)

3.2 iota

iota是一个内置的常量标识符,通常用于表示一个常量声明中的无类型整数序数,一般都是在括号中使用。

go 复制代码
const iota = 0 

看几个使用案例

go 复制代码
const (
   Num = iota // 0
   Num1 // 1
   Num2 // 2
   Num3 // 3
   Num4 // 4
)

也可以这么写

go 复制代码
const (
   Num = iota*2 // 0
   Num1 // 2
   Num2 // 4
   Num3 // 6
   Num4 // 8
)

还可以

go 复制代码
const (
   Num = iota << 2*3 + 1 // 1
   Num1 // 13
   Num2 // 25
   Num3 = iota // 3
   Num4 // 4
)

通过上面几个例子可以发现,iota是递增的,第一个常量使用iota值的表达式,根据序号值的变化会自动的赋值给后续的常量,直到用新的iota重置,这个序号其实就是代码的相对行号,是相对于当前分组的起始行号,看下面的例子

go 复制代码
const (
	Num  = iota<<2*3 + 1 // 1 第一行
	Num2 = iota<<2*3 + 1 // 13 第二行
	_ // 25 第三行
	Num3 //37 第四行
	Num4 = iota // 4 第五行
	_ // 5 第六行
	Num5 // 6 第七行
)

例子中使用了匿名标识符_占了一行的位置,可以看到iota的值本质上就是iota所在行相对于当前const分组的第一行的差值。而不同的const分组则相互不会影响

3.3 枚举

Go 语言没有位枚举单独设计一个数据类型,不像其他语言通常会有一个enum来表示。一般在Go中,都是通过自定义类型+const+iota来实现枚举

go 复制代码
type season byte

func main() {
	const (
		Spring season = iota
		Summer
		Autumn
		Winter
	)
}

这些枚举实际上就是数字,Go也不支持直接将其转换为字符串,但我们可以通过给自定义类型添加方法来返回其字符串表现形式,实现Stringer接口即可。

go 复制代码
func (s Season) Test() string {
	switch s {
	case Spring:
		return "spring"
	case Summer:
		return "summer"
	case Autumn:
		return "autumn"
	case Winter:
		return "winter"
	}
	return ""
}

这样一来就是一个简单的枚举实现了。你也可以通过官方工具Stringeropen in new window来自动生成枚举。

不过它有以下缺点:

  • 类型不安全,因为Season是自定义类型,可以通过强制类型转换将其他数字也转换成该类型

    go 复制代码
    Season(6)
  • 繁琐,字符串表现形式需要自己实现

  • 表达能力弱,因为const仅支持基本数据类型,所以这些枚举值也只能用字符串和数字来进行表示

4. 变量

变量是用于保存一个值的存储位置,允许其存储的值在运行时动态的变化。每声明一个变量,都会为其分配一块内存以存储对应类型的值

4.1 声明

在go中的类型声明是后置的,变量的声明会用到var关键字,格式为var 变量名 类型名,变量名的命名规则必须遵守标识符的命名规则

go 复制代码
package main

import "fmt"

var intNum int
var str string
var char byte

/*
变量的定义
*/
func main() {
	var i int = 15
	i = 10
	fmt.Print(i)
}

在go语言中,变量的定义是以 var 变量名 数据类型 = 值 来确定的。

把它看作是强制规定数据类型的js语法就可以了

注意事项:

  1. 变量表示内存中的一个存储区域

  2. 该区域有自己的名称(变量名)和类型(数据类型)

  3. Golang变量使用的三种方式

    1. 指定变量类型,声明后如果不赋值,使用默认值

      int 数据结构 的默认值是 0

    2. 根据值自行判定变量类型(类型推导)

      go 复制代码
      var num = 12
      var str = "demo1"
      fmt.Print(num, "\n", str)

      跟 JS 语法差不多,使用var 可以让go语言自行推导值是什么数据类型

    3. 省略var,注意:=左侧的变量不应该是已经声明过的,否则会导致编译错误

      go 复制代码
      name := "tom"
      fmt.Print("name is ? ", name)

      := 这种省略方式,等价于 var name string name = "tom"

  4. 多变量声明

    在编程中,有时我们需要一次性声明多个变量,Golang也提供这样的语法

    go 复制代码
    var n1, n2, n3 int
    fmt.Print("n1=", n1, "n2=", n2, "n3=", n3)
    
    var name, age, sex = "张三", "18", "男"
    fmt.Print("name=", name, "age=", age, "sex=", sex)
    
    name1, age1, sex1 := "张三", "18", "男"
    fmt.Print("name=", name1, "age=", age1, "sex=", sex1)
  5. 定义全局变量

    同时,当要声明多个不同类型的变量时,可以使用()进行包裹,可以存在多个()

    go 复制代码
    var (
    	address = "湖南"
    	father  = "老刘"
    	son     = "老八"
    )
    var (
    	name    string
    	age     int
    	address string
    )
    
    var (
    	school string
    	class int
    ) 
    fmt.Print(address, father, son)

    当要声明多个相同类型的变量时,可以只写一次类型

    go 复制代码
    var numA, numB, numC int
  6. 该区域的数据值可以在同一类型范围内不断变化

  7. 变量在同一作用域内不能重名

  8. 变量=变量名+值+数据类型

  9. Go的变量如果没有赋予初始值,编译器会使用默认值,比如int默认值0,string默认值为空

  10. 一个变量如果只是声明而不赋值,那么变量存储的值就是对应类型的零值

4.2 赋值

赋值会用到运算符=,例如

go 复制代码
var name string
name = "jack"

也可以声明的时候直接赋值

go 复制代码
var name string = "jack"

或者这样也可以

go 复制代码
var name string
var age int
name, age = "jack", 1

第二种方式每次都要指定类型,可以使用官方提供的语法糖:短变量初始化,可以省略掉var关键字和后置类型,具体是什么类型交给编译器自行推断。

go 复制代码
name := "jack" // 字符串类型的变量。

虽然可以不用指定类型,但是在后续赋值时,类型必须保持一致,下面这种代码无法通过编译。

go 复制代码
a := 1
a = "123"

// 报错
// cannot use "123" (untyped string constant) as int value in assignment

还需要注意的是,短变量初始化不能使用nil ,因为nil不属于任何类型,编译器无法推断其类型。

go 复制代码
name := nil // 无法通过编译

短变量声明可以批量初始化

go 复制代码
name, age := "jack", 1

短变量声明方式无法对一个已存在的变量使用,因为它本身所代表的就是 var a int = 1,多次使用就代表重复声明一个变量

go 复制代码
// 错误示例
var a int
a := 1

// 错误示例
a := 1
a := 2

但是有一种情况除外,那就是在赋值旧变量的同时声明一个新的变量,比如

go 复制代码
a := 1
a, b := 2, 2

这种代码是可以通过编译的,变量a被重新赋值,而b是新声明的。

在go语言中,有一个规则,那就是所有在函数中的变量都必须要被使用,比如下面的代码只是声明了变量,但没有使用它。那么在编译时就会报错,提示你这个变量声明了但没有使用

go 复制代码
func main() {
	a := 1
}
// a declared and not used

这个规则仅适用于函数内的变量,对于函数外的包级变量则没有这个限制,下面这个代码就可以通过编译。

go 复制代码
var a = 1

func main() {
	
}

4.3 匿名

用下划线可以表示不需要某一个变量

go 复制代码
Open(name string) (*File, error)

比如os.Open函数有两个返回值,你不需要某一个变量时,就可以使用下划线_代替

go 复制代码
func main() {
	a := "张三变量"
    // 当test方法有两个返回值,想要不接收哪个值就可以使用 下划线_ 来舍弃它
	c, _ := test(a)
	fmt.Println(c)
}

func test(str string) (string, bool) {
	fmt.Println(str)
	str += " 进行了修改"
	return str, false
}

4.4 变换

在Go中,如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换,语法上看起来非常直观,例子如下

go 复制代码
num1, num2 := 25, 36
num1, num2 = num2, num1

思考下面这一段代码,这是计算斐波那契数列的一小段代码,三个变量在计算后的值分别是什么

go 复制代码
a, b, c := 0, 1, 1
a, b, c = b, c, a+b

// 答案: 1 1 1

为什么不是 1 1 2 呢?

明明a已经被赋予b的值了,为什么a+b的结果还是1?go在进行多个变量赋值运算时,它的顺序是先计算值再赋值,并非从左到右计算。

go 复制代码
a, b, c = b, c, a+b
// a, b, c = 1, 1, 0+1

4.5 比较

变量之间的比较有一个大前提,那就是它们之间的类型必须相同,go语言中不存在隐式类型转换,像下面这样的代码是无法通过编译的

go 复制代码
func main() {
	var a uint64
	var b int64
    // 编译器会告诉你两者之间类型并不相同
    // 错误:invalid operation: a == b (mismatched types uint64 and int64)
	fmt.Println(a == b) 
}

在没有泛型之前,早期go提供的内置minmax函数只支持浮点数,到了1.21版本,go才终于将这两个内置函数用泛型重写,现在可以使用min函数比较最小值、使用max函数比较最大值

go 复制代码
minVal := min(1, 2, -1, 1.2)
maxVal := max(100, 22, -1, 1.12)

它们的参数支持所有的可比较类型,go中的可比较类型有

  • 布尔
  • 数字
  • 字符串
  • 指针
  • 通道 (仅支持判断是否相等)
  • 元素是可比较类型的数组(切片不可比较)
  • 字段类型都是可比较类型的结构体(仅支持判断是否相等)

除此之外,还可以通过导入标准库cmp来判断,不过仅支持有序类型的参数,在go中内置的有序类型只有数字和字符串。

go 复制代码
import "cmp"

func main() {
	cmp.Compare(1, 2)
	cmp.Less(1, 2)
}

4.6 代码块

在函数内部,可以通过花括号建立一个代码块,代码块彼此之间的变量作用域是相互独立的。例如下面的代码

go 复制代码
func main() {
	a := 1
	
	{
		a := 2
		fmt.Println(a)
	}
	
	{
		a := 3
		fmt.Println(a)
	}
	fmt.Println(a)
}

它输出的顺序:2 3 1

块与块之间的变量相互独立 ,不受干扰,无法访问,但是会受到父块中的影响

go 复制代码
func main() {
	a := 1

	{
		a := 2
		fmt.Println(a)
	}

	{
		fmt.Println(a)
	}
	fmt.Println(a)
}

它的输出的顺序:2 1 1

5. 类型强转

5.1 整型

Go 和 java/c 不同,Go在不同类型的变量之间赋值需要显式转换,也就是说Go中数据类型不能自动转换

在Go中的强制转换会有对应类型的标准函数 例如:int类型 有 int()函数,来转换数据类型 var c int = int(u)

go 复制代码
/*
强制类型转换
*/
func main() {
	var i int = 2
	var f float64 = float64(i)
	var u uint = uint(f)
	fmt.Println(i, f, u)
}

在Go中,低精度 转------》搞精度 没有问题,但是 高精度 转------》低精度,虽然编译不会报错,但是转换出来的结果是按溢出处理,和希望的结果不一样

go 复制代码
var num1 int64 = 9999999
var num2 int8 = int8(num1)
fmt.Println(num2) // 127
  • Go中,数据类型的转换可以从 表示范围小》表示范围大,也可以 范围大》范围小
  • 被转换的是变量存储的数据(值),变量本身的数据类型并没有变化

5.1.1 练习一

go 复制代码
func main() {
	var n1 int32 = 12
	var n2 int64
	var n3 int8

	n2 = n1 + 20 // n2 类型不一致
	n3 = n1 + 20
	fmt.Println(n1, n2, n3)
}

当类型不同时,接受变量的数据类型也必须一直,否则如上图报错

5.2.1 练习二

go 复制代码
var n1 int32 = 12
var n2 int8
var n3 int8

n2 = int8(n1) + 127 // 编译通过,但是会发生溢出
n3 = int8(n1) + 128 // 内存超出上限,直接报错
fmt.Println(n3, n2)

5.2 string 字符串

在程序开发中,我们经常需要将基本类型转成string类型或者string转基本数据类型

占位符名 表示
%d 十进制整数
%f 浮点数(默认保留六位小数)
%s 字符串
%t 布尔型
%b 二进制数
%o 八进制数
%x 小写十六进制数
%X 大写十六进制数
%q ASCII 转 字符
%c ASCII 转 字符串(去引号)

5.2.1 方法一:fmt.Sprintf("%参数",表达式)

  • 参数需要和表达式的数据类型相匹配
  • fmt.Sprintf()... 会返回转换后的字符串
go 复制代码
var str string
var num1 int = 10
var num2 float64 = 123.345
var b bool = true
var c byte = 'a'

str = fmt.Sprintf("%d", num1)
fmt.Println("int 转 string:", str)

str = fmt.Sprintf("%f", num2)
fmt.Println("float64 转 string:", str)

str = fmt.Sprintf("%t", b)
fmt.Println("bool 转 string:", str)

str = fmt.Sprintf("%q", c)
fmt.Println("byte 转 string:", str)

str = fmt.Sprintf("%c", c)
fmt.Println("byte 转 string(去引号):", str)

5.2.2 方法二:strconv

go 复制代码
var str string
var num1 int = 10
var num2 float64 = 123.345
var b bool = true
//var c byte = 'a'


str = strconv.FormatInt(int64(num1), 10)
fmt.Println("int 转 string:", str)
/*
	参数一:转换的变量
	参数二: 'f' 代表转换的格式
	参数三:表示保留小数位个数
	参数四:表示这个小数是float64
*/
str = strconv.FormatFloat(num2, 'f', -1, 64)
fmt.Println("float64 转 string:", str)

str = strconv.FormatBool(b)
fmt.Println("bool 转 string:", str)

5.2.3 string转基本数据类型

go 复制代码
var b bool
var strBool string = "true"
// ParseBool(value bool,err error) 方法返回两个值,第二个返回值隐藏掉就可以了
b, _ = strconv.ParseBool(strBool)
fmt.Println("字符串转bool:", b)

var n1 int
var n2 int64
var strInt string = "12138"
// ParseInt(value int64,err error)也是返回两个值,但是它返回的整型是int64类型,如果想要其他类型必须根据接收的变量然后再转一次
n2, _ = strconv.ParseInt(strInt, 10, 64)
n1 = int(n2)
fmt.Println(n1)

var strFloat string = "123.345"
var f1 float64
f1, _ = strconv.ParseFloat(strFloat, 64)
fmt.Println(f1)

5.2.4 注意

当转换不成功时,转换后的结果会是那个类型的默认值

go 复制代码
var str4 string = "hello"
var n3 int64 = 11
n3, _ = strconv.ParseInt(str4, 10, 64)
fmt.Println(n3)// 0

6. 运算符

下面是Go语言中支持的运算符号的优先级排列

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

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

go 复制代码
a += 1
a /= 2
a &^= 2

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

go 复制代码
a++ // 正确
++a // 错误
  • 另外,当使用 a++ 这样的语法时,它们将不再具有返回值

    意思是说 a = b++ 这种语法是错误的。

6.1 取反运算符

go 复制代码
func main() {
	var a int = 2
	a = ^a

	fmt.Println(a) // -3
}

在Go中,整数是用补码表示的,对于一个有符号整数a,其按位取反的结果是 -x -1

6.2 其他的运算

至于其他的运算符本文就不多说了,因为与其他编程语言差异不大

go 复制代码
i := 1
j := 2
c := i + j
c = i*j
c = i/j
c = i%j
....

7. 😍前篇知识回顾

  1. Go的环境安装与开发工具配置
  2. Go的运行流程步骤与包的概念

8. 💕👉 其他好文推荐

全文资料学习全部参考于:Golang中文学习文档

相关推荐
余辉zmh33 分钟前
【c++篇】:深入c++的set和map容器--掌握提升编程效率的利器
开发语言·c++
无忧无虑Coding3 小时前
pyinstall 打包Django程序
后端·python·django
·云扬·3 小时前
Java IO 与 BIO、NIO、AIO 详解
java·开发语言·笔记·学习·nio·1024程序员节
求积分不加C4 小时前
Spring Boot中使用AOP和反射机制设计一个的幂等注解(两种持久化模式),简单易懂教程
java·spring boot·后端
枫叶_v4 小时前
【SpringBoot】26 实体映射工具(MapStruct)
java·spring boot·后端
东方巴黎~Sunsiny4 小时前
java-图算法
java·开发语言·算法
2401_857617625 小时前
汽车资讯新趋势:Spring Boot技术解读
java·spring boot·后端
小林学习编程6 小时前
从零开始理解Spring Security的认证与授权
java·后端·spring
写bug的羊羊6 小时前
Spring Boot整合Nacos启动时 Failed to rename context [nacos] as [xxx]
java·spring boot·后端
ad禥思妙想6 小时前
如何运行python脚本
开发语言·python