0%

HelloWorld

编写合约

hello.sol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.5.2;

contract InfoContract {

string fName;
uint age;

function setInfo(string memory _fName, uint _age) public {
fName = _fName;
age = _age;
}

function getInfo() public view returns (string memory, uint) {
return (fName, age);
}
}

编译合约

1
solcjs hello.sol --abi --bin -o ./

生成hello_sol_InfoContract.abi和hello_sol_InfoContract.bin文件。

生成合约的Java文件

1
web3j solidity generate --solidityTypes -b hello_sol_hello.bin -a hello_sol_hello.abi -o ./ -p com.test

将生成的Java问价导入到项目中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class Hello extends Contract {
private static final String BINARY = "608060405234801561001057600080fd5b50610346806100206000396000f3fe608060405234801561001057600080fd5b5060043610610053576000357c0100000000000000000000000000000000000000000000000000000000900480635a9b0b89146100585780638262963b146100e2575b600080fd5b6100606101a7565b6040518080602001838152602001828103825284818151815260200191508051906020019080838360005b838110156100a657808201518184015260208101905061008b565b50505050905090810190601f1680156100d35780820380516001836020036101000a031916815260200191505b50935050505060405180910390f35b6101a5600480360360408110156100f857600080fd5b810190808035906020019064010000000081111561011557600080fd5b82018360208201111561012757600080fd5b8035906020019184600183028401116401000000008311171561014957600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050919291929080359060200190929190505050610253565b005b6060600080600154818054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156102445780601f1061021957610100808354040283529160200191610244565b820191906000526020600020905b81548152906001019060200180831161022757829003601f168201915b50505050509150915091509091565b8160009080519060200190610269929190610275565b50806001819055505050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106102b657805160ff19168380011785556102e4565b828001600101855582156102e4579182015b828111156102e35782518255916020019190600101906102c8565b5b5090506102f191906102f5565b5090565b61031791905b808211156103135760008160009055506001016102fb565b5090565b9056fea165627a7a7230582007c004e4d5d896b794dd7a63a8b6bdd16e95744ed1d0aa3c5b0eb8c4c7590e250029";

public static final String FUNC_GETINFO = "getInfo";

public static final String FUNC_SETINFO = "setInfo";

@Deprecated
protected Hello(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit);
}

protected Hello(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) {
super(BINARY, contractAddress, web3j, credentials, contractGasProvider);
}

@Deprecated
protected Hello(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit);
}

protected Hello(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider);
}

public RemoteCall<Tuple2<Utf8String, Uint256>> getInfo() {
final Function function = new Function(FUNC_GETINFO,
Arrays.<Type>asList(),
Arrays.<TypeReference<?>>asList(new TypeReference<Utf8String>() {}, new TypeReference<Uint256>() {}));
return new RemoteCall<Tuple2<Utf8String, Uint256>>(
new Callable<Tuple2<Utf8String, Uint256>>() {
@Override
public Tuple2<Utf8String, Uint256> call() throws Exception {
List<Type> results = executeCallMultipleValueReturn(function);
return new Tuple2<Utf8String, Uint256>(
(Utf8String) results.get(0),
(Uint256) results.get(1));
}
});
}

public RemoteCall<TransactionReceipt> setInfo(Utf8String _fName, Uint256 _age) {
final Function function = new Function(
FUNC_SETINFO,
Arrays.<Type>asList(_fName, _age),
Collections.<TypeReference<?>>emptyList());
return executeRemoteCallTransaction(function);
}

@Deprecated
public static Hello load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
return new Hello(contractAddress, web3j, credentials, gasPrice, gasLimit);
}

@Deprecated
public static Hello load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
return new Hello(contractAddress, web3j, transactionManager, gasPrice, gasLimit);
}

public static Hello load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) {
return new Hello(contractAddress, web3j, credentials, contractGasProvider);
}

public static Hello load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
return new Hello(contractAddress, web3j, transactionManager, contractGasProvider);
}

public static RemoteCall<Hello> deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) {
return deployRemoteCall(Hello.class, web3j, credentials, contractGasProvider, BINARY, "");
}

@Deprecated
public static RemoteCall<Hello> deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
return deployRemoteCall(Hello.class, web3j, credentials, gasPrice, gasLimit, BINARY, "");
}

public static RemoteCall<Hello> deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
return deployRemoteCall(Hello.class, web3j, transactionManager, contractGasProvider, BINARY, "");
}

@Deprecated
public static RemoteCall<Hello> deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
return deployRemoteCall(Hello.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, "");
}
}

获取钱包

1. 创建新钱包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 创建新的钱包(钱包余额为0)
*
* @param password: 钱包的密码(不是私钥)
* @param path: 钱包文件的路径
* @return
* @throws NoSuchAlgorithmException
* @throws NoSuchProviderException
* @throws InvalidAlgorithmParameterException
* @throws CipherException
* @throws IOException
*/
private Credentials genCredentials(String password, String path) throws NoSuchAlgorithmException,
NoSuchProviderException, InvalidAlgorithmParameterException, CipherException, IOException {
String fileName = WalletUtils.generateNewWalletFile(password,
new File(path), false);
return WalletUtils.loadCredentials(password, path + "/" + fileName);
}

2. 获取现有钱包

1
2
3
4
5
6
7
8
/**
* 获取已存在钱包
* @param privKey
* @return
*/
private Credentials getCredentials(String privKey) {
return Credentials.create(privKey);
}

在项目中获取合约对象

1. 部署合约

1
2
3
4
5
private Hello ethDeploy() throws Exception {
Web3j web3 = Web3j.build(new HttpService("http://127.0.0.1:7545")); // 区块链客户端地址(ganache, geth等)
Credentials credentials = getCredentials("e35c46af1701a40df4b86385de0af9078c79cab9fb4e2ce1358b376cc24b0ab3");
return Hello.deploy(web3, credentials, GAS_PRICE, GAS_LIMIT).send();
}

2. 加载已部署合约

需要提前将合约部署到区块链中,我使用的是Remix和Ganache的方式模拟。

1
2
3
4
5
6
7
8
9
private Hello ethLoad(String contractAddress) throws Exception {
String password = "123456";
String filePath = "/home/hearing/WorkSpace/eth/test/src/main/resources";

Web3j web3 = Web3j.build(new HttpService("http://127.0.0.1:7545"));
Credentials credentials = getCredentials("e35c46af1701a40df4b86385de0af9078c79cab9fb4e2ce1358b376cc24b0ab3");

return Hello.load(contractAddress, web3, credentials, GAS_PRICE, GAS_LIMIT);
}

调用合约

1
2
3
4
5
TransactionReceipt receipt = contract.setInfo(new Utf8String("hearing"), new Uint256(21)).send();
if (receipt.isStatusOK()) {
Tuple2 tuple2 = contract.getInfo().send();
System.out.println(tuple2.getValue1());
}

CryptoZombie

erc721.sol

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.19;
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}

safemath.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
pragma solidity ^0.4.18;

/**
* @title SafeMath
* @dev Math operations with safety checks that throw on error
*/
library SafeMath {

/**
* @dev Multiplies two numbers, throws on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}

/**
* @dev Integer division of two numbers, truncating the quotient.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}

/**
* @dev Substracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}

/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}

ownable.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
pragma solidity ^0.4.19;
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address public owner;

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}


/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}


/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}

}

zombiefactory.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
pragma solidity ^0.4.19;

import "./ownable.sol";
import "./safemath.sol";

contract ZombieFactory is Ownable {

using SafeMath for uint256;

event NewZombie(uint zombieId, string name, uint dna);

uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
uint cooldownTime = 1 days;

struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
uint16 winCount;
uint16 lossCount;
}

Zombie[] public zombies;

mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;

function _createZombie(string _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}

function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}

function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}

}

zombiefeeding.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}

contract ZombieFeeding is ZombieFactory {

KittyInterface kittyContract;

modifier onlyOwnerOf(uint _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
_;
}

function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}

function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}

function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}

function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(_species) == keccak256("kitty")) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
_triggerCooldown(myZombie);
}

function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}

zombiehelper.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

uint levelUpFee = 0.001 ether;

modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}

function withdraw() external onlyOwner {
owner.transfer(this.balance);
}

function setLevelUpFee(uint _fee) external onlyOwner {
levelUpFee = _fee;
}

function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}

function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) onlyOwnerOf(_zombieId) {
zombies[_zombieId].name = _newName;
}

function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) onlyOwnerOf(_zombieId) {
zombies[_zombieId].dna = _newDna;
}

function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}

}

zombieattach.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pragma solidity ^0.4.19;

import "./zombiehelper.sol";

contract ZombieBattle is ZombieHelper {
uint randNonce = 0;
uint attackVictoryProbability = 70;

function randMod(uint _modulus) internal returns(uint) {
randNonce++;
return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
}

function attack(uint _zombieId, uint _targetId) external onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
if (rand <= attackVictoryProbability) {
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
} else {
myZombie.lossCount++;
enemyZombie.winCount++;
_triggerCooldown(myZombie);
}
}
}

zombieownership.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
pragma solidity ^0.4.19;

import "./zombieattack.sol";
import "./erc721.sol";
import "./safemath.sol";

/// TODO: 把这里变成 natspec 标准的注释把
contract ZombieOwnership is ZombieAttack, ERC721 {

using SafeMath for uint256;

mapping (uint => address) zombieApprovals;

function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}

function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}

function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].sub(1);
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}

function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}

function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId);
}

function takeOwnership(uint256 _tokenId) public {
require(zombieApprovals[_tokenId] == msg.sender);
address owner = ownerOf(_tokenId);
_transfer(owner, msg.sender, _tokenId);
}
}

Geth 常用命令

启动

1
geth cmd console
  • -–identity:指定节点 ID;
  • -–rpc:表示开启 HTTP-RPC 服务;
  • -–rpcport:指定 HTTP-RPC 服务监听端口号(默认为 8545);
  • -–datadir:指定区块链数据的存储位置;
  • -–port:指定和其他节点连接所用的端口号(默认为 30303);
  • -–nodiscover:关闭节点发现机制,防止加入有同样初始配置的陌生节点。

控制台中的对象

控制台是一个交互式的JavaScript执行环境,在这里面可以执行JavaScript代码,其中也内置了一些用来操作以太坊的JavaScript对象,可以直接使用这些对象。这些对象主要包括:

  • eth:包含一些跟操作区块链相关的方法;
  • net:包含一些查看p2p网络状态的方法;
  • admin:包含一些与管理节点相关的方法;
  • miner:包含启动&停止挖矿的一些方法;
  • personal:主要包含一些管理账户的方法;
  • txpool:包含一些查看交易内存池的方法;
  • web3:包含了以上对象,还包含一些单位换算的方法。

控制台操作

进入以太坊Javascript Console后,就可以使用里面的内置对象做一些操作,常用命令有:

  • personal.newAccount():创建账户;
  • personal.unlockAccount():解锁账户;
  • eth.accounts:枚举系统中的账户;
  • eth.getBalance():查看账户余额,返回值的单位是 Wei(Wei 是以太坊中最小货币面额单位,类似比特币中的聪,1 ether = 10^18 Wei);
  • eth.blockNumber:列出区块总数;
  • eth.getTransaction():获取交易;
  • eth.getBlock():获取区块;
  • miner.start():开始挖矿;
  • miner.stop():停止挖矿;
  • web3.fromWei():把 wei 转为如下种类的以太坊单位(还有其他代币token单位);
    • kwei/ada
    • mwei/babbage
    • gwei/shannon
    • szabo
    • finney
    • ether
    • kether/grand/einstein
    • mether
    • gether
    • tether
  • web3.toWei():把以太坊单位(包含代币单位)转为 wei;
  • txpool.status:交易池中的状态;
  • admin.addPeer():连接到其他节点;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> web3.fromWei("425000000000000000000", "ether")
"425"

> web3.toWei("1", "ether")
"1000000000000000000"

# 转账:转账必须解锁账户,默认账户无需解锁
> personal.unlockAccount(user1)
Unlock account 0xf09dca9e10f3f1d0adfe9af7beeeb579c1d1dd37
Passphrase:
true
> eth.accounts
["0xd7ec36444d13cc079eb116b4f2602cffcdc9adee", "0xf09dca9e10f3f1d0adfe9af7beeeb579c1e7456b3ded2fa250545de4de1"]
> eth.sendTransaction({from:"0xd7ec36444d13cc079eb116b4f2602cffcdc9adee",to:"0xf09dcc1d1dd37",value:web3.toWei(6,"ether")})
"0x68dc8f5db24371bbb3acca13ff15f3561cae4a6d8753f62beab1356380211949"
> eth.getBalance(user1)
6000000000000000000
> eth.getBalance(user0)
1.15792089237316195423570985008687907853269984665640564039451584007913129639927e+77

Mist

Mist是以太坊官方的在线钱包管理工具,通过 Mist 可以很方便的连接上私有网络,从而更好的开发、调试、测试。可以在github上下载。

1
mist --rpc url

Truffle

创建项目

  • 使用 truffle unbox 下载任意 truffle boxes。
  • 使用 truffle init 初始化一个不包含智能合约的项目。

目录结构:

  • contracts/:合约目录
  • migrations/:脚本化部署文件
  • test/:测试智能合约和应用
  • truffle.js:配置文件

编译合约

命令

1
truffle compile

Truffle仅默认编译自上次编译后被修改过的文件,来减少不必要的编译。如果你想编译全部文件,可以使用–compile-all选项。

约定

Truffle需要定义的合约名称和文件名准确匹配。举例来说,如果文件名为MyContract.sol,那么合约文件须为如下两者之一:

1
2
3
4
5
6
7
contract MyContract {
...
}
// or
library MyContract {
...
}

这种匹配是区分大小写的,也就是说大小写也要一致,推荐大写每一个开头字母。

移植

命令

1
truffle migrate

这个命令会执行所有的位于migrations目录内的移植脚本。如果你之前的移植是成功执行的。truffle migrate仅会执行新创建的移植。如果没有新的移植脚本,这个命令不同执行任何操作。可以使用选项–reset来从头执行移植脚本。

移植脚本文件

一个样例文件如下:4_example_migration.js

1
2
3
4
module.exports = function(deployer) {
// deployment steps
deployer.deploy(MyContract);
};

移植js里的exports的函数接受一个deployer对象作为第一个参数。这个对象用于发布过程,提供了一个清晰的语法支持,同时提供一些通过的合约部署职责,比如保存发布的文件以备稍后使用。deployer对象是用来缓存(stage)发布任务的主要操作接口。

初始移植

Truffle需要一个移植合约来使用移植特性。这个合约内需要指定的接口,但你可以按你的意味修改合约。对大多数工程来说,这个合约会在第一次移植时进行的第一次部署,后续都不会再更新。通过truffle init创建一个全新工程时,你会获得一个默认的合约。

文件名:contracts/Migration.sol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
contract Migrations {
address public owner;

// A function with the signature `last_completed_migration()`, returning a uint, is required.
uint public last_completed_migration;

modifier restricted() {
if (msg.sender == owner) _
}

function Migrations() {
owner = msg.sender;
}

// A function with the signature `setCompleted(uint)` is required.
function setCompleted(uint completed) restricted {
last_completed_migration = completed;
}

function upgrade(address new_address) restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}

如果你想使用移植特性,你必须在你第一次部署合约时,部署这个合约。可以使用如下方式来创建一次移植。

文件名:migrations/1_initial_migrations.js

1
2
3
4
module.exports = function(deployer) {
// Deploy the Migrations contract as our only task
deployer.deploy(Migrations);
};

由此,你可以接着创建递增的数字前缀来部署其它合约。

部署器(deployer)

你的移植文件会使用部署器来缓存部署任务。所以,你可以按一定顺序排列发布任务,他们会按正确顺序执行。

1
2
3
// Stage deploying A before B
deployer.deploy(A);
deployer.deploy(B);

另一选中可选的部署方式是使用Promise。将部署任务做成一个队列,是否部署依赖于前一个合约的执行情况。

1
2
3
4
// Deploy A, then deploy B, passing in A's newly deployed address
deployer.deploy(A).then(function() {
return deployer.deploy(B, A.address);
});

如果你想更清晰,你也可以选择实现一个Promise链。

网络相关

要实现不同条件的不同部署步骤,移植代码中需要第二个参数network。示例如下:

1
2
3
4
5
6
module.exports = function(deployer, network) {
// Add demo data if we're not deploying to the live network.
if (network != "live") {
deployer.exec("add_demo_data.js");
}
}

移植到truffle开发环境

truffle有内置的私有连用于测试,这是电脑上的本地网络。使用如下命令创建私有连并与其交互:truffle develop。在 truffle 交互控制台中,可以省略 truffle 前缀,例如可以直接使用 compile 。

移植到 Ganache

在truffle配置文件中配置:

1
2
3
4
5
6
7
8
9
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 7545,
network_id: "*"
}
}
};

部署

1
truffle migrate

ICO 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
pragma solidity 0.4.20;
/**
* 一个简单的代币合约。
*/
contract token {

string public standard = 'yuyangray';
string public name; //代币名称
string public symbol; //代币符号比如'$'
//代币单位,展示的小数点后面多少个0,和以太币一样后面是是18个0
uint8 public decimals = 2;

uint256 public totalSupply; //代币总量
/* 这里每个地址对应的是代币的数量,而不是捐赠的以太币的数量 */
mapping (address => uint256) public balanceOf;

event Transfer(address indexed from, address indexed to, uint256 value); //转帐通知事件


/* 初始化合约,并且把初始的所有代币都给这合约的创建者
* @param _owned 合约的管理者
* @param tokenName 代币名称
* @param tokenSymbol 代币符号
*/
function token(address _owned, string tokenName, string tokenSymbol) public {
//合约的管理者获得的代币总量
balanceOf[_owned] = totalSupply;

name = tokenName;
symbol = tokenSymbol;

}

/**
* 转帐,具体可以根据自己的需求来实现
* @param _to address 接受代币的地址
* @param _value uint256 接受代币的数量
*/
function transfer(address _to, uint256 _value) public {
//从发送者减掉发送额
balanceOf[msg.sender] -= _value;

//给接收者加上相同的量
balanceOf[_to] += _value;

//通知任何监听该交易的客户端
Transfer(msg.sender, _to, _value);
}

/**
* 增加代币,并将代币发送给捐赠新用户
* @param _to address 接受代币的地址
* @param _amount uint256 接受代币的数量
*/
function issue(address _to, uint256 _amount) public {
totalSupply = totalSupply + _amount;
balanceOf[_to] += _amount;

//通知任何监听该交易的客户端
Transfer(this, _to, _amount);
}
}

/**
* 众筹合约
*/
contract Crowdsale is token {
address public beneficiary = msg.sender; //受益人地址,测试时为合约创建者
uint public fundingGoal; //众筹目标,单位是ether
uint public amountRaised; //已筹集金额数量, 单位是ether
uint public deadline; //截止时间
uint public price; //代币价格
bool public fundingGoalReached = false; //达成众筹目标
bool public crowdsaleClosed = false; //众筹关闭


mapping(address => uint256) public balance; //保存众筹地址及对应的以太币数量

// 受益人将众筹金额转走的通知
event GoalReached(address _beneficiary, uint _amountRaised);

// 用来记录众筹资金变动的通知,_isContribution表示是否是捐赠,因为有可能是捐赠者退出或发起者转移众筹资金
event FundTransfer(address _backer, uint _amount, bool _isContribution);

/**
* 初始化构造函数
*
* @param fundingGoalInEthers 众筹以太币总量
* @param durationInMinutes 众筹截止,单位是分钟
* @param tokenName 代币名称
* @param tokenSymbol 代币符号
*/
function Crowdsale(
uint fundingGoalInEthers,
uint durationInMinutes,
string tokenName,
string tokenSymbol
) token(this, tokenName, tokenSymbol) public {
fundingGoal = fundingGoalInEthers * 1 ether;
deadline = now + durationInMinutes * 1 minutes;
price = 500 finney; //1个以太币可以买 2 个代币
}


/**
* 默认函数
*
* 默认函数,可以向合约直接打款
*/
function () payable public {

//判断是否关闭众筹
require(!crowdsaleClosed);
uint amount = msg.value;

//捐款人的金额累加
balance[msg.sender] += amount;

//捐款总额累加
amountRaised += amount;

//转帐操作,转多少代币给捐款人
issue(msg.sender, amount / price * 10 ** uint256(decimals));
FundTransfer(msg.sender, amount, true);
}

/**
* 判断是否已经过了众筹截止限期
*/
modifier afterDeadline() { if (now >= deadline) _; }

/**
* 检测众筹目标是否已经达到
*/
function checkGoalReached() afterDeadline public {
if (amountRaised >= fundingGoal){
//达成众筹目标
fundingGoalReached = true;
GoalReached(beneficiary, amountRaised);
}

//关闭众筹
crowdsaleClosed = true;
}


/**
* 收回资金
*
* 检查是否达到了目标或时间限制,如果有,并且达到了资金目标,
* 将全部金额发送给受益人。如果没有达到目标,每个贡献者都可以退出
* 他们贡献的金额
* 注:这里代码应该是限制了众筹时间结束且众筹目标没有达成的情况下才允许退出。如果去掉限制条件afterDeadline,应该是可以允许众筹时间还未到且众筹目标没有达成的情况下退出
*/
function safeWithdrawal() afterDeadline public {

//如果没有达成众筹目标
if (!fundingGoalReached) {
//获取合约调用者已捐款余额
uint amount = balance[msg.sender];

if (amount > 0) {
//返回合约发起者所有余额
msg.sender.transfer(amount);
FundTransfer(msg.sender, amount, false);
balance[msg.sender] = 0;
}
}

//如果达成众筹目标,并且合约调用者是受益人
if (fundingGoalReached && beneficiary == msg.sender) {

//将所有捐款从合约中给受益人
beneficiary.transfer(amountRaised);

FundTransfer(beneficiary, amount, false);
}
}
}

代币示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
pragma solidity 0.4.20;

interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) external; }

/**
* owned 是一个管理者
*/
contract owned {
address public owner;

/**
* 初台化构造函数
*/
function owned () public {
owner = msg.sender;
}

/**
* 判断当前合约调用者是否是管理员
*/
modifier onlyOwner {
require (msg.sender == owner);
_;
}

/**
* 指派一个新的管理员
* @param newOwner address 新的管理员帐户地址
*/
function transferOwnership(address newOwner) onlyOwner public {
if (newOwner != address(0)) {
owner = newOwner;
}
}
}

