Go语言核心之美 1.3-赋值及类型声明篇

erlib · · 1619 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

赋值(Assignment)

变量的值可以通过赋值操作符 = 来更新, v = 10。

x = 1                       // 具名变量x
*p = true                   // 指针变量
person.name = "bob"         // 结构体struct的字段
count[x] = count[x] * scale // 数组、切片或者map的某个元素

算数操作符和位操作符都有对应的一元操作符形式, v = v + x 等价于 v += x,例如:
count[x] *= scale
这样的缩略形式能省去不少重复的工作,同时数字变量还能通过++递增或者--递减:
v := 1
v++    // same as v = v + 1; v becomes 2
v--    // same as v = v - 1; v becomes 1 again

1.元组(tuple) 赋值
元组赋值允许一次性给多个变量赋值。= 右边的表达式会在左边的变量被更新之前先求值,当=左右两边都有同样的变量时,这种求值方式会非常有用,例如交换两个变量的值:
x, y = y, x

a[i], a[j] = a[j], a[i]
再比如计算两个数的最大公约数(GCD):
func gcd(x, y int) int {
    for y != 0 {
        x, y = y, x%y
    }
    return x
}
或者计算斐波那契数列第N个值:
func fib(n int) int {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        x, y = y, x+y
    }
    return x
}

元组赋值也可以把一些零散的赋值组合起来:
i, j, k = 2, 3, 5

   但是如果表达式较为复杂时,应该尽量避免元组赋值,分开赋值的可读性会更好。

    一些特定的表达式,例如函数调用有多个返回值,这种情况下 = 左边必须有对应数量的变量来进行赋值:

f, err = os.Open("foo.txt")  // 函数调用有两个返回值

很多时候,函数会利用这种多返回值特性来额外返回一个值,这个值会说明函数的调用是否成功(可能是一个error类型变量err,或者bool类型变量ok)。在map中查找某个值,类型断言,从channel中接收值等等都会有两个返回值,其中第二个值就是一个bool类型:   

v, ok = m[key]             // map lookup
v, ok = x.(T)              // type assertion
v, ok = <-ch               // channel receive

Go语言还支持匿名变量 (用过函数式语言的读者应该了解这种机制),我们可以把不想要的值赋给空白标示符(=左边的变量数目和右边的值数目必须相同):

_, err = io.Copy(dst, src) // 只关心Copy的成功与否,不关心具体Copy的字节数,因此丢弃第一个值
_, ok = x.(T)              // 只关心类型断言的成功与否,不关心x的具体值,因此丢弃第一个值

   2.可赋值性

上面的赋值语句是一种显式赋值,但是某些情况下,会发生隐式赋值:在函数调用中,隐式赋值给函数参数;函数返回时,隐式赋值给return的操作数;还有类似下面的组合类型:

medals := []string{"gold", "silver", "bronze"}
这里就是隐式赋值,等价的显式形式是这样的:
medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"

同样的还有map、channel类型等,都支持这种隐式赋值。

无论是用显式赋值或隐式赋值,只要 = 左右两边有相同的类型即可。

针对不同类型的具体赋值规则会在后续章节详细讲解。对于我们目前已经讨论过的那些类型,规则是很简单的:类型必须准确匹配(因此Go是强类型的静态语言),nil可以被赋值给interface或者其它引用类型。 常量(constant)赋值在类型转换时非常有灵活性,可以避免大多数显式类型转换:

	const x = 112
	var v float64 = x  
	fmt.Println(v) //output:112
两个变量能否用 == 或 != 比较取决于可赋值性,a == b 只有在a = b可行时才能判断。

类型声明

变量的类型定义了变量的一些个性化属性,例如变量占据的内存大小、在内存中的排列,变量内部的组织形式,变量支持的操作,变量的行为method等。

在实际项目中,很多自定义类型都有同样的底层类型,例如:int可以是循环的索引,时间戳,文件描述符或者一个月份;float64类型可以是车辆行驶速度,温度。使用type就可以声明具名类型,这样就可以在底层类型之上构建自己的需要的时间戳,文件描述符等类型。

type name underlying-type
具名类型声明一般都发生在package级别,因此该类型是包内可见的,如果类型是导出的(首字母大写),那么就是全局可见的。为了解释类型声明,这里把不同的温度计量单位设置为不同的类型:
package tempconv

import "fmt"

type Celsius float64
type Fahrenheit float64

const (
    AbsoluteZeroC Celsius = -273.15
    FreezingC     Celsius = 0
    BoilingC      Celsius = 100
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
上面定义了两个类型Celsius(摄氏度)和Fahrenheit(华氏度),作为温度的两种不同单位。即使两个类型的底层类型是相同的float64,但是它们是完全不同的类型,所以它们彼此之间不能比较,也不能在算术表达式中结合使用,这种规则可以避免错误的使用两种不同的温度单位,因为两个温度单位的类型是不同的,CToF和FToC返回的两个值也是完全不同的。



对于每个类型T,都有一个对应的类型转换T(x),可以把x转换为T类型。当且仅当两个类型具有相同的底层类型时,才能进行类型转换,例如上文中的Celsius和Fahrenheit,或者两个类型都是指针类型,指向的是同一个底层类型。

数值类型之间、string和[]byte之间都可以进行转换,这些转换可能会改变值的表现形式。例如,将浮点数转化为整数会截取掉小树部分;将string转化为[]byte切片会分配内存空间创建string的一份拷贝(内存拷贝往往是性能瓶颈之一)。总之类型转换是编译期完成的,在运行期是不会失败的!

具名类型的底层类型决定了它的结构和表现形式,也决定了它支持的基本操作(可以理解为继承自底层类型),就好像直接使用底层类型一样。因此对于Celsius和Fahrenheit来说,float64支持的算术操作,它们都支持:

fmt.Printf("%g\n", BoilingC - FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF - CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF - FreezingC)       // compile error: type mismatch
再比如:
type temp int
func main() {	
	var x1 temp = 1
	var x2 temp = 2
	fmt.Println(x1 + x2)//output:3

}

如果两个值有同样的具名类型,那么就可以用比较操作符==和<进行比较;或者两个值,一个是具名类型,一个是具名类型的底层类型,也可以进行比较。但是两个不同的具名类型是不可以直接比较的:

var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!

我们都知道浮点数是不精确的表达形式,因此两个浮点数之间的比较是需要格外小心的。这里注意最后一个类型转换后浮点数之间的比较,Celsius(f)没有改变f的值,是因为c和f两个都是初始化为0,所以可以放心比较。


如果在项目中有些地方需要重复的去写一个复杂类型时,那么使用具名变量可以带来极大的便利。


我们还可以为具名类型定义特有的行为,这些行为就是Go语言中的类型方法(method),在后续章节我们还会详细讲解。

下面的代码定义了Celsuis类型的一个methond:String,

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

很多自定义类型都会定义一个String method,因为在调用fmt包的函数去打印该类型时,String会控制该类型的打印方式(这里其实是实现了Stringer这个接口,和其它OO语言不同,Go语言的接口实现是隐式的):
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c)   // "100°C"
fmt.Println(c)          // "100°C"
fmt.Printf("%g\n", c)   // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String



欢迎大家加入Go语言核心技术QQ群894864,里面热心大神很多哦!


有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:erlib

查看原文:Go语言核心之美 1.3-赋值及类型声明篇

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1619 次点击  
加入收藏 微博
上一篇:Go第四天
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传