Here is a simple example of this workflow:
# initial setup
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 6, 3, 1, 1)
        self.linear = nn.Linear(1, 10) # initialize with any value
        
    def forward(self, x):
        x = self.conv(x)
        x = x.view(x.size(0), -1)
        # unknown shape here as I don't want to calculate it manually
        print(x.shape)
        x = self.linear(x) # will raise a shape mismatch, but that's OK
        return x

model = MyModel()
x = torch.randn(1, 3, 224, 224)
out = model(x)
# > RuntimeError: mat1 and mat2 shapes cannot be multiplied (1x301056 and 1x10)

# Now change the in_features to 301056 
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 6, 3, 1, 1)
        self.linear = nn.Linear(301056, 10)
        
    def forward(self, x):
        x = self.conv(x)
        x = x.view(x.size(0), -1)
        x = self.linear(x)
        return x

model = MyModel()
out = model(x) # works

In the first iteration I’m expecting a shape mismatch if the shape calculation and will refine the in_features later.
Of course one could calculate the output shape of the single conv layer manually, but this is just used as an example in case the previous layers are more complicated.
In the latest PyTorch releases you could also use the “lazy” layers e.g. nn.LazyLinear which don’t expect the in_features anymore.