/**
* @title 基础版的代币合约
*/
contract token {
/* 公共变量 */
string public standard = 'https://mshk.top';
string public name; //代币名称
string public symbol; //代币符号比如'$'
uint8 public decimals = 18; //代币单位,展示的小数点后面多少个0,和以太币一样后面是是18个0
uint256 public totalSupply; //代币总量

/*记录所有余额的映射*/
mapping (address => uint256) public balanceOf;
mapping (address => mapping (address => uint256)) public allowance;

/* 在区块链上创建一个事件,用以通知客户端*/
event Transfer(address indexed from, address indexed to, uint256 value); //转帐通知事件
event Burn(address indexed from, uint256 value); //减去用户余额事件

/* 初始化合约,并且把初始的所有代币都给这合约的创建者
* @param initialSupply 代币的总数
* @param tokenName 代币名称
* @param tokenSymbol 代币符号
*/
function token(uint256 initialSupply, string tokenName, string tokenSymbol) public {

//初始化总量
totalSupply = initialSupply * 10 ** uint256(decimals); //以太币是10^18,后面18个0,所以默认decimals是18

//给指定帐户初始化代币总量,初始化用于奖励合约创建者
//balanceOf[msg.sender] = totalSupply;
balanceOf[this] = totalSupply;

name = tokenName;
symbol = tokenSymbol;

}


/**
* 私有方法从一个帐户发送给另一个帐户代币
* @param _from address 发送代币的地址
* @param _to address 接受代币的地址
* @param _value uint256 接受代币的数量
*/
function _transfer(address _from, address _to, uint256 _value) internal {

//避免转帐的地址是0x0
require(_to != 0x0);

//检查发送者是否拥有足够余额
require(balanceOf[_from] >= _value);

//检查是否溢出
require(balanceOf[_to] + _value > balanceOf[_to]);

//保存数据用于后面的判断
uint previousBalances = balanceOf[_from] + balanceOf[_to];

//从发送者减掉发送额
balanceOf[_from] -= _value;

//给接收者加上相同的量
balanceOf[_to] += _value;

//通知任何监听该交易的客户端
Transfer(_from, _to, _value);

//判断买、卖双方的数据是否和转换前一致
assert(balanceOf[_from] + balanceOf[_to] == previousBalances);

}

/**
* 从主帐户合约调用者发送给别人代币
* @param _to address 接受代币的地址
* @param _value uint256 接受代币的数量
*/
function transfer(address _to, uint256 _value) public {
_transfer(msg.sender, _to, _value);
}

/**
* 从某个指定的帐户中,向另一个帐户发送代币
*
* 调用过程,会检查设置的允许最大交易额
*
* @param _from address 发送者地址
* @param _to address 接受者地址
* @param _value uint256 要转移的代币数量
* @return success 是否交易成功
*/
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
//检查发送者是否拥有足够余额
require(_value <= allowance[_from][msg.sender]); // Check allowance

allowance[_from][msg.sender] -= _value;

_transfer(_from, _to, _value);

return true;
}

/**
* 设置帐户允许支付的最大金额
*
* 一般在智能合约的时候,避免支付过多,造成风险
*
* @param _spender 帐户地址
* @param _value 金额
*/
function approve(address _spender, uint256 _value) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
return true;
}

/**
* 设置帐户允许支付的最大金额
*
* 一般在智能合约的时候,避免支付过多,造成风险,加入时间参数,可以在 tokenRecipient 中做其他操作
*
* @param _spender 帐户地址
* @param _value 金额
* @param _extraData 操作的时间
*/
function approveAndCall(address _spender, uint256 _value, bytes _extraData) public returns (bool success) {
tokenRecipient spender = tokenRecipient(_spender);
if (approve(_spender, _value)) {
spender.receiveApproval(msg.sender, _value, this, _extraData);
return true;
}
}

/**
* 减少代币调用者的余额
*
* 操作以后是不可逆的
*
* @param _value 要删除的数量
*/
function burn(uint256 _value) public returns (bool success) {
//检查帐户余额是否大于要减去的值
require(balanceOf[msg.sender] >= _value); // Check if the sender has enough

//给指定帐户减去余额
balanceOf[msg.sender] -= _value;

//代币问题做相应扣除
totalSupply -= _value;

Burn(msg.sender, _value);
return true;
}

/**
* 删除帐户的余额(含其他帐户)
*
* 删除以后是不可逆的
*
* @param _from 要操作的帐户地址
* @param _value 要减去的数量
*/
function burnFrom(address _from, uint256 _value) public returns (bool success) {

//检查帐户余额是否大于要减去的值
require(balanceOf[_from] >= _value);

//检查 其他帐户 的余额是否够使用
require(_value <= allowance[_from][msg.sender]);

//减掉代币
balanceOf[_from] -= _value;
allowance[_from][msg.sender] -= _value;

//更新总量
totalSupply -= _value;
Burn(_from, _value);
return true;
}



/**
* 匿名方法,预防有人向这合约发送以太币
*/
/*function() {
//return; // Prevents accidental sending of ether
}*/
}

/**
* @title 高级版代币
* 增加冻结用户、挖矿、根据指定汇率购买(售出)代币价格的功能
*/
contract MyAdvancedToken is owned, token {

//卖出的汇率,一个代币,可以卖出多少个以太币,单位是wei
uint256 public sellPrice;

//买入的汇率,1个以太币,可以买几个代币
uint256 public buyPrice;

//是否冻结帐户的列表
mapping (address => bool) public frozenAccount;

//定义一个事件,当有资产被冻结的时候,通知正在监听事件的客户端
event FrozenFunds(address target, bool frozen);


/*初始化合约,并且把初始的所有的令牌都给这合约的创建者
* @param initialSupply 所有币的总数
* @param tokenName 代币名称
* @param tokenSymbol 代币符号
* @param centralMinter 是否指定其他帐户为合约所有者,为0是去中心化
*/
function MyAdvancedToken (
uint256 initialSupply,
string tokenName,
string tokenSymbol,
address centralMinter
) token (initialSupply, tokenName, tokenSymbol) public {

//设置合约的管理者
if(centralMinter != 0 ) owner = centralMinter;

sellPrice = 2; //设置1个单位的代币(单位是wei),能够卖出2个以太币
buyPrice = 4; //设置1个以太币,可以买0.25个代币
}


/**
* 私有方法,从指定帐户转出余额
* @param _from address 发送代币的地址
* @param _to address 接受代币的地址
* @param _value uint256 接受代币的数量
*/
function _transfer(address _from, address _to, uint _value) internal {

//避免转帐的地址是0x0
require (_to != 0x0);

//检查发送者是否拥有足够余额
require (balanceOf[_from] > _value);

//检查是否溢出
require (balanceOf[_to] + _value > balanceOf[_to]);

//检查 冻结帐户
require(!frozenAccount[_from]);
require(!frozenAccount[_to]);



//从发送者减掉发送额
balanceOf[_from] -= _value;

//给接收者加上相同的量
balanceOf[_to] += _value;

//通知任何监听该交易的客户端
Transfer(_from, _to, _value);

}

/**
* 合约拥有者,可以为指定帐户创造一些代币
* @param target address 帐户地址
* @param mintedAmount uint256 增加的金额(单位是wei)
*/
function mintToken(address target, uint256 mintedAmount) onlyOwner public {

//给指定地址增加代币,同时总量也相加
balanceOf[target] += mintedAmount;
totalSupply += mintedAmount;


Transfer(0, this, mintedAmount);
Transfer(this, target, mintedAmount);
}

/**
* 增加冻结帐户名称
*
* 你可能需要监管功能以便你能控制谁可以/谁不可以使用你创建的代币合约
*
* @param target address 帐户地址
* @param freeze bool 是否冻结
*/
function freezeAccount(address target, bool freeze) onlyOwner public {
frozenAccount[target] = freeze;
FrozenFunds(target, freeze);
}

/**
* 设置买卖价格
*
* 如果你想让ether(或其他代币)为你的代币进行背书,以便可以市场价自动化买卖代币,我们可以这么做。如果要使用浮动的价格,也可以在这里设置
*
* @param newSellPrice 新的卖出价格
* @param newBuyPrice 新的买入价格
*/
function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner public {
sellPrice = newSellPrice;
buyPrice = newBuyPrice;
}

/**
* 使用以太币购买代币
*/
function buy() payable public {
uint amount = msg.value / buyPrice;

_transfer(this, msg.sender, amount);
}

/**
* @dev 卖出代币
* @return 要卖出的数量(单位是wei)
*/
function sell(uint256 amount) public {

//检查合约的余额是否充足
require(this.balance >= amount * sellPrice);

_transfer(msg.sender, this, amount);

msg.sender.transfer(amount * sellPrice);
}
}

Truffle+OpenZeppelin ERC20代币实现

1、新建项目

1
truffle unbox react-box

2、安装zeppelin-solidity

1
npm install zeppelin-solidity

3、创建标准的代币合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.4;

import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol";

contract BloggerCoin is StandardToken {
string public name = "BloggerCoin";
string public symbol = "BLC";
uint8 public decimals = 2;
uint256 public INITIAL_SUPPLY = 666666;

function BloggerCoin() public {
totalSupply_ = INITIAL_SUPPLY;
balances[msg.sender] = INITIAL_SUPPLY;
}
}
INITIAL_SUPPLY变量定义了在合约部署时,代币将创建的数量。

4、编译和部署

在migrations/目录下建立一个3_deploy_bloggercoin.js文件,内容如下:

1
2
3
4
5
var BloggerCoin = artifacts.require("./BloggerCoin.sol");

module.exports = function(deployer) {
deployer.deploy(BloggerCoin);
};

编译部署:

1
2
truffle compile
truffle migrate 部署

入门说明

在把智能协议传上以太坊之后,它就变得不可更改, 这种永固性意味着你的代码永远不能被调整或更新。你编译的程序会一直,永久的,不可更改的,存在以太坊上。

非常重要的是,部署在以太坊上的 DApp,并不能保证它真正做到去中心,我们需要阅读并理解它的源代码,才能防止其中没有被部署者恶意植入后门;作为开发人员,如何做到既要给自己留下修复 bug 的余地,又要尽量地放权给使用者,以便让他们放心你,从而愿意把数据放在你的 DApp 中,这确实需要个微妙的平衡。

文件结构

1
pragma solidity ^0.4.0
  1. 版本要高于0.4才可以编译
  2. 表示高于0.5的版本则不可编译,第三位的版本号但可以变,留出来用做bug可以修复(如0.4.1的编译器有bug,可在0.4.2修复,现有合约不用改代码)。

引用源文件

全局引入:

1
import “filename”;

自定义命名空间引入:

1
import * as symbolName from “filename”

分别定义引入:

1
import  {symbol1 as alias, symbol2} from “filename”

非es6兼容的简写语法:

1
2
3
import “filename” as symbolName
// 等同于上述
import * as symbolName from “filename”

类型

值类型:

  • 布尔(Booleans)
  • 整型(Integer)
  • 地址(Address)
  • 定长字节数组(fixed byte arrays)
  • 有理数和整型(Rational and Integer Literals,String literals)
  • 枚举类型(Enums)
  • 函数(Function Types)

引用类型

  • 不定长字节数组(bytes)
  • 字符串(string)
  • 数组(Array)
  • 结构体(Struts)

数据位置

复杂类型,如数组(arrays)和数据结构(struct)在Solidity中有一个额外的属性,数据的存储位置。可选为memory和storage。

memory存储位置同我们普通程序的内存一致。即分配,即使用,越过作用域即不可被访问,等待被回收。而在区块链上,由于底层实现了图灵完备,故而会有非常多的状态需要永久记录下来,那么我们就要使用storage类型了,一旦使用这个类型,数据将永远存在。

基于程序的上下文,大多数时候这样的选择是默认的,我们可以通过指定关键字storage和memory修改它。默认的函数参数,包括返回的参数,他们是memory。默认的局部变量是storage的。默认的状态变量(合约声明的公有变量)是storage。

另外还有第三个存储位置calldata。它存储的是函数参数,是只读的,不会永久存储的一个数据位置。外部函数的参数(不包括返回参数)被强制指定为calldata。效果与memory差不多。

  • storage转换为storage只是修改了它的指针。
  • 将一个memory类型的变量赋值给一个状态变量时,实际是将内存变量拷贝到存储中。
  • memory赋值给局部变量:局部变量虽然是一个storage的,但它仅仅是一个storage类型的指针,所以直接赋值会报错,不能将memory赋值给局部变量.
  • 将storage转为memory,实际是将数据从storage拷贝到memory中。
  • 将一个memory的引用类型赋值给另一个memory的引用,不会创建拷贝(即:memory之间是引用传递)。
  • 对于值类型,总是会进行拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.4.0;

contract DataLocation{
uint valueType;
mapping(uint => uint) public refrenceType;

function changeMemory(){
var tmp = valueType;
tmp = 100;
}

function changeStorage(){
var tmp = refrenceType;
tmp[1] = 100;
}

function getAll() returns (uint, uint){
return (valueType, refrenceType[1]);
}
}

强制的数据位置:

  • 外部函数(External function)的参数(不包括返回参数)强制为:calldata
  • 状态变量(State variables)强制为: storage

默认数据位置:

  • 函数参数(包括返回参数):memory
  • 所有其它的局部变量:storage

布尔

bool可能的取值为常量值true和false

整型

int/uint:

  • 变长的有符号或无符号整型。变量支持的步长以8递增,支持从uint8到uint256,以及int8到int256。uint和int默认代表的是uint256和int256

字面量:

  • 整数字面量,由包含0-9的数字序列组成,默认被解释成十进制。在Solidity中不支持八进制,前导0会被默认忽略,如0100,会被认为是100。
  • 小数由.组成,在他的左边或右边至少要包含一个数字。如1.,.1,1.3均是有效的小数。

字面量本身支持任意精度,也就是可以不会运算溢出,或除法截断。但当它被转换成对应的非字面量类型,如整数或小数。或者将他们与非字面量进行运算,则不能保证精度了。总之就是,字面量怎么都计算都行,但一旦转为对应的变量后,再计算就不保证精度了。

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

contract IntegerLiteral{
function integerTest() returns (uint, uint){
//超出运算字长了
var i = (2**800 + 1) - 2**800;
var j = 1/3*3;
//小数运算
var k = 0.5*8;
return (i, j);
}
}

十六进制字面量,以关键字hex打头,后面紧跟用单或双引号包裹的字符串。如hex”001122ff”。在内部会被表示为二进制流。由于一个字节是8位,所以一个hex是由两个[0-9a-z]字符组成的,不是成双的字符串是会报错的。十六进制的字面量与字符串可以进行同样的类似操作:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;

contract HexLiteralBytes{
function test() returns (bytes4, bytes1, bytes1, bytes1, bytes1){
bytes4 a = hex"001122FF";

return (a, a[0], a[1], a[2], a[3]);
}
}

地址

address:以太坊地址的长度,大小20个字节,160位,所以可以用一个uint160编码。地址是所有合约的基础,所有的合约都会继承地址对象(注意:从0.5.0开始,合约不再继承自地址类型,但仍然可以显式转换为地址),也可以随时将一个地址串,得到对应的代码进行调用。当然地址代表一个普通帐户时,就没有这么多丰富的功能了。

地址类型的成员:

  • 属性:balance
  • 函数:send(),call(),delegatecall(),callcode()。

十六进制的字符串,凡是能通过地址合法性检查(address checksum test)2,就会被认为是地址,如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF。需要注意的是39到41位长的没有通过地址合法性检查的,会提示一个警告,但会被视为普通的有理数字面量。

如果只是想得到当前合约的余额,可以这样写:

1
2
3
4
5
6
7
pragma solidity ^0.4.0;

contract addressTest{
function getBalance() returns (uint){
return this.balance;
}
}

transfer()

transfer()用来发送以太币(以wei为单位)

1
2
3
address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

如果x是合约地址,合约的回退函数(fallback 函数)会随transfer调用一起执行(这个是EVM特性),如果因gas耗光或其他原因失败,转移交易会还原并且合约会抛异常停止。

send()

用来向某个地址发送货币(货币单位是wei)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.0;

contract PayTest {
//得到当前合约的余额
function getBalance() returns (uint) {
return this.balance;//0
}

//向当前合约存款
function deposit() payable returns(address addr, uint amount, bool success){
//msg.sender 全局变量,调用合约的发起方
//msg.value 全局变量,调用合约的发起方转发的货币量,以wei为单位。
//send() 执行的结果
return (msg.sender, msg.value, this.send(msg.value));
}
}

这个合约实现的是充值。this.send(msg.value)意指向合约自身发送msg.value量的以太币。msg.value是合约调用方附带的以太币。

send()方法执行时有一些风险:

  • send与transfer对应,但更底层。如果执行失败,transfer不会因异常停止,而send会返回false。
  • 调用递归深度不能超1024。
  • 如果gas不够,执行会失败。
  • 所以使用这个方法要检查成功与否。或为保险起见,货币操作时要使用一些最佳实践。如果执行失败,将会回撤所有交易,所以务必留意返回结果。

为了同一些不支持ABI协议的进行直接交互(一般的web3.js,soldity都是支持的)。可以使用call()函数,用来向另一个合约发送原始数据。参数支持任何类型任意数量。每个参数会按规则(规则是按ABI)打包成32字节并一一拼接到一起。call()方法支持ABI协议定义的函数选择器。如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名。所以如果你只是想发送消息体,需要避免第一个参数是4个字节。call方法返回一个bool值,以表明执行成功还是失败。正常结束返回true,异常终止返回false。我们无法解析返回结果,因为这样我们得事前知道返回的数据的编码和数据大小(这里的潜在假设是不知道对方使用的协议格式,所以也不会知道返回的结果如何解析,有点祼协议测试的感觉)。

同样也可以使用delegatecall(),它与call方法的区别在于,仅仅是代码会执行,而其它方面,如(存储,余额等)都是用的当前的合约的数据。delegatecall()方法的目的是用来执行另一个合约中的工具库。所以开发者需要保证两个合约中的存储变量能兼容,来保证delegatecall()能顺利执行。

上面的这三个方法call(),delegatecall(),callcode()都是底层的消息传递调用,最好仅在万不得已才进行使用,因为他们破坏了Solidity的类型安全。上述的函数都是底层的函数,使用时要异常小心。当调用一个未知的,可能是恶意的合约时,当你把控制权交给它,它可能回调回你的合约,所以要准备好在调用返回时,应对你的状态变量可能被恶意篡改的情况。

定长字节数组

bytes1, … ,bytes32,允许值以步长1递增。byte默认表示byte1。成员变量length。

枚举类型(enum)

枚举类型是在Solidity中的一种用户自定义类型。他可以显示的与整数进行转换,但不能进行隐式转换。显示的转换会在运行时检查数值范围,如果不匹配,将会引起异常。枚举类型应至少有一名成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.0;

contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;

function setGoStraight() {
choice = ActionChoices.GoStraight;
}

// Since enum types are not part of the ABI, the signature of "getChoice"
// will automatically be changed to "getChoice() returns (uint8)"
// for all matters external to Solidity. The integer type used is just
// large enough to hold all enum values, i.e. if you have more values,
// `uint16` will be used and so on.
function getChoice() returns (ActionChoices) {
return choice;
}

function getDefaultChoice() returns (uint) {
return uint(defaultChoice);
}
}

函数

函数的分类:

  • 内部函数(internal):因为不能在当前合约的上下文环境以外的地方执行,内部函数只能在当前合约内被使用。如在当前的代码块内,包括内部库函数,和继承的函数中。
  • 外部函数(External):外部函数由地址和函数方法签名两部分组成。可作为外部函数调用的参数,或者由外部函数调用返回。

函数的定义:

1
function (<parameter types>) {internal(默认)|external} [constant] [payable] [returns (<return types>)]

若不写类型,默认的函数类型是internal的。如果函数没有返回结果,则必须省略returns关键字。

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

contract Test{
//默认是internal类型的
function noParameter() returns (uint){}

//无返回结果
function noReturn1(uint x) {}

//如果无返回结果,必须省略`returns`关键字
//function noReturn2(uint x) returns {}
}

函数的internal与external:调用一个函数f()时,我们可以直接调用f(),或者使用this.f()。但两者有一个区别。前者是通过internal的方式在调用,而后者是通过external的方式在调用。请注意,这里关于this的使用与大多数语言相背。下面通过一个例子来了解他们的不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
pragma solidity ^0.4.5;

contract FuntionTest{
function internalFunc() internal{}

function externalFunc() external{}

function callFunc(){
//直接使用内部的方式调用
internalFunc();

//不能在内部调用一个外部函数,会报编译错误。
//Error: Undeclared identifier.
//externalFunc();

//不能通过`external`的方式调用一个`internal`
//Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest
//this.internalFunc();

//使用`this`以`external`的方式调用一个外部函数
this.externalFunc();
}
}

contract FunctionTest1{
function externalCall(FuntionTest ft){
//调用另一个合约的外部函数
ft.externalFunc();

//不能调用另一个合约的内部函数
//Error: Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest
//ft.internalFunc();
}
}

不定长字节数组

  • bytes:动态长度的字节数组。

字符串

字符串(string)字面量是指由单引号,或双引号引起来的字符串。字符串并不像C语言,包含结束符,“foo”这个字符串大小仅为三个字节。字符串的长度类型可以是变长的,特殊之处在于,可以隐式的转换为byte1,…byte32。

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;

contract StringConvert{
function test() returns (bytes3){
bytes3 a = "123";

//bytes3 b = "1234";
//Error: Type literal_string "1234" is not implicitly convertible to expected type bytes3.

return a;
}
}
// 上述的字符串字面量,会隐式转换为bytes3。

数组

创建一个数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.4.0;

contract Test {
uint[5] arr = [0,1,2,3,4];//创建一个定长的数组
uint[] storageArr;

function a() public {

uint[5] memory arr1 = [uint(0),1,2,3,4];//uint8显示的转换为uint256,否则会报类型错误。
uint[] memory memoryArr;
//storageArr[0] = 12;
//memoryArr[0] = 13; //执行会报VM error: invalid opcode.,原因是数组还没有执行初始化。

storageArr = new uint[](5);
memoryArr = new uint[](5);

storageArr[0] = 12;
memoryArr[0] = 13;
}
}

Memory数组

对于memory的变长数组,不支持修改length属性,来调整数组大小。memory的变长数组虽然可以通过参数灵活指定大小,但一旦创建,大小不可调整。

push方法

变长的storage数组和bytes(不包括string)有一个push()方法。可以将一个新元素附加到数组最后端,返回值为当前长度uint。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.0;

contract Test {

uint[] arr;

function a() public returns (uint) {
arr.push(1);// 初始化前调用
arr = new uint[](1);
arr[0] = 0;

uint len = arr.push(1);//先数组的最后添加一个元素1,方法返回的是数组的长度

return len;
}
}

memory的数组不可修改,不支持push方法。

多维数组

uint[2][3]在大多数语言中,表示的是两行三列的数组,而Solidity切好相反,它表示的是三行两列的数组。但是访问数组的方法与其他语言一致。

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.4;

contract Test {

uint[2][3] arr = [[1,2],[3,4],[5,6]];

function arr_len() public returns (uint) {

return arr.length; //返回值为3
}
}

固定的字节数组和可变的字节数组

  • bytes和string是一种特殊的数组。bytes类似于byte[],但在外部函数作为参数调用中,会进行压缩打包,更省空间,所以应该尽量使用bytes而不是byte[].
  • bytes0~bytes32 表示创建固定字节大小的数组,不可修改。
  • string是特殊的可变字节数组。可以转换为bytes以通过length获得它的字节长度。也可以通过索引来修改对应的字节内容,通过push方法来增加字节内容。
  • 由于bytes与string,可以自由转换,你可以将字符串s通过bytes(s)转为一个bytes。但需要注意的是通过这种方式访问到的是UTF-8编码的码流,并不是独立的一个个字符。比如中文编码是多字节,变长的,所以你访问到的很有可能只是其中的一个代码点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pragma solidity ^0.4.0;

contract Test {
// 声明一个固定长度的数组,不可修改
bytes9 a = 0x6c697975656368756e;
byte[9] b = [byte(0x6c),0x69,0x79,0x75,0x65,0x63,0x68,0x75,0x6e];

byte[] c = new byte[](10);

// function setAIndex0Byte() public {
// // 错误,不可修改
// a[0] = 0x89;
// }

function setBIndex0Byte() public {
b[0] = 0x89;
}

function setC() public {
for(uint i = 0; i < b.length; i++) {

c.push(b[i]);
c.push(b[i]);
}
}
}

结构体

  • 不能声明一个struct同时将这个struct作为这个struct的一个成员。这个限制是基于结构体的大小必须是有限的。
  • 在函数中,将一个struct赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值时,会影响到原变量。
  • 通常情况下不会考虑使用 uint 变种,因为无论如何定义 uint的大小,Solidity 都会为它保留256位的存储空间。例如,使用 uint8 而不是uint(uint256)不会节省任何 gas。
  • 而如果一个 struct 中有多个 uint,则尽可能使用较小的 uint, Solidity 会将这些 uint 打包在一起,从而占用较少的存储空间。
  • 结构体是不对外可见的(当前只支持internal),所以只可以在当前合约,或合约的子类中使用。包含自定义结构体的函数均需要声明为internal或private的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
pragma solidity ^0.4.0;

contract CrowdFunding{
struct Funder{
address addr;
uint amount;
}

struct Campaign{
address beneficiary;
uint goal;
uint amount;
uint funderNum;
mapping(uint => Funder) funders;
}

uint compaingnID;
mapping (uint => Campaign) campaigns;

function candidate(address beneficiary, uint goal) returns (uint compaingnID){
// initialize
campaigns[compaingnID++] = Campaign(beneficiary, goal, 0, 0);
}

function vote(uint compaingnID) payable {
Campaign c = campaigns[compaingnID];

//another way to initialize
c.funders[c.funderNum++] = Funder({addr: msg.sender, amount: msg.value});
c.amount += msg.value;
}

function check(uint comapingnId) returns (bool){
Campaign c = campaigns[comapingnId];

if(c.amount < c.goal){
return false;
}

uint amount = c.amount;
// incase send much more
c.amount = 0;
if(!c.beneficiary.send(amount)){
throw;
}
return true;
}
}

字典

定义方式为mapping(_KeyType => _KeyValue)。键的类型允许除映射外的所有类型,如数组,合约,枚举,结构体。值的类型无限制。

在映射表中,我们并不存储键的数据,仅仅存储它的keccak256哈希值,用来查找值时使用。因此,映射并没有长度,键集合(或列表),值集合(或列表)这样的概念。

映射类型,仅能用来定义状态变量,或者是在内部函数中作为storage类型的引用。引用是指你可以声明一个,如var storage mappVal的用于存储状态变量的引用的对象,但你没办法使用非状态变量来初始化这个引用。

可以通过将映射标记为public,来让Solidity创建一个访问器。要想访问这样的映射,需要提供一个键值做为参数。如果映射的值类型也是映射,使用访问器访问时,要提供这个映射值所对应的键,不断重复这个过程。下面来看一个例子:

1
2
3
4
5
6
7
8
contract MappingExample{
mapping(address => uint) public balances;

function update(uint amount) returns (address addr){
balances[msg.sender] = amount;
return msg.sender;
}
}

由于调试时,你不一定方便知道自己的发起地址,所以把这个函数,略微调整了一下,以在调用时,返回调用者的地址。编译上述合同后,可以先调用update(),执行成功后,查看调用信息,能看到你更新的地址,这样再查一下这个地址的在映射里存的值。

如果你想通过合约进行上述调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pragma solidity ^0.4.0;

//file indeed for compile
//may store in somewhere and import
contract MappingExample{
mapping(address => uint) public balances;

function update(uint amount) returns (address addr){
balances[msg.sender] = amount;
return msg.sender;
}
}

contract MappingUser{

address conAddr;
address userAddr;

function f() returns (uint amount){
//address not resolved!
//tringing
conAddr = hex"0xf2bd5de8b57ebfc45dcee97524a7a08fccc80aef";
userAddr = hex"0xca35b7d915458ef540ade6068dfe2f44e8fa733c";

return MappingExample(conAddr).balances(userAddr);
}
}

映射并未提供迭代输出的方法,可以自行实现一个数据结构。

运算符delete

delete运算符,用于将某个变量重置为初始值。对于整数,运算符的效果等同于a = 0。而对于定长数组,则是把数组中的每个元素置为初始值,变长数组则是将长度置为0。对于结构体,也是类似,是将所有的成员均重置为初始值。delete对于映射类型几乎无影响,因为键可能是任意的,且往往不可知。所以如果你删除一个结构体,它会递归删除所有非mapping的成员。当然,你是可以单独删除映射里的某个键,以及这个键映射的某个值。

需要强调的是delete a的行为更像赋值,为a赋予一个新对象。我们来看看下文的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pragma solidity ^0.4.0;

contract DeleteExample {
uint data;
uint[] dataArray;

function f() {
//值传递
uint x = data;
//删除x不会影响data
delete x;

//删除data,同样也不会影响x,因为是值传递,它存的是一份原值的拷贝。
delete data;

//引用赋值
uint[] y = dataArray;

//删除dataArray会影响y,y也将被赋值为初值。
delete dataArray;

//下面的操作为报错,因为删除是一个赋值操作,不能向引用类型的storage直接赋值从而报错
//delete y;
}
}

通过上面的代码,我们可以看出,对于值类型,是值传递,删除x不会影响到data,同样的删除data也不会影响到x。因为他们都存了一份原值的拷贝。而对于复杂类型略有不同,复杂类型在赋值时使用的是引用传递。删除会影响所有相关变量。比如上述代码中,删除dataArray同样会影响到y。由于delete的行为更像是赋值操作,所以不能在上述代码中执行delete y,因为不能对一个storage的引用赋值

类型推断

为了方便,并不总是需要明确指定一个变量的类型,编译器会通过第一个向这个对象赋予的值的类型来进行推断1。

1
2
uint24 x = 0x123;
var y = x;

函数的参数,包括返回参数,不可以使用var这种不指定类型的方式。

需要特别注意的是,由于类型推断是根据第一个变量进行的赋值。所以代码for (var i = 0; i < 2000; i++) {}将是一个无限循环,因为一个uint8的i的将小于2000。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.4;

contract Test{
function a() returns (uint){
uint count = 0;
for (var i = 0; i < 2000; i++) {
count++;
if(count >= 2100){
break;
}
}
return count;
}
}

单位

货币单位

一个字面量的数字,可以使用后缀wei,finney,szabo或ether来在不同面额中转换。不含任何后缀的默认单位是wei。如2 ether == 2000 finney的结果是true。

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;

contract EthUnit{
uint a;

function f() returns (bool){
if (2 ether == 2000 finney){
return true;
}

return false;
}
}

时间单位(Time Units)

seconds,minutes,hours,days,weeks,years均可做为后缀,并进行相互转换,默认是seconds为单位。

变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。Unix时间传统用一个32位的整数进行存储,这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。所以,如果我们想让我们的 DApp 跑够20年,我们可以使用64位整数表示时间,但为此我们的用户又得支付更多的 gas。真是个两难的设计啊!

如果你需要进行使用这些单位进行日期计算,需要特别小心,因为不是每年都是365天,且并不是每天都有24小时,因为还有闰秒。由于无法预测闰秒,必须由外部的oracle来更新从而得到一个精确的日历库(内部实现一个日期库也是消耗gas的)。

后缀不能用于变量。如果你想对输入的变量说明其不同的单位,可以使用下面的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;

contract DeleteExample{

function nowInSeconds() returns (uint256){
return now;
}

function f(uint start, uint daysAfter) {
if (now >= start + daysAfter * 1 days) {

}
}
}

内置特性

特殊变量及函数

区块和交易的属性:

  • block.blockhash(uint blockNumber) returns (bytes32),给定区块号的哈希值,只支持最近256个区块,且不包含当前区块。
  • block.coinbase (address) 当前块矿工的地址。
  • block.difficulty (uint)当前块的难度。
  • block.gaslimit (uint)当前块的gaslimit。
  • block.number (uint)当前区块的块号。
  • block.timestamp (uint)当前块的时间戳。
  • msg.data (bytes)完整的调用数据(calldata)。
  • msg.gas (uint)当前还剩的gas。
  • msg.sender (address)当前调用发起人的地址,总是存在。
  • msg.sig (bytes4)调用数据的前四个字节(函数标识符)。
  • msg.value (uint)这个消息所附带的货币量,单位为wei。
  • now (uint)当前块的时间戳,等同于block.timestamp
  • tx.gasprice (uint) 交易的gas价格。
  • tx.origin (address)交易的发送者(完整的调用链)

msg的所有成员值,如msg.sender,msg.value的值可以因为每一次外部函数调用,或库函数调用发生变化(因为msg就是和调用相关的全局变量)。如果你想在库函数中,用msg.sender实现访问控制,你需要将msg.sender做为参数(就是说不能使用默认的msg.value,因为它可能被更改)。为了可扩展性的原因,你只能查最近256个块,所有其它的将返回0.

数学和加密函数

  • asser(bool condition):如果条件不满足,抛出异常。
  • addmod(uint x, uint y, uint k) returns (uint):计算(x + y) % k。加法支持任意的精度。但不超过(wrap around?)2**256。
  • mulmod(uint x, uint y, uint k) returns (uint):计算(x * y) % k。乘法支持任意精度,但不超过(wrap around?)2**256。
  • keccak256(…) returns (bytes32):使用以太坊的(Keccak-256)计算HASH值。紧密打包。
  • sha3(…) returns (bytes32):等同于keccak256()。紧密打包。
  • sha256(…) returns (bytes32):使用SHA-256计算HASH值。紧密打包。
  • ripemd160(…) returns (bytes20):使用RIPEMD-160计算HASH值。紧密打包。
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):通过签名信息恢复非对称加密算法公匙地址。如果出错会返回0,附录提供了一个例子1.
  • revert():取消执行,并回撤状态变化。

地址相关

  • .balance (uint256):Address的余额,以wei为单位。
  • .transfer(uint256 amount):发送给定数量的ether,以wei为单位,到某个地址。失败时抛出异常。
  • .send(uint256 amount) returns (bool):发送给定数量的ether,以wei为单位,到某个地址。失败时返回false。
  • .call(...) returns (bool):发起底层的call调用。失败时返回false。
  • .callcode(...) returns (bool):发起底层的callcode调用,失败时返回false。
  • .delegatecall(...) returns (bool):发起底层的delegatecall调用,失败时返回false。

使用send方法需要注意,调用栈深不能超过1024,或gas不足,都将导致发送失败。使用为了保证你的ether安全,要始终检查返回结果。当用户取款时,使用transfer或使用最佳实践的模式2。

合约相关

  • this(当前合约的类型):当前合约的类型,可以显式的转换为Address
  • selfdestruct(address recipt):销毁当前合约,并把它所有资金发送到给定的地址。

进阶

控制语句

不支持switch和goto,支持if,else,while,do,for,break,continue,return,?:。条件判断中的括号不可省略,但在单行语句中的大括号可以省略。

函数调用

内部函数调用

在当前的合约中,函数可以直接调用(内部调用方式),包括也可递归调用。

外部函数调用

表达式this.g(8);和c.g(2)(这里的c是一个合约实例)是外部调用函数的方式。实现上是通过一个消息调用,而不是直接通过EVM的指令跳转。需要注意的是,在合约的构造器中,不能使用this调用函数,因为当前合约还没有创建完成。

其它合约的函数必须通过外部的方式调用。对于一个外部调用,所有函数的参数必须要拷贝到内存中。当调用其它合约的函数时,可以通过选项.value(),和.gas()来分别指定,要发送的ether量(以wei为单位),和gas值。

命名参数调用和匿名函数参数

函数调用的参数,可以通过指定名字的方式调用,但可以以任意的顺序,使用方式是{}包含。但参数的类型和数量要与定义一致。

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;

contract C {
function add(uint val1, uint val2) returns (uint) { return val1 + val2; }

function g() returns (uint){
// named arguments
return add({val2: 2, val1: 1});
}
}

省略函数名称

没有使用的参数名可以省略(一般常见于返回值)。这些名字在栈(stack)上存在,但不可访问。

1
2
3
4
5
6
7
8
pragma solidity ^0.4.0;

contract C {
// omitted name for parameter
function func(uint k, uint) returns(uint) {
return k;
}
}

创建合约实例

一个合约可以通过new关键字来创建一个合约。要创建合约的完整代码,必须提前知道,所以递归创建依赖是不可能的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.0;

contract Account{
uint accId;

//construction?
function Account(uint accountId) payable{
accId = accountId;
}
}

contract Initialize{
Account account = new Account(10);

function newAccount(uint accountId){
account = new Account(accountId);
}

function newAccountWithEther(uint accountId, uint amount){
account = (new Account).value(amount)(accountId);
}
}

从上面的例子可以看出来,可以在创建合约中,发送ether,但不能限制gas的使用。如果创建因为out-of-stack,或无足够的余额以及其它任何问题,会抛出一个异常。

赋值

Solidity内置支持元组(tuple),可以同时返回多个结果,也可用于同时赋值给多个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pragma solidity ^0.4.0;

contract C {
uint[] data;

function f() returns (uint, bool, uint) {
return (7, true, 2);
}

function g() {
// Declares and assigns the variables. Specifying the type explicitly is not possible.
var (x, b, y) = f();
// Assigns to a pre-existing variable.
(x, y) = (2, 7);
// Common trick to swap values -- does not work for non-value storage types.
(x, y) = (y, x);
// Components can be left out (also for variable declarations).
// If the tuple ends in an empty component,
// the rest of the values are discarded.
(data.length,) = f(); // Sets the length to 7
// The same can be done on the left side.
(,data[3]) = f(); // Sets data[3] to 2
// Components can only be left out at the left-hand-side of assignments, with
// one exception:
(x,) = (1,);
// (1,) is the only way to specify a 1-component tuple, because (1) is
// equivalent to 1.
}
}

作用范围和声明

函数内定义的变量,在整个函数中均可用,无论它在哪里定义,因为Solidity使用了javascript的变量作用范围的规则。与常规语言语法从定义处开始,到当前块结束为止不同。由此,下述代码编译时会抛出一个异常,Identifier already declared。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pragma solidity ^0.4.0;

contract ScopingErrors {
function scoping() {
uint i = 0;

while (i++ < 1) {
uint same1 = 0;
}

while (i++ < 2) {
uint same1 = 0;// Illegal, second declaration of same1
}
}

function minimalScoping() {
{
uint same2 = 0;
}

{
uint same2 = 0;// Illegal, second declaration of same2
}
}

function forLoopScoping() {
for (uint same3 = 0; same3 < 1; same3++) {
}

for (uint same3 = 0; same3 < 1; same3++) {// Illegal, second declaration of same3
}
}

function crossFunction(){
uint same1 = 0;//Illegal
}
}

另外的,如果一个变量被声明了,它会在函数开始前被初始化为默认值。所以下述例子是合法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;

contract C{
function foo() returns (uint) {
// baz is implicitly initialized as 0
uint bar = 5;
if (true) {
bar += baz;
} else {
uint baz = 10;// never executes
}
return bar;// returns 5
}
}

随机数

在Solidity中无法安全地生成随机数,Solidity 中最好的随机数生成器是 keccak256 哈希函数,可以这样来生成一些随机数:

1
2
3
4
5
// 生成一个0到100的随机数:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;

这个方法可以被不诚实的节点攻击,假设我们有一个硬币翻转合约——正面你赢双倍钱,反面你输掉所有的钱。假如它使用上面的方法来决定是正面还是反面 (random >= 50 算正面, random < 50 算反面)。如果我正运行一个节点,我可以只对我自己的节点发布一个事务,且不分享它。我可以运行硬币翻转方法来偷窥我的输赢:如果我输了,我就不把这个事务包含进我要解决的下一个区块中去。我可以一直运行这个方法,直到我赢得了硬币翻转并解决了下一个区块,然后获利。

当然,这需要很大的算力来保证自己可以挖矿成功。

异常

可以使用throw来手动抛出一个异常。抛出异常的效果是当前的执行被终止且被撤销(值的改变和帐户余额的变化都会被回退)。异常还会通过Solidity的函数调用向上冒泡(bubbled up)传递。(send,和底层的函数调用call,delegatecall,callcode是一个例外,当发生异常时,这些函数返回false)。捕捉异常是不可能的。

require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行:

1
2
3
4
5
6
7
8
function sayHiToVitalik(string _name) public returns (string) {
// 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
// (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
// 两字符串的 keccak256 哈希值来进行判断)
require(keccak256(_name) == keccak256("Vitalik"));
// 如果返回 true, 运行如下语句
return "Hi!";
}
1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;

contract Sharer {
function sendHalf(address addr) payable returns (uint balance) {
if (!addr.send(msg.value / 2))
throw; // also reverts the transfer to Sharer
return this.balance;
}
}

用户可以通过下述方式触发一个异常:

  • 调用throw。
  • 调用require,但参数值为false。

通过assert判断内部条件是否达成,require验证输入的有效性。这样的分析工具,可以假设正确的输入,减少错误。这样无效的操作码将永远不会出现。

assert 和 require 区别在于,require 若失败则会返还给用户剩下的 gas, assert 则不会。

合约详解

合约

Solidity中合约有点类似面向对象语言中的类。合约中有用于数据持久化的状态变量(state variables),和可以操作他们的函数。调用另一个合约实例的函数时,会执行一个EVM函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量(state variables)就不能访问了。

合约可以通过Solidity,或不通过Solidity创建。当合约创建时,一个和合约同名的函数(构造器函数)会调用一次,用于初始化。构造器函数是可选的。仅能有一个构造器,所以不支持重载。

如果不通过Solidity,我们可以通过web3.js,使用JavaScript的API来完成合约创建。

当一个文件中有多个contract时,默认将合约名大写的那个合约当做主合约。

构造函数

修改为:

1
2
3
4
5
contract Test {
constructor() {
// ...
}
}

析构函数

1
2
3
4
5
function kill() {
if (owner == msg.sender) { // 检查谁在调用
selfdestruct(owner); // 销毁合约
}
}

可见性和权限控制

Solidity有两种函数调用方式,一种是内部调用,不会创建一个EVM调用(也叫做消息调用),另一种则是外部调用,会创建EVM调用(会发起消息调用)。Solidity对函数和状态变量提供了四种可见性。分别是external,public,internal,private。其中函数默认是public。状态变量默认的可见性是internal。

  • external: 外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。一个外部函数f,不能通过内部的方式来发起调用,(如f()不可以,但可以通过this.f())。外部函数在接收大的数组数据时更加有效。
  • public: 可以在任何地方调用,不管是内部还是外部。对于public类型的状态变量,会自动创建一个访问器(详见下文)。
  • internal:这样声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或继承的合约里调用。需要注意的是不能加前缀this,前缀this是表示通过外部方式访问。
  • private:私有函数和状态变量仅在当前合约中可以访问,在继承的合约内,不可访问。private修饰的函数名一般以”_”开头.

备注:

  • internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。
  • 所有在合约内的东西对外部的观察者来说都是可见,将某些东西标记为private仅仅阻止了其它合约来进行访问和修改,但并不能阻止其它人看到相关的信息。
  • 可见性的标识符的定义位置,对于state variable是在类型后面,函数是在参数列表和返回关键字中间。来看一个定义的例子:
1
2
3
4
5
6
7
pragma solidity ^0.4.0;

contract C {
function f(uint a) private returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}

在下面的例子中,D可以调用c.getData()来访问data的值,但不能调用f。合约E继承自C,所以它可以访问compute函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pragma solidity ^0.4.0;

contract C {
uint private data;

function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }
}


contract D {
function readData() {
C c = new C();
uint local = c.f(7); // error: member "f" is not visible
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // error: member "compute" is not visible
}
}


contract E is C {
function g() {
C c = new C();
uint val = compute(3, 5); // acces to internal member (from derivated to parent contract)
}
}

访问函数

编译器为自动为所有的public的状态变量创建访问函数。下面的合约例子中,编译器会生成一个名叫data的无参,返回值是uint的类型的值data。状态变量的初始化可以在定义时完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;


contract C{
uint public c = 10;
}

contract D{
C c = new C();

function getDataUsingAccessor() returns (uint){
return c.c();
}
}

函数修饰符

modifier 可以用来改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约重写(override)。下面我们来看一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
pragma solidity ^0.4.0;

contract owned {
function owned() { owner = msg.sender; }
address owner;

// This contract only defines a modifier but does not use
// it - it will be used in derived contracts.
// The function body is inserted where the special symbol
// "_;" in the definition of a modifier appears.
// This means that if the owner calls this function, the
// function is executed and otherwise, an exception is
// thrown.
modifier onlyOwner {
if (msg.sender != owner)
throw;
// require(msg.sender == owner);
_;
}
}


contract mortal is owned {
// This contract inherits the "onlyOwner"-modifier from
// "owned" and applies it to the "close"-function, which
// causes that calls to "close" only have an effect if
// they are made by the stored owner.
function close() onlyOwner {
selfdestruct(owner);
}
}


contract priced {
// Modifiers can receive arguments:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}


contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;

function Register(uint initialPrice) { price = initialPrice; }

// It is important to also provide the
// "payable" keyword here, otherwise the function will
// automatically reject all Ether sent to it.
function register() payable costs(price) {
registeredAddresses[msg.sender] = true;
}

function changePrice(uint _price) onlyOwner {
price = _price;
}
}
  • 修改器可以被继承,使用将modifier置于参数后,返回值前即可。
  • 特殊_表示使用修改符的函数体的替换位置。
  • 从合约Register可以看出全约可以多继承,通过,号分隔两个被继承的对象。
  • 修改器也是可以接收参数的,如priced的costs。

使用修改器实现的一个防重复进入的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.0;
contract Mutex {
bool locked;
modifier noReentrancy() {
if (locked) throw;
locked = true;
_;
locked = false;
}

/// This function is protected by a mutex, which means that
/// reentrant calls from within msg.sender.call cannot call f again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() noReentrancy returns (uint) {
if (!msg.sender.call()) throw;
return 7;
}
}

例子中,由于call()方法有可能会调回当前方法,修改器实现了防重入的检查。如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。需要注意的是,在Solidity的早期版本中,有修改器的函数,它的return语句的行为有些不同。在修改器中和函数体内的显式的return语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的”_”后继续执行。修改器的参数可以是任意表达式。在对应的上下文中,所有的函数中引入的符号,在修改器中均可见。但修改器中引入的符号在函数中不可见,因为它们有可能被重写。

常量

常量

状态变量可以被定义为constant,常量。这样的话,它必须在编译期间通过一个表达式赋值。赋值的表达式不允许:1)访问storage;2)区块链数据,如now,this.balance,block.number;3)合约执行的中间数据,如msg.gas;4)向外部合约发起调用。也许会造成内存分配副作用表达式是允许的,但不允许产生其它内存对象的副作用的表达式。内置的函数keccak256,keccak256,ripemd160,ecrecover,addmod,mulmod可以允许调用,即使它们是调用的外部合约。

允许内存分配,从而带来可能的副作用的原因是因为这将允许构建复杂的对象,比如,查找表。虽然当前的特性尚未完整支持。

编译器并不会为常量在storage上预留空间,每个使用的常量都会被对应的常量表达式所替换(也许优化器会直接替换为常量表达式的结果值)。

不是所有的类型都支持常量,当前支持的仅有值类型和字符串。

新版本中:

  • view: 把函数定义为 view, 意味着它只能读取数据不能更改数据
  • pure: pure 函数表明这个函数不访问应用里的数据
1
2
3
4
5
6
7
pragma solidity ^0.4.0;

contract C {
uint constant x = 32**22 + 8;
string constant text = "abc";
bytes32 constant myHash = keccak256("abc");
}

常函数

函数也可被声明为常量,这类函数将承诺自己不修改区块链上任何状态。

1
2
3
4
5
6
7
pragma solidity ^0.4.0;

contract C {
function f(uint a, uint b) constant returns (uint) {
return a * (b + 42);
}
}

访问器(Accessor)方法默认被标记为constant。当前编译器并未强制一个constant的方法不能修改状态。但建议大家对于不会修改数据的标记为constant。

回退函数

每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。

下述提供给回退函数可执行的操作会比常规的花费得多一点。

  • 写入到存储(storage)
  • 创建一个合约
  • 执行一个外部(external)函数调用,会花费非常多的gas
  • 发送ether

请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pragma solidity ^0.4.0;

contract Test {
// This function is called for all messages sent to
// this contract (there is no other function).
// Sending Ether to this contract will cause an exception,
// because the fallback function does not have the "payable"
// modifier.
function() { x = 1; }
uint x;
}


// This contract keeps all Ether sent to it with no way to get it back.
contract Sink {
function() payable { }
}


contract Caller {
function callTest(Test test) {
test.call(0xabcdef01); // hash does not exist
// results in test.x becoming == 1.

// The following call will fail, reject the
// Ether and return false:
test.send(2 ether);
}
}

payable

1
2
3
4
5
6
7
8
contract OnlineStore {
function buySomething() external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == 0.001 ether);
// 如果为真,一些用来向函数调用者发送数字内容的逻辑
transferThing(msg.sender);
}
}

提现

在发送以太之后,它将被存储进合约的以太坊账户中,并冻结在哪里,可以写一个函数来从合约中提现以太,类似这样:

1
2
3
4
5
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}

事件

事件是合约和区块链通讯的一种机制。前端应用“监听”某些事件,并做出反应。(触发事件需要添加emit关键字)

例子:

1
2
3
4
5
6
7
8
9
// 这里建立事件
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
uint result = _x + _y;
//触发事件,通知app
emit IntegersAdded(_x, _y, result);
return result;
}

app前端可以监听这个事件,JavaScript 实现如下:

1
2
3
YourContract.IntegersAdded(function(error, result) { 
// 干些事
}

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
pragma solidity ^0.4.0;

contract owned {
function owned() { owner = msg.sender; }
address owner;
}


// Use "is" to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}


// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
contract Config {
function lookup(uint id) returns (address adr);
}


contract NameReg {
function register(bytes32 name);
function unregister();
}


// Multiple inheritance is possible. Note that "owned" is
// also a base class of "mortal", yet there is only a single
// instance of "owned" (as for virtual inheritance in C++).
contract named is owned, mortal {
function named(bytes32 name) {
Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
NameReg(config.lookup(1)).register(name);
}

// Functions can be overridden by another function with the same name and
// the same number/types of inputs. If the overriding function has different
// types of output parameters, that causes an error.
// Both local and message-based function calls take these overrides
// into account.
function kill() {
if (msg.sender == owner) {
Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
NameReg(config.lookup(1)).unregister();
// It is still possible to call a specific
// overridden function.
mortal.kill();
}
}
}


// If a constructor takes an argument, it needs to be
// provided in the header (or modifier-invocation-style at
// the constructor of the derived contract (see below)).
contract PriceFeed is owned, mortal, named("GoldFeed") {
function updateInfo(uint newInfo) {
if (msg.sender == owner) info = newInfo;
}

function get() constant returns(uint r) { return info; }

uint info;
}

基类构造器的方法

派生的合约需要提供所有父合约需要的所有参数,所以用两种方式来做,见下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

contract Base {
uint x;
function Base(uint _x) { x = _x; }
}


contract Derived is Base(7) {
function Derived(uint _y) Base(_y * _y) {
}
}

或者直接在继承列表中使用is Base(7),或像修改器(modifier)使用方式一样,做为派生构造器定义头的一部分Base(_y * _y)。第一种方式对于构造器是常量的情况比较方便,可以大概说明合约的行为。第二种方式适用于构造的参数值由派生合约的指定的情况。在上述两种都用的情况下,第二种方式优先(一般情况只用其中一种方式就好了)。

继承有相同名字的不同类型成员

当继承最终导致一个合约同时存在多个相同名字的修改器或函数,它将被视为一个错误。同新的如果事件与修改器重名,或者函数与事件重名都将产生错误。作为一个例外,状态变量的getter可以覆盖一个public的函数。

抽象(Abstract Contracts)

抽象函数是没有函数体的的函数。如下:

1
2
3
4
5
pragma solidity ^0.4.0;

contract Feline {
function utterance() returns (bytes32);
}

这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;

contract Feline {
function utterance() returns (bytes32);

function getContractName() returns (string){
return "Feline";
}
}


contract Cat is Feline {
function utterance() returns (bytes32) { return "miaow"; }
}

如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。

接口

接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:

  • 不能继承其它合约,或接口。
  • 不能定义构造器
  • 不能定义变量
  • 不能定义结构体
  • 不能定义枚举类
  • 其中的一些限制可能在未来放开。

接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。接口用自己的关键词表示:

1
2
3
interface Token {
function transfer(address recipient, uint amount);
}

合约可以继承于接口,因为他们可以继承于其它的合约。

Gas优化

  • 结构体中尽量使用uint的少位数,且考虑字节对其
  • “view” 函数不花 “gas”,这是因为 view 函数不会真正改变区块链上的任何数据。如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的(当玩家从web.js外部调用)。
  • 在大多数编程语言中,遍历大数据集合都是昂贵的。但是在 Solidity 中,使用一个标记了external view的函数,遍历比 storage 要便宜太多,因为 view 函数不会产生任何花销。
  • 为了降低成本,不到万不得已,避免将数据写入存储。这也会导致效率低下的编程逻辑:比如每次调用一个函数,都需要在 memory(内存) 中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找。

智能合约升级

将合约进行分离,先部署数据合约,部署后不变。业务合约调用数据合约读取修改数据。保证数据类型不变的情况下,可以对业务合约进行修改,新增。之后部署新的业务合约,废除旧的业务合约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
pragma solidity ^0.4.18;

contract DataContract {
mapping (address => uint256) public balanceOf;
mapping (address => bool) accessAllowed;

function DataContract() public {
accessAllowed[msg.sender] = true;
}

function setBlance(address _address,uint256 v) public {
balanceOf[_address] = v;
}

modifier platform() {
require(accessAllowed[msg.sender] == true);
_;
}

function allowAccess(address _addr) platform public {
accessAllowed[_addr] = true;
}

function denyAccess(address _addr) platform public {
accessAllowed[_addr] = false;
}
}

contract ControlContract {

DataContract dataContract;

function ControlContract(address _dataContractAddr) public {
dataContract = DataContract(_dataContractAddr);
}

function addTen(address addr) public returns (uint){
return dataContract.balanceOf(addr) + 11;
}
}

部署方法如下:

  1. 先部署DataContract合约。
  2. 使用DataContract合约地址作为部署ControlContract合约的参数。
  3. 用ControlContract合约地址作为参数调用DataContract合约的allowAccess方法。

如果需要更新控制合约(如修复了addTen)则重新执行第2-3步,同时对老的控制合约执行denyAccess()。

代币

一个代币在以太坊上就是一个遵循一些共同规则的智能合约:以太坊代币标准(ERC-Token Standard)。建立在以太坊网络上的区块链项目代币,需要遵从以下几种代币标准:ERC-20,ERC-223,ERC-621,ERC-721,ERC-827。其中 ERC 是 Ethereum Request for Comments 的简称。

ERC-20

这是最广泛被大家认可的一种代币形式,简单的列举一些通用的标准函数:

  • function totalSupply() 定义 Token 的总量;
  • function balanceOf(address tokenOwner) 显示用户账户余额;
  • function allowance(address tokenOwner, address spender) 返回剩余金额,显示 address spender 能从 address tokenOwner 里提取的数量;
  • function transfer(address to, uint tokens) 转移对应的金额到指定地址;
  • function approve(address spender, uint tokens)  returns (bool success) 允许  address spender 提取部分 Token;
  • function transferFrom(address from, address to, uint tokens) returns (bool success) 从一个地址转移 token 到另一个地址;

拥有以上所有必要的函数实现我们称为兼容 ERC-20 标准,但在具体实现中会做一些扩展,比如 ERC-223。

ERC-223

这个标准支持所有 ERC-20 的函数、智能合约以及服务,并解决了一些 ERC-20 的缺陷,比如说:在 ERC-20 标准下如果你输入了错误的收款地址,你转账的费用可能会永远丢失,但在 ERC-223 里这个问题被避免了,同时在这个标准下你需要消耗的 GAS 费用只有 ERC-20 的一半。

ERC-621

ERC-621 也是一个基于 ERC-20 升级的标准,解决了 ERC-20 不允许 Token 总量更改的问题,不过为了解决这个问题,ERC-621 增加了两种新的函数:

increaseSupply 和 decreaseSupply

ERC-827

这个标准比 ERC-20 更加灵活,除用于转账外,还可以转移数据和让第三方在获取用户允许的情况下为用户转账。

ERC-721

ERC-721 与 ERC-20 有很大的区别,如果说 ERC-20 与 ERC-223,ERC-621 能够在使用中自由转换的话,ERC-721 是不可与 ERC-20 Token 互相转换的,因为 ERC-721 拥有唯一性。这种 Token 依然可以在交易所里交易,只不过无法分割是一个独立的整体。

1
2
3
4
5
6
7
8
9
10
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}

ERC721 规范有两种不同的方法来转移代币:

  1. 第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId。
  2. 第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。
    • 所有者,用新主人的 address 和你希望他获取的 _tokenId 来调用 approve
    • 新主人用 _tokenId 来调用 takeOwnership,合约会检查确保他获得了批准,然后把代币转移给他。

Ownable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address public owner;

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}

/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}

/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}

SafeMath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
library SafeMath {

function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}

function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}

function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}

function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}

SafeMath 库允许使用 using 关键字,它可以自动把库的所有方法添加给一个数据类型:

1
2
3
4
5
using SafeMath for uint;
// 这下我们可以为任何 uint 调用这些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了

所以简而言之, SafeMath 的 add, sub, mul, 和 div 方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。

背景

”深圳市迅雷网络技术有限公司“前身称“深圳市三代科技开发有限公司”(简称“三代科技”),三代科技开发有限公司于2002年底由邹胜龙先生及程浩先生始创于美国硅谷。2003年1月底,两位创办者回国发展并正式成立深圳市三代科技开发有限公司(简称“三代科技”)。由于发展的需要,“三代科技”于2005年5月正式更名为“深圳市迅雷网络技术有限公司”,暨迅雷在大中华区的研发中心和运营中心。即我们通常所称的“迅雷”。

迅雷立足于为全球互联网提供最好的多媒体下载服务。经过艰苦创业,迅雷在大中华地区以领先的技术和诚信的服务,赢得广大用户的深深喜爱和许多合作伙伴的认同与支持。公司旗舰产品,迅雷,已经成为中国互联网最流行的应用服务软件之一。后又陆续开始进军云计算与区块链领域,推出了玩客云,星域云和迅雷链等产品。[1]

过程

迅雷的一些发展历程:

  • 2006年,迅雷用户数突破1亿,成为用户规模最大的下载加速软件
  • 2007年,迅雷首次对外发布视频业务迅雷看看
  • 2008年,发布迅雷软件助手、迅雷游戏、迅雷安全助手等
  • 2009年,迅雷群侠传网页游戏发布;迅雷会员系统正式发布
  • 2010年,迅雷用户数突破3亿,付费用户达100万
  • 2011年,迅雷看看成为国内首家实现全网高清的在线视频网站
  • 2012年,手机迅雷发布,手机下载进入高速时代
  • 2013年,迅雷用户数突破4.6亿,付费用户达520万
  • 2014年,迅雷开始做云计算和CDN服务
  • 2015年,迅雷、小米联合发布“星域CDN”,随后星域CDN陆续与小米、爱奇艺达成战略合作;迅雷赚钱宝Pro上市,引发用户秒抢热潮
  • 2016年,迅雷赚钱宝全新升级为星域共享平台,获得超过400万“玩客”支持;星域CDN发布“卓越版”和“标准版”直播新品,成为小米、爱奇艺、熊猫直播、bilibili、陌陌等百家企业直播业务首选服务商
  • 2017年,迅雷短视频日均播放量破亿;迅雷召开玩客云战略发布会,宣布共享计算和区块链的全新战略
  • 2018年,超级区块链平台迅雷链正式发布,引领区块链进入3.0时代;星域CDN升级为综合型云计算平台——星域云;迅雷举办全球区块链应用大赛

事件

提起”迅雷“这个词,可能大部分的人心中都是满满的回忆感,如一个相处十多年的老友般熟悉,可如今细细想来,去网上搜一搜迅雷现在的业务,却会发现现在的迅雷早已经不是当初那个只有下载业务,靠着会员收费盈利的公司了,它早已不仅仅是一个简单的下载工具了。

迅雷早期的时候,是一个比较纯粹的下载工具,在下载领域,前前后后出现过许多的下载工具,如QQ旋风,VeryCD电驴,快车等,也都成功过,但在技术与行业的发展之下,最终活下来的只有迅雷。在2007年前后,几乎每个PC用户有两个软件都是必装的,一个是QQ,而另一个便是迅雷。当时迅雷用户达到了4亿,装机量达到8000万台,市场份额超过50%,是中国互联网仅次于QQ的客户端软件。而达到这一成就,迅雷仅仅用了4年。这样的迅雷曾经感染了无数的有志青年,在那段时间,有许多的腾讯员工跳槽到了迅雷,弄得腾讯非常紧张,甚至影响到了马化腾。腾讯前产品运营副总监黄卓生回忆道,“Pony挺忌惮这个事,在内部开会,要抢占这个入口,在PC时代,大家都还在强调装机量。”[2] 腾讯后来推出了QQ旋风,可惜最后也败下阵来,停更下架。

然而,迅雷却没有如腾讯等公司一样抓住发展的机遇,扶摇直上,发展成为互联网大头,反之,迅雷前几年一直在走下坡路,接连错过了许多的发展机遇与风口,慢慢地从巨无霸沦落成为了路人,而前段时间迅雷的内讧,也让人唏嘘。

曾经有许多专业人士,其中也包括迅雷自己内部的管理人员,对迅雷错过的一些风口做过总结,迅雷在互联网的发展过程中,先后错过了”浏览器”,“流媒体”以及“应用商店”等重要的机遇。一观迅雷的发展历程,迅雷都曾从这些风口中找到过切入点,投入过一小部分资源发展这些方向,但大部分都浅尝辄止,等到重点投入资源的时候,已经是红海市场了。

转折

2014年11月,前腾讯云计算公司总裁陈磊正式出任迅雷CTO,同时兼任迅雷旗下网心科技CEO,2015年11月,迅雷董事会通过决议任命陈磊为迅雷网络技术有限公司联席CEO。陈磊曾在腾讯负责过腾讯云和腾讯开放平台的管理工作,也曾在Google和微软从事过云计算相关的工作。由下载工具起家的迅雷,却选了一个云计算相关的人才做CEO,事情自然会有所转折。

果然,陈磊没有辜负大家的期望。在他上任后,他对公司业务方向做出了果断的调整,始终坚持以技术创新带动业务发展的策略,也取得了一些阶段性的成果。近两年来,迅雷推出了一个名为星域云的产品,这是一款创新型的云计算产品。2017年年初,迅雷的云计算业务营收规模开始高速增长,连续多个季度均保持数十个百分比的增速。在云计算业务的拉动下,迅雷逐渐突破了原有的商业模式,不再局限于传统的下载业务,而是在其基础之上衍生出了一些更具发展前景的新业务。

2018年11月14日,迅雷正式发布了截至2018年9月30日的第三季度未经审计的财务报告。财报显示,迅雷报告期内总营收为4530万美元,同比增长1.1%,连续3年实现同比增长;毛利率52.7%,而2017年同期为35.5%。[3]

而毛利率提升的主要原因来自于带宽成本的大幅降低,这主要得益于迅雷独有的共享计算模式,它通过玩客云收集闲置的带宽资源,进行再分配,从而让用户就近获取内容,加快访问速度。而对于用户来说,它通过赚钱宝奖励的方式使用户贡献出自己的闲置带宽,用户从中获得奖励,而迅雷则使得这些用户无形之中都成为了自己的CDN节点,大幅度地降低了部署CDN的成本。

在云计算成为迅雷的业务重心后,它还在集中资源发展其区块链业务,吸取之前对错过的风口浅尝辄止的教训,看准了方向之后,All in区块链,在风口之上创新。迅雷是国内共享计算与区块链的先行者,已实现将区块链技术与共享计算业务结合,吸引了海量共享计算的参与者。2018年4月,迅雷正式发布了当前全球范围内唯一实现百万TPS性能的主链——迅雷链,同期迅雷也举办了一场全球区块链开发大赛,为迅雷链聚集了两千多名来自世界各地的开发者。在7月初的大赛决赛上,迅雷又发布了其自主研发的一个迅雷链文件系统TCFS(Thunder Chain File System),进一步完善了迅雷链的开发者生态。这些技术高度让迅雷成为国内区块链行业的领军者,目前迅雷链已经向生态化发展,并被视为区块链3.0时代的标杆性主链。

在今年的第三季度,迅雷及旗下网心科技被中国管理科学研究院授予“中国区块链技术示范基地”,以表彰迅雷在区块链行业的领头作用以及为行业发展做出的重大贡献。同年10月份,在世界级管理奖项“拉姆.查兰管理实践奖”的评选中,迅雷集团凭借近几年的成功转型实践,荣获了这一管理界的重量级荣誉奖项。

思考

平心而论,迅雷的技术能力是很强的,它本身也长于技术上的创新,他们发明了P2SP的内容传输算法,并第一个将之用于实际应用,同时也第一个将P2SP应用在流媒体点播领域。在最近一年,迅雷又用共享的思想颠覆了传统CDN的应用方式,通过玩客云将家庭中原本闲置的带宽、存储和计算资源收集回收,转化为低成本的云计算服务输送至互联网企业。共享计算绕开了传统云计算耗能耗资巨大的数据中心模式,从而得以实现带宽成本的大幅度降低。

然而,迅雷之所以错过了这么多的机遇,有一个原因是没有定好一个准确而正确的战略,战略上的不确定,会导致战术的失误,也因此,迅雷虽然都有涉足过浏览器,流媒体以及应用商店等方向,却都没有深入地进行研究与开发,没有All in 他们的优势资源,最终导致当行业已经到了红海之时,已经失去了最好的时机。

而对于当时的CEO邹胜龙先生来说,很多人把矛头指向了他。他过于干涉项目,总是亲自去参与某些项目,自身过于深入项目的细节。CEO在很多时候,应该关注大方向,而不是过度执着于某些细节,不然反而会成为项目进行的阻碍。在迅雷的历史上,有几个里程碑式的大版本,比如说迅雷6,就是邹胜龙亲自关心开发的,最后却都不了了之。和他对比鲜明的是马化腾,马化腾自己会亲自去体验产品,对产品的细节要求很严格,但他从来只把握大方向,提出要修改的细节,却不会干涉项目具体的执行。

另外,邹胜龙先生也过于关注技术了,总给人一种理工男,技术宅的感觉,从没有站出来为自己的产品代言过。而且他凡事太过于精打细算,放不开。就像当年做视频一样,他由于觉得版权价格太高而选择了退出,结果却错失良机。

在迅雷的巅峰岁月2007的年初,《环球企业家》杂志采访邹胜龙,问他当年的最重要目标是什么,邹胜龙的回答却是:“保住目前的市场份额”。当时的竞争已经很激烈了,腾讯的竞争产品已经推出来了,网际快车也准备卷土重来。然而他的目标不是与竞争对手争,而是保住目前的市场份额,这也许是跟他的性格有关吧,他不喜欢打仗,喜欢关起门来把事做好,一个标准的技术男形象。

在陈磊上任CEO之后,针对迅雷的发展,提出了改革和转型的方案,突破传统的局限,开展新业务,进军云计算和区块链的领域,不止在技术上创新,同时也追求商业模式上的创新,抓住发展趋势,明确战略方向,重点投入资源发展,使这个曾经的巨无霸有了新的转机与发展。

结论

  • 领导者不应事事亲为,过分拘泥于细节
  • 领导者需要有长远的眼光,不能只在意眼前的得失
  • 领导者需要能看清楚发展的趋势,企业或者公司需要有明确的战略方向,才能决定投入的资源比例,才能抓住机遇。
  • 在互联网行业,需要有创新才能更好地发展,如果故步自封,踟蹰不前,则终将被淘汰。
  • 技术创新固然重要,商业模式上的创新也不容忽视。

附录

[1]:百度百科词条:深圳市迅雷网络技术有限公司
[2]:投资界:https://news.pedaily.cn/201711/423767.shtml
[3]:同花顺财经:http://stock.10jqka.com.cn/usstock/20181114/c608162184.shtml

区块链概述

区块链并非单一创新技术,而是将许多跨领域技术凑在一起,包括密码学、数学、演算法与经济模型,并结合点对点网路关系,利用数学基础就能建立信任效果,成为一个不需基于彼此信任基础、也不需仰赖单一中心化机构就能够运作的分散式系统,而比特币便是第一个采用区块链技术而打造出的一套P2P电子现金系统,用来实现一个可去中心化,并确保交易安全性、可追踪性的数位货币体系。

区块链:一种特殊的公开的分布式数据库,具有分布式存储,分布式记录,去中心化,分布式维护等特性。

  • 全民记账
  • 不可篡改(只能增查,不能改删)
  • 可追溯
  • 去中心化

核心技术:

  • 区块链底层
  • 智能合约
  • 密码学和数字签名用于身份地址标识
  • 共识算法用于工作量证明
  • 分布式存储技术用于存储交易记录和区块,分布式网络技术用于网络通信和节点发现

区块链分类:

  • 共有链
  • 私有链
  • 联盟链

区块链版本:

  • 区块链1.0-可编程货币:数字货币去中心化,数字化货币及支付系统
  • 区块链2.0-可编程金融:智能合约,数字资产,金融应用
  • 区块链3.0-可编程社会:去中心化互信网络,去中心化信任机制,公证,仲裁,审计,物流,医疗等领域

区块链2.0是指智能合约,智能合约与货币相结合,对金融领域提供了更加广泛的应用场景,代表是’以太坊’,以太坊 = 区块链 + 智能合约。

账本验证

  • Hash

账户所有权

非对称加密:公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

  • 账户用一个地址(Hash值)来标识
  • 一个地址拥有一个私钥(如果私钥丢失了,比特币可能会丢失;私钥丢失了的话,是不能找回的;私钥可以通过两次哈希处理得到地址,而地址不能推导出私钥;非对称)
  • 不泄露私钥的情况下证明我们拥有某个地址的私钥:通过交易签名(非对称)的方式
    • 把交易进行Hash得到摘要
    • 用私钥对摘要进行签名(加密),同时私钥也生成付款方的公钥
    • 广播交易,验证信息
    • 用签名和付款方的公钥进行验证,返回交易摘要,验证是否是原始的交易信息(解密)

私钥-公钥关系:

  • 公钥和私钥成对出现
  • 公开的密钥叫公钥,只有自己知道的叫私钥
  • 用公钥加密的数据只有对应的私钥可以解密
  • 用私钥加密的数据只有对应的公钥可以解密
  • 如果可以用公钥解密,则必然是对应的私钥加的密
  • 如果可以用私钥解密,则必然是对应的公钥加的密

钱包

加密数字货币是一种基于区块链技术的数字货币,加密数字货币钱包是专门用来管理这些资产的应用。加密数字货币钱包提供钱包地址的创建、加密数字货币转账、每个钱包地址交易历史的查询等基础金融功能。钱包应用按照密码学原理创建1个或多个钱包地址,每个钱包地址都对应1个密钥对:私钥和公钥。公钥是根据私钥进行一定的数学运算生成,与私钥一一对应。公钥主要是对外交易使用,每次交易都必须使用私钥对交易记录进行签名以证明对相关钱包地址里面的资产有控制权。私钥是唯一能够证明对于数字资产有控制权的凭证,因此对于数字资产钱包来说,私钥是最重要的。私钥的生成和存储方式决定了资产安全与否。因此,通常意义上的数字资产安全其实就是私钥的安全,一个钱包是不是安全主要看它能否安全的管理和使用私钥。

挖矿(记账)

  • 记账:Hash打包过程
  • 节点消耗资源,有奖励

挖矿-工作量证明:

  1. 一段时间内,只有一人可以记账成功
  2. 通过解决密码学难题(即工作量证明)竞争获得唯一记账权
  3. 其他节点复制记账结果

普通的hash打包过程会获得一个摘要信息(hash值),规定获得的摘要信息必须以特定值开头才能算作挖矿成功,获得唯一记账权,故需要引入一个“随机数”作为Hash运算的参数。

共识机制

  • 两个节点同时完成工作量证明,使用谁的区块,使用共识机制。
  • 节点工作量只有在其他得节点认同其是有效的,因此会主动遵守规定
  • 每个节点会选择一条累计工作量最大(最长)的区块链,延长最长链

例如,两个节点同时完成工作量证明,分别打包成3458A和3458B,周围的节点选择出累计工作量最大(/最长)的区块链为3458A后进行广播,以3458A作为主链,3458B作为备用链。若之后节点接收到3458B传来的3459B区块,周围的节点选择出累计工作量最大(/最长)的区块链为3458B<——3459B,此时再把备用链3458B作为主链,3458B<——3459B接入区块链。有时候某些节点先接收了3459B,此时会将3459B作为孤块保存,一旦该节点接收到3458B,就会将3458B<——3459B接入区块链。

交易原理

当你发起一笔比特币交易后,将交易广播到全网,挖矿节点接收到交易后,先将其放入本地内存池进行一些基本验证,比如该笔交易发费的比特币是否是未被花费的交易,如果验证成功,则将其放入”未确认交易池“等待被打包,如果验证失败,则被标记为”无效交易“不会被打包,也就是说,旷工在寻找nonce的同时还需要及时验证每笔交易。

所谓比特币交易就是从一个比特币钱包向另一个中转账,每笔交易都有数字签名来保证安全。一个交易一旦发生那么就是对所有人都公开的,每个交易的历史可以最终追溯到相应的比特币最初被挖出来的那个点。

比特币转账需要支付给旷工手续费,按交易所占的字节数计费,旷工一般会优先打包手续费高的交易到区块里。

比特币的UTXO模型

比特币系统没有余额的概念,它使用的是UTXO模型(Unspent Transaction Outputs,未使用过的交易输出),我们在交易过程中经常说的钱包余额,实际上是一个钱包地址的UTXO集合。所以,在比特币网络中,存储比特币余额的是交易输出,准确点说就是未使用过的交易输出,而每一笔交易的输入实际上引用的是上一笔交易的输出。所以,要计算一个用户的比特币余额,就需要遍历整个交易的历史。而以太坊由于采用了Account模型,也就是采用余额的概念,所以不需要溯源整个交易历史。

交易分类

交易分为2类:

  • Coinbase交易:挖矿奖励,没有输入,只有输出
  • 普通交易:用户之间的普通转账交易

交易过程

假设,由于Alice挖矿被奖励了12.5个比特币。而Alice在一笔交易中,需要转账给Bob10个比特币。而Bob最终确认并接收了Alice发送的10个比特币,而同时由于多出了2.5个比特币。其实这笔交易最终是生成了2个输出,一个是发送给Bob的10个比特币,另一个是找零产生的发给Alice的2.5个比特币(备注:这里不考虑交易费)。

Alice的锁定脚本的作用是,设定成只有Alice才能使用这笔输出。而要使用这个UTXO,就必须要证明自己是Alice。

比特币交易解码后的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"version": 1,
"locktime": 0,
#交易输入(input)部分
"vin": [
{
#输入引用的交易(transaction)HASH
"txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",

#引用交易中的UTXO索引(第一个为0,此处代表上述txid交易中的第一个UTXO)
"vout": 0,

#解锁脚本,用于解锁UTXO的脚本(这是可以花费这笔UTXO的关键信息)
"scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813
[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
"sequence": 4294967295
}
],
#交易输出(output)部分
"vout": [
{
#第一个输出的比特币数量
"value": 0.01500000,
#锁定脚本,后续的交易如要使用该输出,必须解锁锁定脚本
"scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG"
},
{
#第二个输出的比特币数量
"value": 0.08450000,
"scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG",
}
]
}

交易脚本

比特币客户端使用一个用类Forth脚本语言编写的脚本去验证比特币的交易,这个脚本语言不是图灵完备的,不具备循环等复杂的特性。它是一种基于堆栈的执行语言,该脚本语言的简单特性,虽然使得它不能实现复杂的功能,但是也提高了交易脚本的安全性(设计简单,减少了攻击面)。而以太坊就是诟病比特币交易脚本功能有限,所以设计了一个图灵完备的脚本语言,也就是我们常说的智能合约脚本语言,能实现更复杂的功能,但同时也增加了安全隐患。

当一笔比特币交易被验证时,每一个输入中的解锁脚本与其所引用的输出中的锁定脚本同时执行,从而检查这笔交易是否有效。如图所示,是最为常见类型的比特币交易的解锁和锁定脚本:

验证过程:当我们拿到一笔交易时,将当前输入的解锁脚本,和该输入所引用的上一笔交易输出的锁定脚本进行特定的验证过程,最终若返回TRUE,说明交易有效。

区块链架构模型

  • 数据层:封装了底层数据区块的链式结构,以及相关的非对称的公钥私钥加密技术和时间戳技术;是整个区块链最底层的数据结构。
  • 网络层:P2P机制,数据传播机制,数据间认证机制
  • 共识层:共识机制算法
  • 激励层:用于公有链,联盟链
  • 合约层:智能合约
  • 应用层:与上述区块链核心架构分开部署,通过RPC(Remote Procedure Call:远程过程调用)互联

链式结构

Hash函数:

  • MD系列:已经不再安全
  • SHA系列:推荐SHA256, SHA3
1
2
3
4
5
6
func hash(input string) string  {
// func Sum256(data []byte) [Size]byte
hashInByte :=sha256.Sum256([]byte (input))
// func EncodeToString(src []byte) string
return hex.EncodeToString(hashInByte[:])
}

ICO和IPO

  • ICO(Initial Coin Offering),首次币发行,源自股票市场的首次公开发行(IPO)概念,是区块链项目首次发行代币,募集比特币、以太坊等通用数字货币的行为。
  • IPO(Initial Public Offerings),首次公开募股,是指一家企业或公司 (股份有限公司)第一次将它的股份向公众出售(首次公开发行,指股份公司首次向社会公众公开招股的发行方式)。

智能合约

智能合约是一套以数字形式定义的承诺,包括合约参与方可以在上面执行这些承诺的协议,也就是说智能合约是一套能够自动执行某些手动才能完成任务的协议。其实智能合约相当于在计算机系统中构建一份参与方均可读的合同。只有当某一事件触发后,则会立即执行。例如,A向B付款XX元,B则给予A货物。

智能合约并不是一定要依赖于区块链来实现,而区块链的部分基础特性决定了智能合约更加适合于在区块链上来实现。比如去中心化,数据的防篡改,高可用性等。去中心化能够保证数据的全网备份与不可受第三方机构的干扰,无需担心数据会被篡改。同时也立于以后的审计工作。高可用性不会存在如目前的中心服务或者中心存储系统受到攻击或其他问题而发生合约不执行的问题。其实综合来说,区块链给予智能合约最好的特性就是“信任机制。

目前来说,智能合约同样也有很多的局限性。例如线下的问题解决起来还是无法与线上的问题相提并论的。如商品问题,给予的商品质量如何评估与上链,是否还需要依赖于第三方? 同时智能合约的编写者对于合约的细节把控必须特别的严谨。一是合约漏洞,不严谨的合约造成对某个参与者的损失。二是可能会出现Bug,而如果bug被黑客所利用那么就会造成重大的损失。同样智能合约的法律问题也是要考虑的重点。

以太坊

以太坊(英文Ethereum)是一个开源的有智能合约功能的公共区块链平台,通过其专用加密货币以太币(Ether)提供去中心化的虚拟机(“以太虚拟机” Ethereum Virtual Machine)来处理点对点合约。

比特币并不完美,其中协议的扩展性是一项不足,例如比特币网络里只有一种符号——比特币,用户无法自定义另外的符号,这些符号可以是代表公司的股票,或者是债务凭证等,这就损失了一些功能。另外,比特币协议里使用了一套基于堆栈的脚本语言,这语言虽然具有一定灵活性,使得像多重签名这样的功能得以实现,然而却不足以构建更高级的应用,例如去中心化交易所等。以太坊从设计上就是为了解决比特币扩展性不足的问题。

以太坊是一个平台,它上面提供各种模块让用户来搭建应用。具体来说,以太坊通过一套图灵完备的脚本语言(EthereumVirtual Machinecode,简称EVM语言)来建立应用,它类似于汇编语言,但以太坊里的编程并不需要直接使用EVM语言,而是类似C语言、Python、Lisp等高级语言,再通过编译器转成EVM语言。

上面所说的平台之上的应用,其实就是合约,这是以太坊的核心。合约是一个活在以太坊系统里的自动代理人,他有一个自己的以太币地址,当用户向合约的地址里发送一笔交易后,该合约就被激活,然后根据交易中的额外信息,合约会运行自身的代码,最后返回一个结果,这个结果可能是从合约的地址发出另外一笔交易。需要指出的是,以太坊中的交易,不单只是发送以太币而已,它还可以嵌入相当多的额外信息。如果一笔交易是发送给合约的,那么这些信息就非常重要,因为合约将根据这些信息来完成自身的业务逻辑。合约所能提供的业务,几乎是无穷无尽的,它的边界就是你的想象力,因为图灵完备的语言提供了完整的自由度,让用户搭建各种应用。

比特币网络事实上是一套分布式的数据库,而以太坊则更进一步,她可以看作是一台分布式的计算机:区块链是计算机的ROM,合约是程序,而以太坊的矿工们则负责计算,担任CPU的角色。这台计算机不是、也不可能是免费使用的,不然任何人都可以往里面存储各种垃圾信息和执行各种鸡毛蒜皮的计算,使用它至少需要支付计算费和存储费,当然还有其它一些费用。

分叉

比特币社区开发者一直在致力于改进比特币,但是不像传统软件的升级,一个分布式共识系统的升级是非常困难的,需要协调好所有的系统参与者。每次升级可能会伴随着区块链的共识规则改变,这会导致整个网络中升级了软件的节点与未升级软件的节点运行在不同的规则下,于是分叉就产生了。

硬分叉

如果区块链软件的共识规则被改变,并且这种规则改变无法向前兼容,旧节点无法认可新节点产生的区块,即为硬分叉。这时候旧节点会拒绝新规则的区块,于是新节点和旧节点会开始在不同的区块链上运行(挖矿、交易、验证等),由于新旧节点可能长期存在,这种分叉也可能会长期持续下去。

软分叉

如果区块链的共识规则改变后,这种改变是向前兼容的,旧节点可以兼容新节点产生的区块,即为软分叉。实际上,软分叉通常刚开始并不会产生两条区块链,因为新规则下产生的块会被旧节点接受,旧节点只是无法识别新规则的真实意义。所以新旧节点仍然处于同一条区块链上,对整个系统的影响也就较小。

共识算法

PoW

POW(Proof of Work):工作量证明,即你能够获得的币的数量,取决于你挖矿贡献的有效工作,也就是说,你用于挖矿的矿机的性能越好,分给你的收益就会越多,这就是根据你的工作证明来执行币的分配方式。通俗的说,PoW 的意思就是社会主义,按劳分配,多劳多得。

PoW 的优势与劣势:

  • PoW机制的设计目的是保证安全。无论是在中心化还是非中心化系统中,防止作弊都是很重要的。
  • PoW 假设大多数人不会作弊,如果你想作弊,你要有压倒大多数人的算力(51%攻击),但不能防止矿工抱团取暖。

PoS

PoS(Proof of Stake):权益证明,类似比特币这样的 PoW 币种挖矿带来了巨大的电力能源消耗,为了解决这种情况,所以有了 PoS。PoS 试图解决 PoW 机制中大量资源被浪费的情况。这种机制通过计算你持有占总币数的百分比以及占有币数的时间来决定记账权。在现实世界中 PoS 很普遍,最为熟知的例子就是股票。股票是用来记录股权的证明,股票持有量多的,拥有更高更多的投票权和收益权。通俗的说,PoS 就是资本主义,按钱分配,钱生钱。

  • Proof of Deposit:这次挖到矿的旷工投入的币会锁定一段时间才能接着投入。如果有分叉的话锁定的币只会在当前分支起锁定效果。

PoS 的优势与劣势:

  • Pos 当然也能防作弊,因为如果一名持有 51%以上股权的人作弊,相当于他坑了自己,因为一个人自己不会杀死自己的钱。
  • PoS 机制由股东自己保证安全,工作原理是利益捆绑。在这个模式下,不持有 PoS 的人无法对 PoS 构成威胁。PoS 的安全取决于持有者,和其他任何因素无关。

DPoS

算法概述

DPoS(Delegated Proof of Stake):委托股权证明,是 PoS 的进化方案,由 Dan Larimer 发明。(例子:比特股 BTS)

在常规 PoW 和 PoS 中,一大影响效率之处在于任何一个新加入的 Block,都需要被整个网络所有节点做确认。DPoS 优化方案在于:通过不同的策略,不定时的选中一小群节点,这一小群节点做新区块的创建,验证,签名和相互监督,这样就大幅度的减少了区块创建和确认所需要消耗的时间和算力成本。

DPOS算法分为两部分:选择一组块生产者和调度生产。选举过程确保利益相关方最终得到控制,因为当网络不能顺利运行时,利益相关方的损失最大。选举方法对实际运行中如何达成共识几乎没有影响,因此主要介绍如何在块生产者被选择之后达成共识。

假设3个块生产者A,B和C。因为共识(的达成)需要2/3+1多数来解决所有情况,这个简化的模型将假设生产者C是打破僵局的那个人。在现实世界中,将有21个或更多的块生产者。像工作量证明一样,一般规则是最长链胜出。任何时候当一个诚实的对等节点看到一个有效的更长链,它都会从当前分叉切换到更长的这条链。

正常操作

在正常操作模式下,块生产者每3秒钟轮流生成一个块。假设没有人错过自己的轮次,那么这将产生最长链。块生产者在被调度轮次之外的任何时间段出块都是无效的。

少数分叉

不超过节点总数三分之一的恶意或故障节点可能创建少数分叉。在这种情况下,少数分叉每9秒只能产生一个块,而多数分叉每9秒可以产生两个块。这样,诚实的2/3多数将永远比少数(的链)更长。

离线少数的多重生产

(离线的)少数人可以试图产生无限数量的分叉,但是他们的所有分叉都将比多数人的那条链短,因为少数人在出块速度上注定比多数人来的更慢。

网络碎片化

网络完全有可能碎片化,导致没有任何分叉拥有多数块生成者。在这种情况下,最长的链将倒向最大的那个少数群体。当网络连通性恢复时,较小的少数群体会自然切换到最长的那条链,明确的共识将恢复。

有可能存在这样三个分叉,其中两个最长的分叉长度相同。在这种情况下,第3个(较小)分叉的块生产者重新加入网络时会打破平局。块生产者总数为奇数,因此不可能长时间保持平局。稍后我们还会讲到生产者“洗牌”,它使得出块顺序随机化,从而确保即使是生产者数目相同的两个分叉也会以不同的步长增长,最终导致一个分叉超过另一个。

在线少数的多重生产

在这种场景下,少数节点B在其时间段内产生了两个或更多可供选择的块。下一个计划生产者(C)可以选择基于B产生的任何一种方案继续构建链条。一旦如此,这个选择就成为最长的链,而所有选择B1的节点都将切换分叉。少数不良生产者企图广播再多的替代块也无关紧要,它们作为最长链的一部分永远不会超过一轮。

最后不可逆块

在网络碎片化的情况下,多个分叉都有可能持续不断增长相当长的时间。长远来看最长的链终将获胜,但观察者需要一种确切的手段来判定一个块是否绝对处于增长最快的那条链。这可以通过观察来自2/3+1多数块生产者的确认来决定。

多数生产者舞弊

如果多数生产者变得腐败,那么他们可以产生无限数量的分叉,每个分叉都看起来以2/3多数确认向前走。这种情况下,最后不可逆块算法蜕变为最长链算法。最长链就是为最大多数所批准的那条链,而这将由少数剩下的诚实节点决定。这种行为不会持续很长时间,因为利益相关方最终会投票替换生产者。

确定性生产者洗牌

在上面所有例子中,我们展示的都是块生产者按循环调度出块。实际上,每出N个块(N是生产者数量),块生产者集合都会洗牌一次。这种随机性确保块生成者B不会总是忽略块生成者A,每当形成多个拥有相同数量生产者的分叉时,平局最终都会被打破。

结论

在每一个我们能想到的自然网络分裂的情况下,委托权益证明都是强健的,甚至在面对相当数量生产者舞弊的情形时也是安全的。不像其它共识算法,当大多数生产者不合格时,DPOS还是可以继续工作。在此过程中,社区可以投票替换掉不合格的生产者,直到恢复100%参与率。

说到底,DPOS引人注目的安全性来自于其选择块生产者和验证节点质量的算法。运用赞成投票的过程可以确保一个人即使拥有50%的有效投票权也不能独自挑选哪怕一个生产者。DPOS旨在优化拥有强壮网络连接的诚实节点100%参与(共识过程)的名义条件。这使得DPOS有能力在平均只有1.5秒的时间内以99.9%的确定性确认交易,同时以优雅和可检测的方式降级 – 从降级中恢复正常也不过是小事一桩。

PoW + PoS 混合机制

为了结合两种挖矿方式的优点,开始有了基于 PoW+PoS 混合共识机制的币。例如 Hcash,以及以太坊 ETH 也正在向 PoW+PoS 混合挖升级矿转变。

PoW + PoS 混合机制的优势:

  • 假设一个币它的机制是PoW + PoS 的混合机制。那么持有该币的用户与矿工均可以参与到投票中,共同参与该币社区的重大决定,持币者与矿工都可以影响预先编制好的更新,如隔离见证(SegWit)、增大区块等等。如果这些更新被广泛认可,无需开发者干预,链就会自动分叉以配合更新。而这才是真正的去中心化。
  • 以混合机制来实现广义上的 DAO(去中心化自治组织)的高效运行。通过 PoW+PoS 公平的按持币数量与工作量分配投票权重,实现社区自治。

DAG

概述

DAG:Directed Acyclic Graph,即“有向无环图”。区块链是每个区块记多笔交易,而DAG是每个区块存一笔交易,所以它们的本质相同。在IOTA白皮书里,把结扎在一起的交易称为缠结(Tangle)。

使用者每发起一笔交易,必须验证之前的两笔交易。如果一笔交易不被后来的交易所验证,它就会在账本里失去合法性。交易发起者自己选择两笔合法交易,花2秒钟找出一个随机数,让“随机数+信息”的哈希值符合系统要求。

验证所需的工作量与前手交易权重成正比,交易权重相当于验证难度,难度越高验证时间越长。IOTA中,权重是以3为底的指数增长:3的1次方、3的2次方、3的3次方……被验证次数越多的交易权重越大。所以,不用担心验证完两笔正确交易却没人来验证你,因为验证新鲜交易更容易,如果验证靠前的陈旧交易,工作量会指数级地翻上去.

如果只往前验证一笔,网络会被大算力操控。算力强者很容易抬高交易权重,拉长尾巴,以堵死后面的验证通路,让随后的诚实交易不得不屈从大算力;可验太多交易又会耗时过长。所以,验两笔能兼顾安全和效率。

于是,发起者一边提交自己的交易,一边验证别人的交易,以此编织着一个去中心化网络。

DAG保证账本安全

假如A转给B价值100万元的IOTA,B确认后把100万元货物交给A,A靠算力发起攻击,用一笔权重更大的交易验证合法交易之前的交易。只要超过主体诚实的DAG,随后的交易都会接在A的DAG后面生长,这样就赖掉之前的交易,白白从B手里拿走100万元的货。此时需要汇聚34%才能实现双重支付?

IOTA团队说,网络还没成熟,所以先找了个协管员看场子,这名协管员就是一台名叫Coordinator的服务器。所有交易是否合法,暂时全由这位协管员拍板,拍板后告诉其他节点,该验证哪些交易。

影响账本安全的另一个因素是数字签名,因为攻击者无法使用他们没有的私钥签出和你一样的数字签名,而保障这件事的是哈希算法.IOTA使用了自己开发的哈希算法curl,但是curl算法的哈希值极易发生碰撞,于是就能伪造数字签名。

IOTA的DAG是靠后手保护前手,一旦攻击者成功伪造数字签名,后手挑不出伪造者的错,非法交易就能大摇大摆地通过验证,这意味着别人用其他私钥也能撬走你账户里的钱。现在IOTA有协管员保护,但如果撤下协管员,签名能否会被仿冒,就完全得寄希望于攻击者没有哈希出和你一样的签名。而协管员自己也有私钥,一旦泄露,造成的结果将比服务器电源插座被拔还要严重,因为此时持有私钥者具备改动任意交易的技术可能,这就是集中管账的风险。

总结

DAG是一种数据存储结构,从它被发明的30多年来一直都有人使用,本身并没有问题。但它和区块链的区别在于DAG没有传统意义上的共识,每笔交易的可信与否取决于相信这笔交易的人数。所以采用DAG技术的核心问题在于如何保护全网达成的一致.

IOTA使用了中心化方案:先协管员看护,以后慢慢放开。同样采用DAG的另一种加密货币Byteball就很淳朴,12名矿工通过收交易手续费的方式保护系统安全。

DAG曾经作为比特币扩容的方案,但最终没被采用,因为基于DAG的分布式网络在保护共识方面很难比区块更有效。

HashNet

HashNet采用分层分片共识机制, 上层网络中的节点称为全节点(full node),主要负责下层分片建立、下层分片重组、新局部全节点加入、局部全节点退出,不参与全局共识,也不参与记账,这避免了形成性能瓶颈的风险,极大的提高了交易吞吐量。下层网络中的节点称为局部全节点(local full node),形成各个分片,片内进行交易达成共识,采用后缀匹配法确保每笔交易只由一个特定的分片处理,避免了双重支付,同时片间通过异步机制同步各个片内的共识结果,从而达到每个局部全节点拥有全局账本。

HashNet共识机制的主要优势在于:

  1. 全节点和局部全节点具有较强的稳定性和处理能力,能够有效避免HashGraph长时间无法达成共识的问题,也能够避免因网络被分割造成的恶意节点攻击问题;
  2. 与当前其他带分片的区块链项目相比,HashNet采用分布式异步分片重组,完全打破了集中式分片重组的机制,极大地提高了重组时的安全性;
  3. 交易共识不需要上层节点参与,交易达成速度极快(交易确认时间仅仅依赖于片内节点数量),更重要的是,交易吞吐量,即TPS,与下层分片数量成正比,即分片数量越多,TPS越高;
  4. 片内和片间节点均为对等,没有所谓的Leader,避免了潜在的中心化可能和性能瓶颈。

区块链项目

实践一-创建区块链

https://github.com/ljd1996/blockchain_go/tree/part_1

创建Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package code

import (
"crypto/sha256"
"encoding/hex"
"time"
"fmt"
)

type Block struct {
Index int64 // 区块编号
Timestamp int64 // 区块时间戳
PreBlockHash string // 上一个区块Hash值
Hash string // 当前区块Hash值
Data string //区块数据
}

func (block *Block) Print() {
fmt.Printf("Index: %d\n", block.Index)
fmt.Printf("PreHash: %s\n", block.PreBlockHash)
fmt.Printf("CurHash: %s\n", block.Hash)
fmt.Printf("Data: %s\n", block.Data)
fmt.Printf("Timestamp: %d\n", block.Timestamp)
}

/** 计算Hash值 */
func calculateHash(b Block) string {
blockData := string(b.Index) + string(b.Timestamp) + string(b.PreBlockHash) + string(b.Data)
hashByte := sha256.Sum256([]byte(blockData))
return hex.EncodeToString(hashByte[:])
}

/** 生成下一个区块 */
func GenNewBlock(preBlock Block, data string) Block {
newBlock := Block{}
newBlock.Index = preBlock.Index + 1
newBlock.Timestamp = time.Now().Unix()
newBlock.PreBlockHash = preBlock.Hash
newBlock.Data = data
newBlock.Hash = calculateHash(newBlock)
return newBlock
}

/** 生成创始区块 */
func GenGenesisBlock() Block {
preBlock := Block{}
preBlock.Index = -1
preBlock.Hash = ""
return GenNewBlock(preBlock, "Genesis Block")
}

创建BlockChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package code

import (
"log"
)

type BlockChain struct {
Blocks []*Block
}

func (bc *BlockChain) Print() {
for _, block := range bc.Blocks {
block.Print()
}
}

func (bc *BlockChain) SendData(data string) {
preBlock := bc.Blocks[len(bc.Blocks)-1]
newBlock := GenNewBlock(*preBlock, data)
bc.appendBlock(&newBlock)
}

func (bc *BlockChain) appendBlock(newBlock *Block) {
if len(bc.Blocks) == 0 {
bc.Blocks = append(bc.Blocks, newBlock)
return
}
if isValid(*newBlock, *bc.Blocks[len(bc.Blocks)-1]) {
bc.Blocks = append(bc.Blocks, newBlock)
} else {
log.Print("invalid block!")
}
}

func isValid(newBlock Block, oldBlock Block) bool {
return (newBlock.Index-1 == oldBlock.Index) &&
(newBlock.PreBlockHash == oldBlock.Hash) &&
(calculateHash(newBlock) == newBlock.Hash)
}

func NewBlockChain() *BlockChain {
genesisBLock := GenGenesisBlock()
blockChain := BlockChain{}
blockChain.appendBlock(&genesisBLock)
return &blockChain
}

实践二-工作量证明

https://github.com/ljd1996/blockchain_go/tree/part_2

Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"time"
)

// Block keeps block headers
type Block struct {
Timestamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int // 跟工作量证明相关
}

// NewBlock creates and returns Block
func NewBlock(data string, prevBlockHash []byte) *Block {
block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0}
pow := NewProofOfWork(block)
nonce, hash := pow.Run()

block.Hash = hash[:]
block.Nonce = nonce

return block
}

// NewGenesisBlock creates and returns genesis Block
func NewGenesisBlock() *Block {
return NewBlock("Genesis Block", []byte{})
}

BlockChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

// Blockchain keeps a sequence of Blocks
type Blockchain struct {
blocks []*Block
}

// AddBlock saves provided data as a block in the blockchain
func (bc *Blockchain) AddBlock(data string) {
prevBlock := bc.blocks[len(bc.blocks)-1]
newBlock := NewBlock(data, prevBlock.Hash)
bc.blocks = append(bc.blocks, newBlock)
}

// NewBlockchain creates a new Blockchain with genesis Block
func NewBlockchain() *Blockchain {
return &Blockchain{[]*Block{NewGenesisBlock()}}
}

ProofOfWork

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package main

import (
"bytes"
"crypto/sha256"
"fmt"
"math"
"math/big"
)

var (
maxNonce = math.MaxInt64
)

const targetBits = 24

// ProofOfWork represents a proof-of-work
type ProofOfWork struct {
block *Block
target *big.Int
}

// NewProofOfWork builds and returns a ProofOfWork
func NewProofOfWork(b *Block) *ProofOfWork {
target := big.NewInt(1)
target.Lsh(target, uint(256-targetBits))

pow := &ProofOfWork{b, target}

return pow
}

func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.Data,
IntToHex(pow.block.Timestamp),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
},
[]byte{},
)

return data
}

// Run performs a proof-of-work
func (pow *ProofOfWork) Run() (int, []byte) {
var hashInt big.Int
var hash [32]byte
nonce := 0

fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
for nonce < maxNonce {
data := pow.prepareData(nonce)

hash = sha256.Sum256(data)
fmt.Printf("\r%x", hash)
hashInt.SetBytes(hash[:])

if hashInt.Cmp(pow.target) == -1 {
break
} else {
nonce++
}
}
fmt.Print("\n\n")

return nonce, hash[:]
}

// Validate validates block's PoW
func (pow *ProofOfWork) Validate() bool {
var hashInt big.Int

data := pow.prepareData(pow.block.Nonce)
hash := sha256.Sum256(data)
hashInt.SetBytes(hash[:])

isValid := hashInt.Cmp(pow.target) == -1

return isValid
}

实践三-持久化(bolt)

https://github.com/ljd1996/blockchain_go/tree/part_3

实践四-交易

https://github.com/ljd1996/blockchain_go/tree/part_4

1
2
3
4
5
6
7
type Block struct {
Timestamp int64
Transactions []*Transaction
PrevBlockHash []byte
Hash []byte
Nonce int
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Transaction represents a Bitcoin transaction
type Transaction struct {
ID []byte
Vin []TXInput
Vout []TXOutput
}

// TXInput represents a transaction input
type TXInput struct {
Txid []byte
Vout int
ScriptSig string
}

// TXOutput represents a transaction output
type TXOutput struct {
Value int
ScriptPubKey string
}

实践五-地址及身份标识

https://github.com/ljd1996/blockchain_go/tree/part_5

实践六-钱包/交易/RPC

https://github.com/ljd1996/blockchain_go/tree/part_6

https://github.com/ljd1996/blockchain_go/tree/part_7

https://github.com/ljd1996/blockchain_go/tree/master

图灵

图灵

艾伦·麦席森·图灵(Alan Mathison Turing,1912年6月23日-1954年6月7日),英国数学家、逻辑学家,被称为计算机科学之父,人工智能之父。

图灵机

停机问题通俗地说,停机问题就是判断任意一个程序是否能在有限的时间之内结束运行的问题。该问题等价于如下的判定问题:是否存在一个程序P,对于任意输入的程序w,能够判断w会在有限时间内结束或者死循环。有人猜测图灵机模型是图灵在思考停机问题而顺带设计出来的.

图灵的基本思想是用机器来模拟人们用纸笔进行数学运算的过程,它运算过程看作下列两种简单的动作:

  • 在纸上写上或擦除某个符号;
  • 把注意力从纸的一个位置移动到另一个位置;

逻辑结构上图灵机有四个部分组成:

  • 一个无限长的存储带,带子有一个个连续的存储格子组成,每个格子可以存储一个数字或符号
  • 一个读写头,读写头可以在存储带上左右移动,并可以读、修改存储格上的数字或符号
  • 内部状态存储器,该存储器可以记录图灵机的当前状态,并且有一种特殊状态为停机状态
  • 控制程序指令,指令可以根据当前状态以及当前读写头所指的格子上的符号来确定读写头下一步的动作(左移还是右移),并改变状态存储器的值,令机器进入一个新的状态或保持状态不变。

图灵完备

可图灵指在可计算性理论中,编程语言或任意其他的逻辑系统如具有等用于通用图灵机的计算能力。换言之,此系统可与通用图灵机互相模拟。

简单来说,能够抽象成图灵机的系统或编程语言就是图灵完备的;一切可计算的问题图灵机都能计算,因此满足这样要求的逻辑系统、装置或者编程语言就叫图灵完备的。

在可计算性理论里,如果一系列操作数据的规则(如指令集、编程语言、细胞自动机)按照一定的顺序可以计算出结果,被称为图灵完备(turing complete)。

一个有图灵完备指令集的设备被定义为通用计算机。如果是图灵完备的,它(计算机设备)有能力执行条件跳转(if、while、goto语句)以及改变内存数据。如果某个东西展现出了图灵完备,它就有能力表现出可以模拟原始计算机,而即使最简单的计算机也能模拟出最复杂的计算机。所有的通用编程语言和现代计算机的指令集都是图灵完备的(C++ template就是图灵完备的),都能解决内存有限的问题。图灵完备的机器都被定义有无限内存,但是机器指令集却通常定义为只工作在特定的、有限数量的RAM上。

图灵完备的语言,有循环执行语句,判断分支语句等。理论上能解决任何算法。但有可能进入死循环而程序崩溃。

图灵不完备也不是没有意义,有些场景我们需要限制语言本身。如限制循环和递归, 可以保证该语言能写的程序一定是终止的。

比特币的脚本系统是图灵不完备的,以太坊的智能合约系统是图灵完备的。各有优缺点,图灵不完备会更安全些,图灵完备会更智能些。

IO模型

IO可以理解为对流的操作,当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备。
  2. 将数据从内核拷贝到进程中

同步阻塞IO

用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。

同步非阻塞IO

由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。

即用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

IO多路复用

IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

异步IO

“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

闭包

闭包的本质源自两点,词法作用域和函数当作值传递

  • 词法作用域就是按照代码书写时的样子,内部函数可以访问函数外面的变量。
  • 函数当作值传递,就是可以把函数当作一个值来赋值,当作参数传给别的函数,也可以把函数当作一个值 return。一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量,即函数所需要的数据结构保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性。本来执行过程和词法作用域是封闭的,这种返回的函数就好比是一个虫洞。

显然,闭包的形成很简单,在执行过程完毕后,返回函数,或者将函数得以保留下来,即形成闭包。

闭包可以理解成:一个持有外部环境变量的函数就是闭包。理解闭包通常有着以下几个关键点:

  1. 函数
  2. 自由变量
  3. 环境

举例如下:

1
2
3
4
5
6
7
function a(x, y) {
console.log(x, y) //在这里,x和y都不是自由变量
function b(){
console.log(x, y) //但在这个内部函数b中,x和y相对于b都是自由变量,而函数a的作用域则是环境。
}
//无论b最终是否会作为返回值被函数a返回,b本身都已经形成了闭包。
}

“闭”的意思不是封闭内部状态,而是封闭外部状态,当外部状态的scope失效的时候,还有一份保留在内部状态里,就是所说的封闭外部状态。

知乎有个回答:我叫独孤求败,我在一个山洞里,里面有世界上最好的剑法,还有最好的武器。我学习了里面的剑法,拿走了最好的剑。离开了这里。我来到这个江湖,快意恩仇。但是从来没有人知道我这把剑的来历,和我这一身的武功。那山洞就是一个闭包,而我,就是那个山洞里唯一一个可以与外界交汇的地方。这山洞的一切对外人而言就像不存在一样,只有我才拥有这里面的宝藏!

名词及概念

  • Coinbase transcation
  • 1 BTC(比特币) = 100 cBTC(比特分) = 1000 mBTC(毫比特) = 1000000 uBTC(微比特) = 1E8(100000000)聪
  • Genesis block:第一个区块(有特殊hash值)
  • mining,miner,nonce
  • selfish mining:恶意节点挖到区块后不发布,而是等到积累一定数量的区块后再一次性发布,用来防止six confirmation(需要恶意节点数超过50%)
  • 比特币 translation-based ledger
  • 以太坊 account-based ledger
  • 比特币中缺省six confirmation:防止恶意节点攻击(区块链增加一个区块后,后续要等待五个区块的添加)
  • 比特币:double spending attack(付款人);以太坊:replay attack(收款人)
  • bounty:赏金
  • 区块链的安全性:所有的全节点要独立验证发布的区块
  • zhenxiao.com/
  • 最初每个区块可以产生50枚比特币奖励,在每21万个区块后(每21万个区块约4年时间),每个区块的奖励减半(现在每个区块可以奖励12.5枚比特币),所以比特币共有2100万个,到2140年,比特币将无法细分。
  • 安全性保证:密码学以及共识机制
  • 密码朋克(cypherpunk):结合了电脑朋克的思想,在电脑化空间下的个体精神,使用强加密(密文)保护个人隐私。
  • 比特币水龙头网站:给访问网站的每个人分发小额比特币奖励,达到引流作用。
  • 每个钱包都是一个节点,其中拥有完整区块链账本的节点叫做全节点,只拥有根hash值和一些信息的叫轻节点。
  • 算力(也称哈希率)是比特币网络处理能力的度量单位。即为计算机(CPU)计算哈希函数输出的速度。一个挖矿机每秒钟能做多少次hash碰撞,就是其“算力”的代表,单位写成hash/s。
  • 零知识证明:证明者能够在不向验证者提供信息本身内容的情况下,使验证者相信某个论断是真实可信的一种技术.可以在不泄漏信息本身内容的情况下,证明拥有某个秘密.
  • 重放攻击:在比特币硬分叉后,新链与原链拥有相同的交易数据、地址、私钥、交易方式。在硬分叉之前的一种币,会因为分叉而变成两种,即可得到等额的新币.同时,在一种币上进行的交易,也可以广播到另一种币上.

比特币

概述

交易流程:

  1. 有一个新的交易,要向全网公布广播。
  2. 每一个节点,都要将收集到的交易信息,放入一个区块中。
  3. 每一个节点都要试着在自己的区块中,找到一个足够难的工作量证明。
  4. 当一个节点找到了自己的工作量证明,都要对全网进行公布广播。
  5. 当且仅当这个区块中的所有交易是有效且之前从未有过,其他的节点才承认这个交易过程的有效性。
  6. 其他节点表示承认这个区块,表示认可的方法就是,根据这个区块的编码,往后延长这个链接。

UTXO可以看做是一个比特币驱动的状态机。

密码学原理

数据结构

Hash Pointer

Merkle Tree

  • block header:包含某个区块所包含的所有交易组成的Merkle Tree组成的根Hash值,但是没有交易的具体内容。
  • block body:包含交易的列表和内容
  • 全节点:包含header和body
  • 轻节点:只包含header

Merkle Proof/Proof of membership/Proof of inclusion

  • 证明数据(交易)是否归属于此merkle树。
  • 在不需要存储整个数据的情况下,就可以简明地证明数据是否属于资料组的一部分。
  • 验证某些数据组是包含在更大的数据组中,而无须显示整个数据组或其子资料组。

Proof of non-membership

  • sorted merkle tree

共识协议

账本的内容要取得分布式的共识.

普通投票法:

  • 少数服从多数:恶意节点可以生成许多帐号投票,直到超过阈值(没有决定是否有投票权,简单的直接投票不行).

最长链投票法:

  • 分叉攻击:选择最长链
  • 两个节点同时记账成功(等长分叉):两个区块会保留一段时间,直到其中某个区块找到下一个区块.

实现

用户把交易发布到区块链网络上,节点把这些交易打包成区块,然后添加到区块链中.

可以通过修改Coinbase交易中的Coinbase字段来修改MarkleRoot的hash值,Coinbase tx中保存有旷工的地址,BlockHeader中保存有Nonce。

网络

挖矿

挖矿设备:

  • CPU->GPU->ASIC(Application Specific Integranted Circle)->矿池

矿池:

  • 一个pool manager(打包等计算hash之外的工作)和许多miner(只负责计算hash)
  • 可以用almost valid block(不可用)来计算每个miner的工作量.

脚本

比特币使用的脚本语言特别简单,基于堆栈。

思考

  • “区块恋”:把一个私钥分成两部分,一人持有一份,会降低安全性,破解私钥难度变低。
  • 使用多重签名(multisig)的方式让多个合伙人拥有同一个钱包。
  • 分布式共识:理论上被证明不可能(共识一旦达成就不可修改)。

以太坊

概述

可以把以太坊看做一个交易驱动的状态机(transaction-driven state machine)。以太坊(Ethereum)是一个建立在区块链技术之上,去中心化应用平台,它允许任何人在平台中建立和使用通过区块链技术运行的去中心化应用。

在没有以太坊之前,写区块链应用是这样的:拷贝一份比特币代码,然后去改底层代码如加密算法,共识机制,网络协议等等(很多山寨币就是这样,改改就出来一个新币)。

以太坊平台对底层区块链技术进行了封装,让区块链应用开发者可以直接基于以太坊平台进行开发,开发者只要专注于应用本身的开发,从而大大降低了难度。

账户

为防止replay attack,转账的时候转账人会携带一个nonce值来标识当前他已进行交易的总数,然后对这些信息进行签名。

账户分类:

  • 外部账户(externally owned account):人操作的账户。公私钥对,balance和nonce
  • 合约账户(smart contract account):智能合约的载体。balance,nonce,code(代码),storage();不能自己发起交易;

状态树

账户:account addr(160bits)->account state(40个16bits)

每次有账户记录更改都是新建新的MPT,只不过大部分节点是复用的(不直接在原MPT上修改是为了防止分叉出现时便于回滚)。

名称 类型 意义
parentHash common.Hash 父区块的哈希值
UncleHash common.Hash 叔父区块列表的哈希值
Coinbase common.Address 打包该区块的矿工的地址,用于接收矿工费
Root common.Hash 状态树的根哈希值
TxHash common.Hash 交易树的根哈希值
ReceiptHash common.Hash 收据树的根哈希值
Bloom Bloom 交易收据日志组成的Bloom过滤器
Difficulty *Big.Int 本区块的难度
Number *Big.Int 本区块块号,区块号从0号开始算起
GasLimit uint64 本区块中所有交易消耗的Gas上限,这个数值不等于所有交易的中Gas limit字段的和
GasUsed uint64 本区块中所有交易使用的Gas之和
Time *big.Int 区块产生的unix时间戳,一般是打包区块的时间,这个字段不是出块的时间戳
Extra []byte 区块的附加数据
MixDigest common.Hash 哈希值,与Nonce的组合用于工作量计算
Nonce BlockNonce 区块产生时的随机值

交易树和收据树

交易树和收据树都属于MPT,只把当前区块包含的交易组织起来,而状态树包含了所有的账户信息,块头包含这三棵树的根hash值。

bloom filter:支持查找某个元素是否包含在某个大的集合里(哈希映射)(一般来说不支持删除元素操作,因为考虑到哈希碰撞)。

Merkle Proof:每个交易完成后会生成一个收据,里面包含一个bloom filter记录交易的类型,地址等。发布的区块在它的块头里也有一个总的bloom filter,是这个区块里所有的bloom filter的并集。

GHOST协议

  • 一个区块可以包括两个uncle block(7代之内有共同的祖先,随着代数增加,奖励越来越少)。
  • 一个分叉的非主分支的区块,最多是接下来6个区块的uncle,是当前主分支区块的兄弟区块。
  • 只会接收当前分叉的第一个区块作为uncle block。
  • 主分支的区块招安两个uncle block都由多到少有一定的奖励。
  • 以太坊区块链中7代及其以内的叔父区块都能得到奖励,超过7代的叔父区块将不会得到奖励,这样是为了避免有些矿工专门在之前的链上制造分叉后坐等被后面的节点招安情况。
  • 以太坊中的出块奖励不会随着区块数量的增多而减少,以太坊中无论何时出块都会获得出块奖励,而比特币中区块的树目超过两千一百万以后就没有出块奖励,此后矿工挖矿的动力来自于交易费了。

挖矿算法

  • difficult to solve but easy to verify.
  • 以太坊使用GPU挖矿(原则上的设计应该是可以让旷工使用普通电脑的CPU挖矿)

挖矿过程(ethash算法):

  1. 对于每一个块,首先计算一个种子(seed),该种子只和当前块的信息有关;然后根据种子生成一个32M的随机数据集(cache),每隔30000个块会重新生成seed(对原来的seed求hash),并根据新的seed计算新的cache。cache的初始大小为16M,每隔30000个块大小增大。
  2. 根据Cache生成一个1GB大小的数据集合DAG(有向非循环图),它是一个完整的搜索空间,挖矿的过程就是从DAG中随机选择元素(类似于比特币挖矿中查找合适Nonce)再进行哈希运算,可以从Cache快速计算DAG指定位置的元素,进而哈希验证。每隔30000个块会重新更新,并增大容量。

轻节点可以只保存cache,而旷工要保存整个DAG。

难度调整算法

出块时间:15s

智能合约

概述

智能合约是运行在区块链上的一段代码,代码的逻辑定义了合约的内容,智能合约可以理解为在区块链上可以自动执行的(由消息驱动的)、以代码形式编写的合同(特殊的交易)。Solidity是智能合约最常用的语言。智能合约的账户保存了合约当前的运行状态:

  • balance:余额
  • nonce:交易次数
  • code:合约代码
  • storage:存储,数据结构是一颗MPT

EVM(Ethereum Virtual Machine)以太坊虚拟机是以太坊中智能合约的运行环境。它运行在以太坊节点上,当我们把合约部署到以太坊网络上之后,合约就可以在以太坊网络中运行了。

外部账户调用智能合约:创建一个交易,接收地址为要调用的智能合约的地址,data域填写要调用的函数和参数的编码值。

一个合约调用另一个合约中的函数:

  • 直接调用(代码中)
  • 使用address类型的call函数
  • 代理调用delegatecall()

智能合约的创建和运行:

  • 以太坊虚拟机上运行的是合约的字节码形式,需要我们在部署之前先对合约进行编译,代码编写完成后,编译成bytecode
  • 创建合约:外部账户发起一个转账交易到0x0的地址
    • 转账金额为0,但是要支付汽油费
    • 合约的代码放在data域
  • 智能合约运行在EVM(Ethereum Virtual Machine)上
  • 以太坊是交易驱动的状态机
    • 调用智能合约的交易发布到区块链上后,每个旷工都会执行这个交易,从当前状态确定性地转移到下一个状态

合约部署

  • 以太坊客户端,可以理解为一个开发者工具,它提供账户管理、挖矿、转账、智能合约的部署和执行等等功能
  • EVM是由以太坊客户端提供的

Geth是典型的开发以太坊时使用的客户端,基于Go语言开发。 Geth提供了一个交互式命令控制台,通过命令控制台中包含了以太坊的各种功能(API)。Geth控制台和Chrome浏览器开发者工具里的面的控制台是类似的,不过Geth控制台是跑在终端里。相对于Geth,Mist则是图形化操作界面的以太坊客户端。

智能合约的部署是指把合约字节码发布到区块链上,并使用一个特定的地址来标示这个合约,这个地址称为合约账户。

外部账户与合约账户的区别和关系是这样的:一个外部账户可以通过创建和用自己的私钥来对交易进行签名,来发送消息给另一个外部账户或合约账户。

在两个外部账户之间传送消息是价值转移的过程。但从外部账户到合约账户的消息会激活合约账户的代码,允许它执行各种动作(比如转移代币,写入内部存储,挖出一个新代币,执行一些运算,创建一个新的合约等等)。

只有当外部账户发出指令时,合同账户才会执行相应的操作。

合约部署就是将编译好的合约字节码通过外部账号发送交易的形式部署到以太坊区块链上(由实际矿工出块之后,才真正部署成功)。

合约运行

合约部署之后,当需要调用这个智能合约的方法时只需要向这个合约账户发送消息(交易)即可,通过消息触发后智能合约的代码就会在EVM中执行了。

Gas

  • 以太坊上用Gas机制来计费,Gas也可以认为是一个工作量单位,智能合约越复杂(计算步骤的数量和类型,占用的内存等),用来完成运行就需要越多Gas。
  • 任何特定的合约所需的运行合约的Gas数量是固定的,由合约的复杂度决定。
  • 而Gas价格由运行合约的人在提交运行合约请求的时候规定,以确定他愿意为这次交易愿意付出的费用:Gas价格(用以太币计价) * Gas数量。

Gas的目的是限制执行交易所需的工作量,同时为执行支付费用。当EVM执行交易时,Gas将按照特定规则被逐渐消耗,无论执行到什么位置,一旦Gas被耗尽,将会触发异常。当前调用帧所做的所有状态修改都将被回滚, 如果执行结束还有Gas剩余,这些Gas将被返还给发送账户。如果没有这个限制,就会有人写出无法停止(如:死循环)的合约来阻塞网络。

因此实际上(把前面的内容串起来),我们需要一个有以太币余额的外部账户,来发起一个交易(普通交易或部署、运行一个合约),运行时,矿工收取相应的工作量费用。

汽油费(gas fee):只有挖矿成功的那个人可以得到

  • 智能合约是一个Turing-complete Programming Model
  • 智能合约中的指定要收取汽油费,由交易的人来支付
  • EVM中不同的指令消耗的汽油费不一样
智能合约可以获取的区块信息

注意:

  • 旷工先执行智能合约,再进行挖矿
  • 旷工要验证刚发布的区块(无偿),才能更新状态,不然无法进行接下来的挖矿,因为发布的区块里没有三棵树的内容,只是块头里有根hash值。
  • 发布到区块链上的交易不一定是成功执行的,因为要发布后旷工才能拿到汽油费(即使是执行失败的交易也要支付汽油费)。
  • 不支持多线程,无法产生真正意义的随机数(智能合约必须有确定性)。

以太坊网络

以太坊官网测试网络Testnet

测试网络中,我们可以很容易获得免费的以太币,缺点是需要发很长时间初始化节点。

私有链

创建自己的以太币私有测试网络,通常也称为私有链,我们可以用它来作为一个测试环境来开发、调试和测试智能合约。通过Geth很容易就可以创建一个属于自己的测试网络,以太币想挖多少挖多少,也免去了同步正式网络的整个区块链数据。

开发者网络(模式)

相比私有链,开发者网络(模式)下,会自动分配一个有大量余额的开发者账户给我们使用。

模拟环境

另一个创建测试网络的方法是使用testrpc,testrpc是在本地使用内存模拟的一个以太坊环境,对于开发调试来说,更方便快捷。而且testrpc可以在启动时帮我们创建10个存有资金的测试账户。进行合约开发时,可以在testrpc中测试通过后,再部署到Geth节点中去。

testrpc 现在已经并入到Truffle 开发框架中,现在名字是Ganache CLI。

DAO

  • Decentralized Autonomous Organization(去中心化的自治组织):建立在代码之上,基于共识协议来维护组织制度。
  • The DAO:一个DAO组织

美链

美链是一个部署在以太坊上的智能合约,有自己的代币BEC。

ABI

ABI 全称是 Application Binary Interface,应用程序二进制接口,简单来说就是以太坊的合约调用时的接口说明。

从外部施加给以太坊的行为都称之为向以太坊网络提交了一个交易,调用合约函数其实是向合约地址(账户)提交了一个交易,这个交易有一个附加数据,这个附加的数据就是ABI的编码数据。因此要想和合约交互,就离不开ABI数据。

演示调用set(1)时,这个交易附带的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;

contract SimpleStorage {

uint storedData;

function set(uint x) public {
storedData = x;
}

function get() public constant returns (uint) {
return storedData;
}
}

部署到以太坊网络上后,通过参数1调用set函数,打开etherscan查看实时交易数据:

这个数据就是ABI的编码数据:

1
0x60fe47b10000000000000000000000000000000000000000000000000000000000000001

这个数据可以分成两个子部分:

  1. 函数选择器(4字节):0x60fe47b1
  2. 第一个参数(32字节):00000000000000000000000000000000000000000000000000000000000000001

函数选择器值实际是对函数签名字符串进行sha3(keccak256)哈希运算之后,取前4个字节,用代码表示就是(参数部分则是使用对应的16进制数):

1
bytes4(sha3(“set(uint256)”)) == 0x60fe47b1

获得函数对应的ABI有两种方法:

一个是 solidity 提供了ABI的相关API,用来直接得到ABI编码信息,这些函数有:

  • abi.encode(…) returns (bytes):计算参数的ABI编码。
  • abi.encodePacked(…) returns (bytes):计算参数的紧密打包编码
  • abi. encodeWithSelector(bytes4 selector, …) returns (bytes): 计算函数选择器和参数的ABI编码
  • abi.encodeWithSignature(string signature, …) returns (bytes): 等价于* abi.encodeWithSelector(bytes4(keccak256(signature), …)

通过ABI编码函数可以在不用调用函数的情况下,获得ABI编码值,下面通过一段代码来看看这些方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.24;

contract testABI {
uint storedData;

function set(uint x) public {
storedData = x;
}

function abiEncode() public constant returns (bytes) {
abi.encode(1); // 计算1的ABI编码
return abi.encodeWithSignature("set(uint256)", 1); //计算函数set(uint256) 及参数1 的ABI 编码
}
}

另一个web3提供相应的API,例如使用web3计算函数选择器的方式如下:

1
web3.eth.abi.encodeFunctionSignature('myMethod(uint256,string)');

Dapp

以太坊社区把基于智能合约的应用称为去中心化的应用程序(Decentralized App)。如果我们把区块链理解为一个不可篡改的数据库,智能合约理解为和数据库打交道的程序,那就很容易理解Dapp了,一个Dapp不单单有智能合约,比如还需要有一个友好的用户界面和其他的东西。

Truffle是Dapp开发框架,他可以帮我们处理掉大量无关紧要的小事情,让我们可以迅速开始写代码-编译-部署-测试-打包DApp这个流程。

DAPP和传统App关键是后端部分不同,后端不再是一个中心化的服务器,而是分布式网络上任意节点,注意可以是任意一个节点,在应用中给节点发送的请求通常称为交易,交易和中心化下的请求有几个很大的不同是:交易的数据经过用户个人签名之后发送到节点,节点收到交易请求之后,会把请求广播到整个网络,交易在网络达成共识之后,才算是真正的执行。中心化下的请求大多数都是同步的(及时拿到结果),而交易大多数是异步的,这也是在开发去中心应用时需要注意的地方,从节点上获得数据状态(比如交易的结果),一般是通过事件回调来获得。

总结

以太坊是平台,它让我们方便的使用区块链技术开发去中心化的应用,在这个应用中,使用Solidity来编写和区块链交互的智能合约,合约编写好后之后,我们需要用以太坊客户端用一个有余额的账户去部署及运行合约(使用Truffle框架可以更好的帮助我们做这些事情了)。为了开发方便,我们可以用Geth或testrpc来搭建一个测试网络。

爬百度图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import requests
import re
import os


def get_page_url(url, param):
response = requests.get(url, params=param)
response.encoding = 'utf-8'
return response.text


def parse_page(str):
pattern = re.compile('"middleURL":"(.*?)",') #利用正则匹配图片url
url_list = re.findall(pattern, str)
return url_list


def run(keyword, path, num):
url = "https://image.baidu.com/search/acjson"
i = 0
# size = 0
# if int(num) < 30:
# size = int(num)
# else:
# size = 30
for j in range(0, int(num), 30):
params = {"ipn": "rj", "tn": "resultjson_com", "word": keyword, "pn": str(j)}
html = get_page_url(url, params)
lists = parse_page(html)
print(lists)
for item in lists:
try:
img_data = requests.get(item, timeout=10).content
with open(path + "/" + str(i) + ".jpg", "wb") as f:
f.write(img_data)
f.close()
i = i+1
except requests.exceptions.ConnectionError:
print('can not download')
continue


def make_dir(keyword):
path = '百度图片/'
path = path+keyword
is_exists = os.path.exists(path)
if not is_exists:
os.makedirs(path)
return path
else:
print(path + '目录已存在')
return path


def main():
keyword = input("input keyword about images you want to download: ")
num = input("input the number that you want: ")
path = make_dir(keyword)
run(keyword, path, num)


if __name__ == '__main__':
main()

爬百度资讯文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import requests
import re
import os
from pyquery import PyQuery as pq


def get_page(url):
headers = {
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36',
'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'max-age=0',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'
}
response = requests.get(url=url,headers=headers)
response.encoding = "utf-8"
# print(response.status_code)
if response.status_code == 200:
return response.text
return None

if __name__ == '__main__':
keyword = input("input keyword about news that you want to download: ")
numStart = input("input the start number that you want: ")
numEnd = input("input the end number that you want: ")

for page in range(int(numStart), int(numEnd)):
url = 'https://www.baidu.com/s?rtt=1&bsst=1&cl=2&tn=news&medium=2&word=' + keyword + '&pn=' + str(page*10)
result = pq(get_page(url))
print('\nnow the page is '+str(page) + ' and the url is ' + url)
for item in range(result('.result').size()):
url = pq(result('.result').eq(item).html())('a').attr('href')
try:
# 下载文章
d = pq(url=url)
title = d('.article-title').text()
print('now the article is ' + title)
print('now the url is ' + url + '\n')
if not os.path.exists(title):
os.mkdir(title)
fobj = open(title + '/' + title + '.txt', 'w+')
fobj.write(d('.article-title').text())
for i in range(d('.bjh-p').size()):
fobj.write('\n' + d('.bjh-p').eq(i).text())
fobj.close()
# 下载图片
for imgIndex in range(d('.img-container').size()):
img_data = requests.get(pq(d('.img-container').eq(imgIndex).html())('img').attr('src'), timeout=10).content
with open(title + "/" + str(imgIndex) + ".jpg", "wb") as f:
f.write(img_data)
f.close()
except Exception as e:
print('there is a error when parsing this article, maybe it is not exist.')

比较目录差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import java.security.MessageDigest
import java.util.stream.Collectors

class SimilarCalculate {
static void main(String[] args) {
if (args == null || args.length < 2) {
return
}
String path1 = args[0]
String path2 = args[1]
compareByCode(path1, path2)
println("--------------------------------------")
compareByShell(path1, path2)
}

private static void compareByCode(String path1, String path2) {
def f1 = new File(path1)
def f2 = new File(path2)
int diffCount1, nonCount1, diffCount2, nonCount2
(diffCount1, nonCount1) = compareDir(f1, f2)
(diffCount2, nonCount2) = compareDir(f2, f1)
println("Value: $diffCount1, $nonCount1, $diffCount2, $nonCount2")
printResult(f1, diffCount1 + nonCount1 + nonCount2)
}

private static List<Integer> compareDir(File src, File des) {
if (!src.isDirectory() || !des.isDirectory()) {
return
}
int diffCount = 0
int nonCount = 0
src.eachFileRecurse {
if (it.isFile()) {
String subPath = src.relativePath(it)
File desFile = new File(des, subPath)
if (desFile.exists()) {
if (!fileSame(it, desFile)) {
diffCount++
}
} else {
nonCount++
}
}
}
return [diffCount, nonCount]
}

private static void printResult(File origin, int diff) {
int total = 0
origin.eachFileRecurse {
if (it.isFile()) {
total++
}
}
println("Diff: ${diff}, Total: ${total}, Percentage: ${diff * 1f / total}")
}

private static void compareByShell(String path1, String path2) {
def f1 = new File(path1)
def f2 = new File(path2)
createDir(f1, f2)
createDir(f2, f1)
String result = "diff -rq $path1 $path2".execute().text.trim()
printResult(f1, getLineNumberByIo(result))
}

private static int getLineNumberByIo(String target) {
LineNumberReader lnr = new LineNumberReader(new CharArrayReader(target.toCharArray()))
lnr.skip(Long.MAX_VALUE)
lnr.close()
return lnr.getLineNumber() + 1
}

private static void createDir(File src, File des) {
if (!src.isDirectory() || !des.isDirectory()) {
return
}
src.eachFileRecurse {
if (it.isDirectory()) {
String subPath = src.relativePath(it)
File newFile = new File(des, subPath)
if (!newFile.exists()) {
// println("Create path in ${des.absolutePath}: $subPath")
newFile.mkdirs()
}
}
}
}

// --------------------------------------------------------------------------------------------------------- //
private static boolean fileSame(File f1, File f2) {
return getMD5(f1) == getMD5(f2)
// String s1 = is2String(new FileInputStream(f1))
// String s2 = is2String(new FileInputStream(f2))
// return s1 == s2
}

private static String is2String(InputStream is) {
return new BufferedReader(new InputStreamReader(is)).lines().parallel().collect(Collectors.joining("\n"));
}

private static String getMD5(File file) {
FileInputStream fileInputStream = null
try {
MessageDigest MD5 = MessageDigest.getInstance("MD5")
fileInputStream = new FileInputStream(file)
byte[] buffer = new byte[8192]
int length
while ((length = fileInputStream.read(buffer)) != -1) {
MD5.update(buffer, 0, length)
}
return new String(encodeHex(MD5.digest()))
} catch (Exception e) {
e.printStackTrace()
return null
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close()
}
} catch (IOException e) {
e.printStackTrace()
}
}
}

/**
* Used to build output as Hex
*/
private static final char[] DIGITS_LOWER =
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];

/**
* Used to build output as Hex
*/
private static final char[] DIGITS_UPPER =
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];

public static char[] encodeHex(final byte[] data) {
return encodeHex(data, true);
}

public static char[] encodeHex(final byte[] data, final boolean toLowerCase) {
return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
}

protected static char[] encodeHex(final byte[] data, final char[] toDigits) {
final int l = data.length;
final char[] out = new char[l << 1];
// two characters form the hex value.
for (int i = 0, j = 0; i < l; i++) {
out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
out[j++] = toDigits[0x0F & data[i]];
}
return out;
}
}

安装与配置

环境变量:

  • GOROOT:Go的安装目录
  • GOPATH:Go的工作区的集合,通过go get下载的扩展包会放在其中
  • GOBIN:存放Go程序的可执行文件
  • PATH:export PATH=$PATH:$GOROOT:/bin:$GOBIN

基本规则

工作区

工作区是放置Go源码文件的目录。一般情况下,Go源码文件都需要存放到工作区中。但是对于命令源码文件来说,这不是必须的。

每一个工作区的结构都类似包含:golib/,src/,pkg/,bin/

  • src目录用于存放源码文件;以代码包为组织形式
  • pkg目录用于存放归档文件(名称以.a为后缀的文件) 所有归档文件都会被存放到该目录下的平台相关目录中,用样以代码包为组织形式
  • 平台相关目录:两个隐含的Go语言环境变量:GOOS和GOARCH

命令基础

  • go build 用于编译源码文件、代码包、依赖包
  • go run 可以编译并运行Go源码文件
  • go get 主要是用来动态获取远程代码包

基础语法

基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

const NAME string = "Hello" //常量
var a string = "aaa" //全局变量的声明和赋值
type b int //一般类型的声明

type User struct {
} //结构体的声明

type IUser interface {
} //接口的声明

func fun() {

} //函数声明

func main() {
fmt.Println("hello")
}

package

  • package是最基本得分发单位工程管理中依赖关系的体现
  • 每个Go语言的代码文件开头都拥有一个package声明,表示源码文件所属的代码包
  • 要生成GO语言可执行程序,必须要有main的package包,且必须在该包下有**main()**函数
  • 同一路径下只能存在一个package,一个package可以拆分成多个源文件组成

import

1
2
import "pac1"
import "pac2"

等价于:

1
2
3
4
import (
"pac1"
"pac2"
)
  • 如果一个main导入其他包,包将被顺序导入
  • 如果导入的包中有其他包(包B),会首先导入B包,然后初始化B包中的常量与变量,最后如果B中有init,会自动执行init();
  • 所有包导入完成之后才会对main中常量和变量进行初始化,然后执行main中的init函数(如果存在),最后执行main函数;
  • 如果一个包被导入多次,则该包只会被导入一次。

变量和常量

变量声明

第一种,指定变量类型,声明后若不赋值,使用默认值。

1
2
var v_name v_type
v_name = value

第二种,根据值自行判定变量类型。

1
var v_name = value

第三种,省略var, :=左侧的变量不应该是已经声明过的,否则会导致编译错误。这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明。

1
v_name := value

如果想要交换两个变量的值,则可以简单地使用 a, b = b, a,两个变量的类型必须是相同。

多变量声明

1
2
3
4
5
6
7
8
9
10
11
12
13
//类型相同多个变量, 非全局变量
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3

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

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

// 这种因式分解关键字的写法一般用于声明全局变量
var (
vname1 v_type1
vname2 v_type2
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

var x, y int
var ( // 这种因式分解关键字的写法一般用于声明全局变量
a int
b bool
)

var c, d int = 1, 2
var e, f = 123, "hello"

//这种不带声明格式的只能在函数体中出现
//g, h := 123, "hello"

func main(){
g, h := 123, "hello"
println(x, y, a, b, c, d, e, f, g, h)
}
以上实例执行结果为:

0 0 0 false 1 2 123 hello 123 hello

值类型和引用类型

所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值,当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝, 值类型的变量的值存储在栈中。

更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。这个内存地址为称之为指针,这个指针实际上也被存在另外的某一个字中。同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。

当使用赋值语句 r2 = r1 时,只有引用(地址)被复制,如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容。

常量

1
2
3
4
5
6
7
8
9
10
11
const identifier [type] = value

// 多个相同类型的声明可以简写为:
const c_name1, c_name2 = value1, value2

// 常量还可以用作枚举:
const (
Unknown = 0
Female = 1
Male = 2
)

字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。

iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量。iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

iota 可以被用作枚举值:

1
2
3
4
5
const (
a = iota
b = iota
c = iota
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

1
2
3
4
5
const (
a = iota
b
c
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}

// 以上实例运行结果为:
//0 1 2 ha ha 100 100 7 8

再看个有趣的的 iota 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"
const (
i=1<<iota
j=3<<iota
k
l
)

func main() {
fmt.Println("i=",i)
fmt.Println("j=",j)
fmt.Println("k=",k)
fmt.Println("l=",l)
}

//以上实例运行结果为:
//i= 1
//j= 6
//k= 12
//l= 24

//i=1:左移 0 位,不变仍为 1;
//j=3:左移 1 位,变为二进制 110, 即 6;
//k=3:左移 2 位,变为二进制 1100, 即 12;
//l=3:左移 3 位,变为二进制 11000,即 24。

new和make

1
2
func make(t Type, size ...IntegerType) Type
func new(Type) *Type

二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零(new不常用)。

流程控制

条件语句

if-else/switch

语句 描述
if bool {} else {} 判断语句
switch switch 语句用于基于不同条件执行不同动作。
1
2
3
4
5
6
7
8
9
10
11
if true {

} else {

}
switch expr {
case 1:
break
default:
break
}

select

1
2
3
4
5
6
7
8
9
10
11
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
// 省略若干条语句
select {
case e1 := <-ch1:
fmt.Printf("1th case is selected. e1=%v.\n", e1)
case e2 := <-ch2:
fmt.Printf("2th case is selected. e2=%v.\n", e2)
default:
fmt.Println("No data!")
}

如果该select语句被执行时通道ch1和ch2中都没有任何数据,那么肯定只有default case会被执行。但是,只要有一个通道在当时有数据就不会轮到default case执行了。显然,对于包含通道接收操作的case来讲,其执行条件就是通道中存在数据(或者说通道未空)。如果在当时有数据的通道多于一个,那么Go语言会通过一种伪随机的算法来决定哪一个case将被执行。

defer

defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。

  1. defer执行顺序为先进后出
1
2
3
4
5
6
7
8
9
10
11
12
13
func deferIt() {
defer func() {
fmt.Print(1)
}()
defer func() {
fmt.Print(2)
}()
defer func() {
fmt.Print(3)
}()
fmt.Print(4)
}
// deferIt函数的执行会使标准输出上打印出4321。
  1. 当defer被声明时,其参数就会被实时解析,函数值和函数参数被求值,但函数不会立即调用
1
2
3
4
5
6
func a() {
i := 0
defer fmt.Println(i)
i++
return
} // 0
1
2
3
4
5
6
7
8
9
func deferIt3() {
f := func(i int) int {
fmt.Printf("%d ",i)
return i * 10
}
for i := 1; i < 5; i++ {
defer fmt.Printf("%d ", f(i))
}
} // 1 2 3 4 40 30 20 10
  1. defer可以读取有名返回值
1
2
3
4
func c() (i int) {
defer func() { i++ }()
return 1
} // 1 2
1
2
3
4
5
6
7
func deferIt4() {
for i := 1; i < 5; i++ {
defer func() {
fmt.Print(i)
}()
}
} // 5555
1
2
3
4
5
6
7
func deferIt4() {
for i := 1; i < 5; i++ {
defer func(n int) {
fmt.Print(n)
}(i)
}
} // 4321

循环语句

go语言没有while语句,可用for实现。

1
2
3
for i := 1; i < 101; i++ {

}

函数

go中函数可以返回多个值:

1
2
3
4
func function_name([parameter type]) [return_types...] {
// 函数体
[return x...]
}

函数作为变量:

1
2
3
4
5
6
7
8
9
func main(){
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}

/* 使用函数 */
fmt.Println(getSquareRoot(9))
}

Go 语言支持匿名函数,可作为闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}

func main(){
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()

/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())

/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}

既然可以在代表函数的变量上实施调用表达式,那么在匿名函数上肯定也是可行的。因为它们的本质是相同的。示例如下(这里的result变量的类型不是函数类型,而与后面的匿名函数的结果类型是相同的):

1
2
3
var result = func(part1 string, part2 string) string {
return part1 + part2
}("1", "2")

方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。

1
2
3
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 定义结构体 */
type Circle struct {
radius float64
}

func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}

数据类型

bool

1
var b bool = true/false

数字类型

  • uint: 32或64位
  • int: 同于uint
  • uint8
  • uint16
  • uint32
  • uint64
  • int8
  • int16
  • int32
  • int64
  • float32
  • float64
  • complex64: 32位实数和虚数
  • complex128: 64位实数和虚数
  • byte: 类似uint8
  • rune: 类似int32
  • uintptr: 无符号整型,用于存放一个指针

字符串类型

string:Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本。

数组类型

1
2
3
var variable_name [SIZE] variable_type

var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:

1
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

多维数组:

1
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type

初始化二维数组:

1
2
3
4
5
a = [3][4]int{  
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}

向函数传递数组:

1
2
3
4
5
// 形参设定数组大小:
void myFunction(param [10]int) {}

// 形参未设定数组大小:
void myFunction(param []int)

指针

空指针为nil,指代零值或空值。

1
2
3
4
5
var var_name *var-type
// 指针数组
var var_name [5]*var-type
// 指向指针的指针
var var_name **var-type

指针作为函数参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200

fmt.Printf("交换前 a 的值 : %d\n", a )
fmt.Printf("交换前 b 的值 : %d\n", b )

/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &b 指向 b 变量的地址
*/
swap(&a, &b);

fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}

func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址的值 */
*x = *y /* 将 y 赋值给 x */
*y = temp /* 将 temp 赋值给 y */
}

结构体类型

用法:

1
2
3
4
5
6
7
8
9
10
type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}

variable_name := structure_variable_type {value1, value2...valuen}
// 或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

访问时通过.操作访问结构体成员。

切片类型

1
2
3
4
5
6
7
8
9
10
11
12
// 声明一个未指定大小的数组来定义切片:
var identifier []type

// 使用make()函数来创建切片:
var slice1 []type = make([]type, len)

// 也可以简写为:
slice1 := make([]type, len)

// 也可以指定容量,其中capacity为可选参数。
make([]T, length, capacity)
// 这里 len 是数组的长度并且也是切片的初始长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
s :=[] int {1,2,3 } 
// 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3

s := arr[:]
// 初始化切片s,是数组arr的引用

s := arr[startIndex:endIndex]
// 将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片

s := arr[startIndex:]
// 缺省endIndex时将表示一直到arr的最后一个元素

s := arr[:endIndex]
// 缺省startIndex时将表示从arr的第一个元素开始

s1 := s[startIndex:endIndex]
// 通过切片s初始化切片s1

s :=make([]int,len,cap)
// 通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
  • 切片是可索引的,并且可以由 len() 方法获取长度。切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
  • 一个切片在未初始化之前默认为 nil,长度为 0.
  • 对于底层数组容量是 k 的切片 slice[i:j] 来说:长度: j-i;容量: k-i.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
var numbers []int

printSlice(numbers)

if(numbers == nil){
fmt.Printf("切片是空的")
}
}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
// 以上实例运行输出结果为:

// len=0 cap=0 slice=[]
// 切片是空的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
printSlice(numbers) // len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]

/* 打印原始切片 */
fmt.Println("numbers ==", numbers) // numbers == [0 1 2 3 4 5 6 7 8]

/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4]) // numbers[1:4] == [1 2 3]

/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3]) // numbers[:3] == [0 1 2]

/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:]) // numbers[4:] == [4 5 6 7 8]

numbers1 := make([]int,0,5)
printSlice(numbers1) // len=0 cap=5 slice=[]

/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2) // len=2 cap=9 slice=[0 1]

/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3) // len=3 cap=7 slice=[2 3 4]
}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

append的用法有两种:

  • slice = append(slice, elem1, elem2)
  • slice = append(slice, anotherSlice…)

append要点:

  • 用append把一个或多个元素添加在一个slice的后面;
  • append的slice有一个underlying array,此即slice和array的关系;
  • 另外slice有一个length和capability的概念;
  • 如果slice还有剩余的空间,可以添加这些新元素,那么append就将新的元素放在slice后面的空余空间中;
  • 如果slice的空间不足以放下新增的元素,那么就需要重现创建一个数组;这时可能是alloc、也可能是realloc的方式分配这个新的数组;也就是说,这个新的slice可能和之前的slice在同一个起始地址上,也可能不是一个新的地址。——通常而言,是一个新的地址。
  • 分配了新的地址之后,再把原来slice中的元素逐个拷贝到新的slice中,并返回。
  • 当一个append执行达到了切片的容量,它会自动扩容为原来的两倍大小

copy用法:

  • copy(to_slide, from_slide)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func main() {
var numbers []int
printSlice(numbers) // len=0 cap=0 slice=[]

/* 允许追加空切片 */
numbers = append(numbers, 0) // len=1 cap=1 slice=[0]
printSlice(numbers)

/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers) // len=2 cap=2 slice=[0 1]

/* 同时添加多个元素 */
numbers = append(numbers, 2, 3, 4)
printSlice(numbers) // len=5 cap=6 slice=[0 1 2 3 4]

numbers = append(numbers, 5, 6)
printSlice(numbers) // len=7 cap=12 slice=[0 1 2 3 4 5 6]

/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)

/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1, numbers)
printSlice(numbers1) // len=7 cap=24 slice=[0 1 2 3 4 5 6]
}

func printSlice(x []int) {
fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

Range

range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对的 key 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
//在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}

Map类型

1
2
3
4
5
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func main() {
var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string)

/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap [ "France" ] = "Paris"
countryCapitalMap [ "Italy" ] = "罗马"
countryCapitalMap [ "Japan" ] = "东京"
countryCapitalMap [ "India " ] = "新德里"

/*使用键输出地图值 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [country])
}

/*查看元素在集合中是否存在 */
captial, ok := countryCapitalMap [ "美国" ] /*如果确定是真实的,则存在,否则不存在 */
/*fmt.Println(captial) */
/*fmt.Println(ok) */
if (ok) {
fmt.Println("美国的首都是", captial)
} else {
fmt.Println("美国的首都不存在")
}
}

delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
/* 创建map */
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}

fmt.Println("原始地图")

/* 打印地图 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}

/*删除元素*/ delete(countryCapitalMap, "France")
fmt.Println("法国条目被删除")

fmt.Println("删除元素后地图")

/*打印地图*/
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
}

List

包含于conatiner/list包中。

初始化:

1
2
3
4
5
// 1. New方法初始化list
变量名 := list.New()

// 2. 通过声明初始化list
var 变量名 list.List

添加元素:

1
2
3
4
5
6
7
8
9
10
11
12
// 追加新元素到末尾,返回该元素指针
func (l *List) PushBack(v interface{}) *Element
// 追加另一个列表到末尾
func (l *List) PushBackList(other *List)
// 添加新元素到开头,返回该元素指针
func (l *List) PushFront(v interface{}) *Element
// 添加另一个列表到开头
func (l *List) PushFrontList(other *List)
// 在mark后面插入新元素,返回新元素指针
func (l *List) InsertAfter(v interface{}, mark *Element) *Element
// 在mark前插入新元素,返回新元素指针
func (l *List) InsertBefore(v interface{}, mark *Element) *Element

移动元素:

1
2
3
4
5
6
7
8
// 移动e到mark之后
func (l *List) MoveAfter(e, mark *Element)
// 移动e到mark之前
func (l *List) MoveBefore(e, mark *Element)
// 移动e到末尾
func (l *List) MoveToBack(e *Element)
// 移动e到开头
func (l *List) MoveToFront(e *Element)

访问元素:

1
2
3
4
// 返回结尾元素
func (l *List) Back() *Element
// 返回开头元素
func (l *List) Front() *Element

遍历列表:

1
2
3
4
// 返回下一个元素,如果没有下一个元素,返回nil
func (e *Element) Next() *Element
// 返回前一个元素,如果没有前一个元素,返回nil
func (e *Element) Prev() *Element

获取列表长度:

1
func (l *List) Len() int

移除元素:

1
2
3
4
// 移除e,返回e的值
func (l *List) Remove(e *Element) interface{}
// 清空列表
func (l *List) Init() *List

interface类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
/* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
// ...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type Phone interface {
call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}

func main() {
var phone Phone

phone = new(NokiaPhone)
phone.call()

phone = new(IPhone)
phone.call()
}

Channel类型

Channel创建

channel 使用内置的 make 函数创建:

1
ch := make(chan int)

Channel读写

channel的读写操作(channel 一定要初始化后才能进行读写操作,否则会永久阻塞):

1
2
3
4
5
6
7
8
9
10
11
12
ch := make(chan int)

// write to channel
ch <- x

// read from channel
x <- ch

// another way to read
x = <- ch

value, ok := <- ch1

这里的变量ok的值同样是bool类型的。它代表了通道值的状态,true代表通道值有效,而false则代表通道值已无效(或称已关闭)。如果在接收操作进行之前或过程中通道值被关闭了,则接收操作会立即结束并返回一个该通道值的元素类型的零值。

Channel关闭

golang 提供了内置的 close 函数对 channel 进行关闭操作。

1
2
ch := make(chan int)
close(ch)
  • 关闭一个未初始化(nil) 的 channel 会产生 panic
  • 重复关闭同一个 channel 会产生 panic
  • 向一个已关闭的 channel 中发送消息会产生 panic
  • 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭
  • 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
1
2
3
4
5
6
7
8
9
10
11
12
ch := make(chan int, 10)
ch <- 11
ch <- 12

close(ch)

for x := range ch {
fmt.Println(x) // 11 12
}

x, ok := <- ch
fmt.Println(x, ok) // 0 false

无缓存的 channel

从无缓存的 channel 中读取消息会阻塞,直到有 goroutine 向该 channel 中发送消息;同理,向无缓存的 channel 中发送消息也会阻塞,直到有 goroutine 从 channel 中读取消息。

非缓冲的通道值的初始化方法如下:

1
make(chan int, 0)

有缓存的 channel

有缓存的 channel 的声明方式为指定 make 函数的第二个参数,该参数为 channel 缓存的容量

1
ch := make(chan int, 10)

有缓存的 channel 类似一个阻塞队列(采用环形数组实现)。当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他 goroutine 从中读取消息;相应的,当 channel 中消息不为空时,读取消息不会出现阻塞,当 channel 为空时,读取操作会造成阻塞,直到有 goroutine 向 channel 中写入消息。

1
2
3
4
ch := make(chan int, 3)

// blocked, read from empty buffered channel
<- ch
1
2
3
4
5
6
7
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3

// blocked, send to full buffered channel
ch <- 4

通过 len 函数可以获得 chan 中的元素个数,通过 cap 函数可以得到 channel 的缓存长度。

双向/单向Channel

默认情况下,通道都是双向的,即双向通道。如果数据只能在通道中单向传输,那么该通道就被称作单向通道。在编写类型声明的时候可以这样做:

1
type Receiver <-chan int

类型Receiver代表了一个只可从中接收数据的单向通道类型。这样的通道也被称为接收通道。在关键字chan左边的接收操作符<-形象地表示出了数据的流向。相对应的,如果想声明一个发送通道类型,那么应该这样:

1
type Sender chan<- int

可以把一个双向通道值赋予上述类型的变量:

1
2
3
4
5
6
var myChannel = make(chan int, 3)
var sender Sender = myChannel
var receiver Receiver = myChannel

//但是,反之则是不行的。像下面这样的代码是通不过编译的:
var myChannel1 chan int = sender

单向通道的主要作用是约束程序对通道值的使用方式。

channel应用

广播功能实现

当一个通道关闭时, 所有对此通道的读取的goroutine都会退出阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"fmt"
"time"
)
func notify(id int, channel chan int){
<-channel//接收到数据或通道关闭时退出阻塞
fmt.Printf("%d receive a message.\n", id)
}
func broadcast(channel chan int){
fmt.Printf("Broadcast:\n")
close(channel)//关闭通道}
func main(){
channel := make(chan int,1)
for i:=0;i<10 ;i++ {
go notify(i,channel)
}
go broadcast(channel)
time.Sleep(time.Second)
}

select使用

select用于在多个channel上同时进行侦听并收发消息,当任何一个case满足条件时即执行,如果没有可执行的case则会执行default的case,如果没有指定default case,则会阻塞程序。select的语法如下:

1
2
3
4
5
6
7
select {
case communication clause :
statement(s);
case communication clause :
statement(s); /*可以定义任意数量的 case */default : /*可选 */
statement(s);
}

Select多路复用中:

  • 每个case都必须是一次通信
  • 所有channel表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果任意某个通信可以进行,它就执行;其它被忽略。
  • 如果有多个case都可以运行,Select会随机公平地选出一个执行。其它不会执行。
  • 否则,如果有default子句,则执行default语句。如果没有default子句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package mainimport (
"fmt"
"time")

func doWork(channels *[10]chan int){
for {
select {
case x1 := <-channels[0]:
fmt.Println("receive x1: ",x1)
case x2 := <-channels[1]:
fmt.Println("receive x2: ",x2)
case x3 := <-channels[2]:
fmt.Println("receive x3: ",x3)
case x4 := <-channels[3]:
fmt.Println("receive x4: ",x4)
case x5 := <-channels[4]:
fmt.Println("receive x5: ",x5)
case x6 := <-channels[5]:
fmt.Println("receive x6: ",x6)
case x7 := <-channels[6]:
fmt.Println("receive x7: ",x7)
case x8 := <-channels[7]:
fmt.Println("receive x8: ",x8)
case x9 := <-channels[8]:
fmt.Println("receive x9: ",x9)
case x10 := <-channels[9]:
fmt.Println("receive x10: ",x10)
}
}
}
func main(){
var channels [10]chan int
go doWork(&channels)
for i := 0; i < 10; i++ {
channels[i] = make(chan int,1)
channels[i]<- i
}
time.Sleep(time.Second*5)
}
// 结果如下:
receive x4: 3
receive x10: 9
receive x9: 8
receive x5: 4
receive x2: 1
receive x7: 6
receive x8: 7
receive x1: 0
receive x3: 2
receive x6: 5

类型转换

1
type_name(expression)

错误处理

error

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。error类型是一个接口类型,这是它的定义:

1
2
3
type error interface {
Error() string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package errors

// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}

func main() {
result, err:= Sqrt(-1)

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

自定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 定义一个 DivideError 结构
type DivideError struct {
dividee int
divider int
}

// 实现 `error` 接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}

// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}

func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当被除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}

panic

panic可被意译为运行时恐慌,因为它只有在程序运行的时候才会被“抛出来”,并且,恐慌是会被扩散的,当有运行时恐慌发生时,它会被迅速地向调用栈的上层传递。如果不显式地处理它的话,程序的运行瞬间就会被终止——程序崩溃。内建函数panic可以让我们人为地产生一个运行时恐慌,不过,这种致命错误是可以被恢复的,在Go语言中,内建函数recover就可以做到这一点。

实际上,内建函数panic和recover是天生的一对,前者用于产生运行时恐慌,而后者用于“恢复”它。不过要注意,recover函数必须要在defer语句中调用才有效,因为一旦有运行时恐慌发生,当前函数以及在调用栈上的所有代码都是失去对流程的控制权,只有defer语句携带的函数中的代码才可能在运行时恐慌迅速向调用栈上层蔓延时“拦截到”它。

1
2
3
4
5
defer func() {
if p := recover(); p != nil {
fmt.Printf("Fatal error: %s\n", p)
}
}()

recover()函数会返回一个interface{}类型的值,如果不为nil,那么就说明当前确有运行时恐慌发生,这时我们需根据情况做相应处理。注意,一旦defer语句中的recover函数调用被执行了,运行时恐慌就会被恢复,不论是否进行了后续处理, 所以,我们一定不要只“拦截”不处理。

panic函数可接受一个interface{}类型的值作为其参数,也就是说,可以在调用panic函数的时候可以传入任何类型的值,不过,建议在这里只传入error类型的值,这样它表达的语义才是精确的,更重要的是,当我们调用recover函数来“恢复”由于调用panic函数而引发的运行时恐慌的时候,得到的值正是调用后者时传给它的那个参数,因此,有这样一个约定是很有必要的。

面向对象编程

在Golang的对象可以用一句话总结:“面向对象就是将要处理的数据跟函数进行绑定的方法”。

封装

1
2
3
4
5
6
7
8
9
10
11
12
13
type Foo struct {
baz string
}

// 类似于成员函数
func (f *Foo) echo() {
fmt.Println(f.baz)
}

func main() {
f := Foo{baz: "hello"}
f.echo()
}

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Foo struct {
baz string
}

type Bar struct {
Foo
}

func (f *Foo) echo() {
fmt.Println(f.baz)
}

func main() {
b := Bar{Foo{baz: "hello"}}
b.echo()
}

多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Foo interface {
test()
}

type Bar struct {}
type Baz struct {}

func (b Bar) test() {}
func (b Baz) test() {}

func main() {
var f Foo
f = Bar{}
f = Baz{}
fmt.Println(f)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/obj
// 在其他包引用时,需要把要被引用的标志首字母大写
package obj

import "fmt"

type Person interface {
Buy(value int)
}

type User struct {
Name string
Money int
}

func (user *User) Buy(value int) {
user.Money -= value
fmt.Printf("%s Buy a thing of %d, rest %d\n", user.Name, value, user.Money)
}

type Worker struct {
User
IdW string
}

type Student struct {
IdS string
User
}

// src
func main() {
user := obj.Worker{User: obj.User{Name: "hearing", Money:21}, IdW: "001"}
user.Buy(10)
}

Go并发编程

基础概念

  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

Coroutine(协程)

Coroutine(协程)是一种用户态的轻量级线程,特点如下:

  1. 轻量级线程
  2. 非抢占式多任务处理,由协程主动交出控制权。
  3. 编译器/解释器/虚拟机层面的任务
  4. 多个协程可能在一个或多个线程上运行。
  5. 子程序是协程的一个特例。

不同语言对协程的支持:

  1. C++通过Boost.Coroutine实现对协程的支持
  2. Java不支持
  3. Python通过yield关键字实现协程,Python3.5开始使用async def对原生协程的支持

Goroutine

概述

goroutine说到底其实就是线程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。

在Go语言中,只需要在函数调用前加上关键字go即可创建一个并发任务单元,新建的任务会被放入队列中,等待调度器安排。进程在启动的时候,会创建一个主线程,主线程结束时,程序进程将终止,因此,进程至少有一个线程。main函数里,必须让主线程等待,确保进程不会被终止。go语言中并发指的是让某个函数独立于其它函数运行的能力,一个goroutine是一个独立的工作单元,Go的runtime(运行时)会在逻辑处理器上调度goroutine来运行,一个逻辑处理器绑定一个操作系统线程,因此goroutine不是线程,是一个协程。

  • 进程:一个程序对应一个独立程序空间
  • 线程:一个执行空间,一个进程可以有多个线程
  • 逻辑处理器:执行创建的goroutine,绑定一个线程
  • 调度器:Go运行时中的,分配goroutine给不同的逻辑处理器
  • 全局运行队列:所有刚创建的goroutine队列
  • 本地运行队列:逻辑处理器的goroutine队列

当创建一个goroutine后,会先存放在全局运行队列中,等待Go运行时的调度器进行调度,把goroutine分配给其中的一个逻辑处理器,并放到逻辑处理器对应的本地运行队列中,最终等着被逻辑处理器执行即可。

Go的并发是管理、调度、执行goroutine的方式。默认情况下,Go默认会给每个可用的物理处理器都分配一个逻辑处理器。可以在程序开头使用runtime.GOMAXPROCS(n)设置逻辑处理器的数量。
如果需要设置逻辑处理器的数量,一般采用如下代码设置:runtime.GOMAXPROCS(runtime.NumCPU())。对于并发,Go语言本身自己实现的调度,对于并行,与物理处理器的核数有关,多核就可以并行并发,单核只能并发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"sync"
)

func main(){
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
fmt.Printf("Hello,Go.This is %d\n", i)
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
fmt.Printf("Hello,World.This is %d\n", i)
}
}()
wg.Wait()
}

sync.WaitGroup使用非常简单,使用Add方法设设置计数器为2,每一个goroutine的函数执行完后,调用Done方法减1。Wait方法表示如果计数器大于0,就会阻塞,main函数会一直等待2个goroutine完成再结束。

调度机制

为了实现M:N线程调度机制,Go引入了3个结构体:

  • M:操作系统的内核空间线程
  • G:goroutine对象,G结构体包含调度一个goroutine所需要的堆栈和instruction pointer(IP指令指针),以及其它一些重要的调度信息。每次go调用的时候,都创建一个G对象。
  • P:Processor,调度的上下文,实现M:N调度模型的关键,M必须拿到P才能对G进行调度,P限定了go调度goroutine的最大并发度。每一个运行的M都必须绑定一个P。

P的个数是GOMAXPROCS(最大256),启动时固定,一般不修改; M的个数和P的个数不一定相同(会有休眠的M或者不需要太多的M);每一个P保存着本地G任务队列,也能使用全局G任务队列。

runtime包

  • runtime.Gosched()用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。
  • 调用runtime.Goexit()将立即终止当前goroutine执⾏,调度器确保所有已注册defer延迟调用被执行。
  • 调用runtime.GOMAXPROCS()用来设置可以并行计算的CPU核数的最大值,并返回设置前的值。

死锁

Go程序中死锁是指所有的goroutine在等待资源的释放,Goroutine死锁产生的原因如下:

  • 只在单一的goroutine里操作无缓冲信道,一定死锁
  • 非缓冲信道上如果发生流入无流出,或者流出无流入,会导致死锁

因此,解决死锁的方法有:

  • 取走无缓冲通道的数据或是发送数据到无缓冲通道
  • 使用缓冲通道

定风波·湖村晚

苍耳

  湖面蒹葭荡影重,黄昏渐映水寒清。远处人家芦絮乱,亲唤,小童归去老村惊。      候鸟返巢双戏景,微冷,农家冬至夜燃灯。灯影幢幢人影瘦,浊酒,菜花香入梦回轻。

忆王孙·何事

苍耳

风吹雨落夜眠惊,半醒难分睡意轻。
梦入江南画柳情。
呓呢声,侧卧窗帘闲点灯。

忆王孙·洛书记

苍耳

墨花纸染化龙飞,门掩窗寒瑟瑟吹。
今日闲情久假回。笔轻挥,平仄无声春似归。

青玉案·初八天

苍耳

空山雾隐闻声去。犬出没、人来处。寒意不消听落雨。楼台风闹,绿拂三五。未见春些许。
夜来明灭灯如诉。酒过三巡醒梁祝。辗转难眠人不宿。贪嗔爱恨,七情六欲。梦里花开故。

浣溪沙·凡草

苍耳

媚影妖娆咏作仙,千般颜色惹人怜。一朝散尽满枝闲。
最是平常孤草晃,青青风里荡花间。春泥一化百芳翩。

青玉案·秋书

苍耳

  林间阡陌行清秋。道凉意,风如哭。梢末黄鹂轻唤舞。却身反顾,碧空云天,尽是千金缕。      梧桐也似金黄雨,银杏终将叶颜枯。昨岁不堪风落处。如此这般,盈盈笑语,只是秋书苦。

潇湘梦-如仙

苍耳

风如烟,卿画颜。
小楼帘下舞蹁跹。
浊酒几杯还故里,
青丝如梦九天仙。

点绛唇-冬醉

苍耳

2018/11/20

雨霁天回,庭前思是春疏影。梧桐满径,凉却知冬盛。
念道寻禅,难揣天书命。趁酒兴,醉饮小令,一曲南山梦。

忆王孙·九月随想

苍耳

秋风吹散雨携情,九月萧萧孤叶清。
半掩楼台午梦惊。
呓呢声,侧卧窗帘闲点灯。